Skip to content

Achievements

The Cartridge Achievements system enables games to reward players for completing achievements with built-in progress tracking and Cartridge points.

Key Features

  • Packages: Games can define achievements using the provided Cairo packages
  • Rewards: Games can reward players with Cartridge points for completing achievements
  • Profile: Players can view their achievements and scores without leaving the game

Benefits for Game Developers

  • Simplicity: Easy integration with existing Starknet smart contracts and Dojo
  • Cost-effectiveness: Achievements are event-based, no additional storage is required
  • Performance (coming soon): Plugin attached to Torii to improve achievement computation performance

How It Works

Achievements consist of:

  • Achievement Definition: A unique identifier, title, description, and set of tasks
  • Tasks: Each task has an identifier, total target, and description
  • Completion: A task completes when enough progression has been made; an achievement completes when all its tasks are completed

For the complete implementation, see the GitHub repository.

Setup

Dependencies

Add the Cartridge achievement package as a dependency in your Scarb.toml:

[dependencies]
starknet = "2.8.4"
dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.5.1" }
achievement = { git = "https://github.com/cartridge-gg/arcade", tag = "v1.5.1" } 
 
[[target.starknet-contract]]
build-external-contracts = [
    "dojo::world::world_contract::world",
    "achievement::events::index::e_TrophyCreation", 
    "achievement::events::index::e_TrophyProgression", 
]

Torii Configuration

The progression events require historical event management by Torii, meaning every event will remain available in the event_messages_historical table:

rpc = <YOUR-RPC-URL>
world_address = <YOUR-WORLD-ADDRESS>
 
[indexing]
...
 
[sql] 
historical = ["<YOUR-NAMESPACE>-TrophyProgression"] 

Creating Achievements

Emit events to define your achievements using the provided Starknet components:

#[dojo::contract]
pub mod Actions {
    use achievement::components::achievable::AchievableComponent; 
    use achievement::types::task::{Task, TaskTrait}; 
    component!(path: AchievableComponent, storage: achievable, event: AchievableEvent); 
    impl AchievableInternalImpl = AchievableComponent::InternalImpl<ContractState>; 
 
    #[storage]
    struct Storage {
        #[substorage(v0)]
        achievable: AchievableComponent::Storage, 
    }
 
    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        AchievableEvent: AchievableComponent::Event, 
    }
 
    fn dojo_init(self: @ContractState) {
        // [Event] Emit all Achievement creation events
        let world = self.world("<YOUR-NAMESPACE>");
        let task_id = 'TASK_IDENTIFIER';
        let task_target = 100;
        let task = TaskTrait::new(task_id, task_target, "Do something 100 times");
        let tasks: Span<Task> = array![task].span();
 
        self.achievable 
            .create( 
                world, 
                id: 'ACHIEVEMENT_IDENTIFIER', 
                hidden: false, 
                index: 0, 
                points: 10, 
                start: 0, 
                end: 0, 
                group: 'Group', 
                title: "Achievement title", 
                description: "The achievement description", 
                tasks: tasks, 
                data: "", 
                icon: 'fa-trophy', 
            ); // [!code focus]
        } // [!code focus]
    }
}

AchievableComponent.create Parameters

AchievableComponent.create(
    self: @ComponentState<TContractState>,
    world: WorldStorage,
    id: felt252,
    hidden: bool,
    index: u8,
    points: u16,
    start: u64,
    end: u64,
    group: felt252,
    icon: felt252,
    title: felt252,
    description: ByteArray,
    tasks: Span<Task>,
    data: ByteArray,
)
ParameterDescription
idUnique achievement identifier
hiddenWhether to hide the achievement in the controller UI
indexPage index within the group for display ordering
pointsCartridge points to reward the player
startStart timestamp for ephemeral achievements (0 for everlasting)
endEnd timestamp for ephemeral achievements (0 for everlasting)
groupAchievement group for organizing achievements together
iconFontAwesome icon name (e.g., fa-trophy)
titleAchievement title
descriptionAchievement description
tasksAchievement tasks (see Task type below)
dataReserved for future use

See also AchievableComponent

Task Type

pub struct Task {
    id: felt252,
    total: u32,
    description: ByteArray,
}
ParameterDescription
idTask identifier (can be shared across achievements)
totalTarget count for task completion
descriptionTask description

See also Task

Tracking Progression

Emit events to track player progress on tasks:

#[dojo::contract]
pub mod Actions {
    use achievement::store::{Store, StoreTrait};
    // ...
    #[abi(embed_v0)]
    impl ActionsImpl of IActions<ContractState> {
        fn play(ref self: ContractState, do: felt252) {
            let world = self.world(@"<YOUR-NAMESPACE>")
            // If the player meets the task requirement, emit an event to track the progress
            if do === 'something' {
                let store = StoreTrait::new(world);
                let player_id = starknet::get_caller_address();
                let task_id = 'TASK_IDENTIFIER';
                let count = 1;
                let time = starknet::get_block_timestamp();
                store.progress(player_id.into(), task_id, count, time);
            }
        }
    }
}

AchievableComponent.progress Parameters

AchievableComponent.progress(
    self: @ComponentState<TContractState>,
    world: WorldStorage,
    player_id: felt252,
    task_id: felt252,
    count: u32,
)
ParameterDescription
player_idThe player identifier
task_idThe task identifier
countProgression count to add

Client Integration

Controller Configuration

Configure the controller with the required parameters:

new ControllerConnector({
  url,
  rpc,
  profileUrl,
  namespace: "dopewars", 
  slot: "ryomainnet", 
  theme,
  colorMode,
  policies,
});

Opening the Achievements Page

Add a button to open the achievements page in your game client:

const { connector } = useAccount();
 
const handleClick = useCallback(() => {
  if (!connector?.controller) {
    console.error("Connector not initialized");
    return;
  }
  connector.controller.openProfile("achievements");
}, [connector]);

Testing

Add the corresponding events to your namespace definition in tests:

fn namespace_def() -> NamespaceDef {
    NamespaceDef {
        namespace: "namespace", resources: [
            // ...
            TestResource::Event(achievement::events::index::e_TrophyCreation::TEST_CLASS_HASH),
            TestResource::Event(achievement::events::index::e_TrophyProgression::TEST_CLASS_HASH),
            TestResource::Contract(Actions::TEST_CLASS_HASH),
        ].span()
    };
}

Examples