Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Quick Start: Number Guess Game

This tutorial walks through building a complete EGS-compliant game based on the Number Guess reference implementation.

What We're Building

A number-guessing game where:

  • The contract generates a secret number within a configurable range
  • The player guesses until they find it (or run out of attempts)
  • Score is calculated based on efficiency (fewer guesses = higher score)
  • Settings define difficulty (range size, max attempts)
  • Objectives track achievements (first win, quick thinker, perfect game)

Step 1: Compose Components

#[starknet::contract]
mod NumberGuess {
    use game_components_embeddable_game_standard::minigame::MinigameComponent;
    use game_components_embeddable_game_standard::minigame::objectives::ObjectivesComponent;
    use game_components_embeddable_game_standard::minigame::settings::SettingsComponent;
    use openzeppelin_introspection::src5::SRC5Component;
 
    component!(path: MinigameComponent, storage: minigame, event: MinigameEvent);
    component!(path: ObjectivesComponent, storage: objectives, event: ObjectivesEvent);
    component!(path: SettingsComponent, storage: settings, event: SettingsEvent);
    component!(path: SRC5Component, storage: src5, event: SRC5Event);
 
    #[abi(embed_v0)]
    impl SRC5Impl = SRC5Component::SRC5Impl<ContractState>;
 
    #[storage]
    struct Storage {
        #[substorage(v0)]
        minigame: MinigameComponent::Storage,
        #[substorage(v0)]
        objectives: ObjectivesComponent::Storage,
        #[substorage(v0)]
        settings: SettingsComponent::Storage,
        #[substorage(v0)]
        src5: SRC5Component::Storage,
        // ... your game-specific storage fields here
    }
    // ...
}

Step 2: Implement IMinigameTokenData

This is the core requirement. The token contract reads your game's state through this interface:

use game_components_embeddable_game_standard::minigame::interface::{IMINIGAME_ID, IMinigameTokenData};
 
#[abi(embed_v0)]
impl MinigameTokenDataImpl of IMinigameTokenData<ContractState> {
    fn score(self: @ContractState, token_id: felt252) -> u64 {
        self.scores.read(token_id)
    }
 
    fn game_over(self: @ContractState, token_id: felt252) -> bool {
        self.game_overs.read(token_id)
    }
 
    fn score_batch(self: @ContractState, token_ids: Span<felt252>) -> Array<u64> {
        let mut results = array![];
        for token_id in token_ids {
            results.append(self.scores.read(*token_id));
        };
        results
    }
 
    fn game_over_batch(self: @ContractState, token_ids: Span<felt252>) -> Array<bool> {
        let mut results = array![];
        for token_id in token_ids {
            results.append(self.game_overs.read(*token_id));
        };
        results
    }
}

Step 3: Wrap Actions with EGS Hooks

Every player-facing action must:

  1. assert_token_ownership — verify the caller owns the token. A player should never be able to act on a token they don't own. This should be the first check in every action.
  2. pre_action — validate the token is playable (exists, not expired, game not over)
  3. post_action — sync score and game_over state back to the token contract

Single-contract games (component methods)

If your game logic lives in the same contract that was initialized with the MinigameComponent, you can use the component's internal methods directly:

fn play(ref self: ContractState, token_id: felt252) {
    // 1. Verify caller owns the token
    self.minigame.assert_token_ownership(token_id);
    // 2. Validate token is playable
    self.minigame.pre_action(token_id);
 
    // ... your game logic here ...
    // Update self.scores, self.game_overs, etc.
 
    // 3. Sync state back to the token contract
    self.minigame.post_action(token_id);
}

Multi-contract games (library functions)

If your game spans multiple contracts (e.g. separate combat, exploration, and market systems), the contracts that don't have the MinigameComponent won't have access to self.minigame. Instead, import and call the library functions directly, passing the Denshokan Token address (your minigame_token_address):

use game_components_embeddable_game_standard::minigame::minigame::{
    assert_token_ownership, pre_action, post_action,
};

Each system contract stores the token address (set during construction) and passes it explicitly:

fn start_game(ref self: ContractState, adventurer_id: felt252, weapon: u8) {
    let minigame_token_address = self.minigame_token_address.read();
 
    assert_token_ownership(minigame_token_address, adventurer_id);
    pre_action(minigame_token_address, adventurer_id);
 
    // ... game logic ...
 
    post_action(minigame_token_address, adventurer_id);
}

This pattern lets any number of contracts validate and update token state without needing the component — they just need the token contract address.

Step 4: Add Settings (Optional)

Settings and objectives let platforms and players configure difficulty and track achievements. You define the schema that makes sense for your game — the SettingsComponent and ObjectivesComponent handle the registry-facing metadata while your contract stores the actual gameplay parameters.

See Settings & Objectives for the full implementation guide, or the Number Guess source code for a working example.

Step 5: Register with Registry

Your game's initializer should register it with the game registry. The minigame_token_address parameter is the Denshokan Token contract — see Deployed Contracts for the current Sepolia address.

use game_components_embeddable_game_standard::minigame::MinigameComponent;
 
#[abi(embed_v0)]
impl InitImpl of INumberGuessInit<ContractState> {
    fn initializer(
        ref self: ContractState,
        game_creator: ContractAddress,
        game_name: ByteArray,
        game_description: ByteArray,
        game_developer: ByteArray,
        game_publisher: ByteArray,
        game_genre: ByteArray,
        game_image: ByteArray,
        game_color: Option<ByteArray>,
        client_url: Option<ByteArray>,
        renderer_address: Option<ContractAddress>,
        settings_address: Option<ContractAddress>,
        objectives_address: Option<ContractAddress>,
        minigame_token_address: ContractAddress, // Denshokan Token address
        royalty_fraction: Option<u128>,
        skills_address: Option<ContractAddress>,
        version: u64,
    ) {
        // Register with the game registry via the MinigameComponent
        self.minigame.initializer(
            game_creator, game_name, game_description,
            game_developer, game_publisher, game_genre,
            game_image, game_color, client_url,
            renderer_address, settings_address, objectives_address,
            minigame_token_address, royalty_fraction,
            skills_address, version,
        );
 
        // ... set up your game's default settings, objectives, etc.
    }
}

Step 6: Test

#[test]
fn test_number_guess_basic() {
    // Deploy the game and token contracts
    let (game, token) = deploy_number_guess_with_token();
 
    // Mint a token with Easy settings (settings_id=1)
    let token_id = mint_token(token, settings_id: 1, to: PLAYER());
 
    // Start a new game — settings are extracted from the packed token ID
    game.new_game(token_id);
 
    // Binary search for the secret number
    let (mut low, mut high) = game.get_range(token_id);
    loop {
        let mid = (low + high) / 2;
        let result = game.guess(token_id, mid);
        if result == 0 { break; }         // Correct!
        if result == -1 { low = mid + 1; } // Too low
        else { high = mid - 1; }           // Too high
    };
 
    // Verify the game recorded a score
    let score = game.score(token_id);
    assert!(score > 0, "Should have a score after winning");
 
    // Verify game state
    assert!(game.games_won(token_id) == 1, "Should have 1 win");
}

Summary

To make your game EGS-compliant:

  1. Required: Implement IMinigameTokenData (score + game_over)
  2. Required: Register IMINIGAME_ID via SRC5
  3. Required: Register with the game Registry
  4. Required: Call assert_token_ownership, pre_action, and post_action in every player-facing action
  5. Optional: Implement IMinigameTokenSettings for custom difficulty
  6. Optional: Implement IMinigameTokenObjectives for achievements
  7. Optional: Implement IMinigameDetails for rich game state display

For a complete reference implementation, see the Number Guess source code.