Guessing Game Contract
Creating a module
First, we need to create a module. A module, in Move is a collection of related code (structs, functions, constants) that is published at a specific address. Modules provide encapsulation and organization for your smart contract logic.
module module_addr::guessing_game {
}
Contract Explanation and Breakdown
Creating a Module
A module in Move is a collection of related code (structs, functions, constants) that is published at a specific address. Modules provide encapsulation and organization for your smart contract logic.
module module_addr::guessing_game {
// Module contents go here
}
Here, module_addr
is a named address (set at compile/deploy time), and guessing_game
is the module's name.
Error Codes
The contract defines error constants to provide clear, descriptive error messages for different failure conditions:
const E_NO_GAME: u64 = 1; // No game initialized for this account
const E_GAME_OVER: u64 = 2; // Game is over, must reset
const E_GAME_NOT_OVER: u64 = 3; // Game is not over, must finish guessing
const E_GAME_INITIALIZED: u64 = 4;// Game already initialized
const E_ALREADY_GUESSED: u64 = 5; // Number already guessed
Game State
The Game
struct stores all state for a single game:
struct Game has key {
number: u8, // The number to guess
guesses: vector<u8>, // All previous guesses
game_over: bool, // Whether the game is over
}
- The
key
ability allows this struct to be stored in global storage under an account address.
Game Functions
Creating a Game
entry fun create_game(caller: &signer, number: u8) {
let caller_addr = signer::address_of(caller);
assert!(!exists<Game>(caller_addr), E_GAME_INITIALIZED);
move_to(caller, Game {
number,
guesses: vector[],
game_over: false,
})
}
- Only one game per account is allowed at a time.
- Stores the new game in the caller's account.
Making a Guess
public entry fun guess(caller: &signer, number: u8) acquires Game {
let caller_addr = signer::address_of(caller);
assert!(exists<Game>(caller_addr), E_NO_GAME);
let game = &mut Game[caller_addr];
assert!(!game.game_over, E_GAME_OVER);
assert!(!game.guesses.contains(&number), E_ALREADY_GUESSED);
game.guesses.push_back(number);
if (number == game.number) {
game.game_over = true;
}
}
- Checks that the game exists and is not over.
- Prevents duplicate guesses.
- Marks the game as over if the guess is correct.
Resetting the Game
entry fun reset_game(caller: &signer, new_num: u8) acquires Game {
let caller_addr = signer::address_of(caller);
assert!(exists<Game>(caller_addr), E_NO_GAME);
let game = &mut Game[caller_addr];
assert!(game.game_over, E_GAME_NOT_OVER);
game.game_over = false;
game.guesses = vector[];
game.number = new_num;
}
- Only allowed if the game is over.
- Resets guesses and sets a new number.
Removing the Game State
entry fun remove_state(caller: &signer) acquires Game {
let caller_addr = signer::address_of(caller);
assert!(exists<Game>(caller_addr), E_NO_GAME);
let Game { .. } = move_from<Game>(caller_addr);
}
- Deletes the game from storage for the caller.
View Functions
These functions allow anyone to query the game state without modifying it:
#[view]
public fun is_game_over(addr: address): bool acquires Game {
assert!(exists<Game>(addr), E_NO_GAME);
Game[addr].game_over
}
#[view]
public fun number(addr: address): u8 acquires Game {
assert!(exists<Game>(addr), E_NO_GAME);
assert!(is_game_over(addr), E_GAME_NOT_OVER);
Game[addr].number
}
#[view]
public fun guesses(addr: address): vector<u8> acquires Game {
assert!(exists<Game>(addr), E_NO_GAME);
Game[addr].guesses
}
is_game_over
: Returns whether the game is over.number
: Returns the answer, but only if the game is over (prevents cheating).guesses
: Returns all guesses made so far.
Key Concepts Demonstrated
- State Management: Uses a struct with the
key
ability for on-chain state. - Access Control: Only the account owner can modify their game.
- Error Handling: Uses clear error codes and assertions.
- Resource Safety: Ensures proper creation, update, and deletion of game state.
- View Functions: Provides read-only access to game state.
- Game Logic: Implements a simple, fair guessing game with validation.
This contract is a practical example of how to build a stateful, interactive Move smart contract on Aptos, and demonstrates best practices for error handling, state management, and user interaction.
Here is the full contract at the end:
/// Guessing game
///
/// - Call `create_game` to start the game
/// - Guess with `guess`
/// - When the number is guessed correctly, the game is over
/// - Can be reset with `reset_game`
/// - Can be cleaned up with `remove_state`
///
module module_addr::guessing_game {
use std::signer;
/// No game initialized for this account, please call create_game first
const E_NO_GAME: u64 = 1;
/// Game is over, please call reset_game
const E_GAME_OVER: u64 = 2;
/// Game is not over, please guess until the game is over
const E_GAME_NOT_OVER: u64 = 3;
/// Game is already initialized, please guess until the game is over
const E_GAME_INITIALIZED: u64 = 4;
/// Number is already guessed, please guess a different number
const E_ALREADY_GUESSED: u64 = 5;
/// State of the game
struct Game has key {
number: u8,
guesses: vector<u8>,
game_over: bool,
}
/// Creates a game
entry fun create_game(caller: &signer, number: u8) {
let caller_addr = signer::address_of(caller);
assert!(!exists<Game>(caller_addr), E_GAME_INITIALIZED);
move_to(caller, Game {
number,
guesses: vector[],
game_over: false,
})
}
/// Guesses on the game
public entry fun guess(caller: &signer, number: u8) acquires Game {
let caller_addr = signer::address_of(caller);
// Check that the game exists
assert!(exists<Game>(caller_addr), E_NO_GAME);
let game = &mut Game[caller_addr];
// Check that the game isn't over
assert!(!game.game_over, E_GAME_OVER);
// Check that I haven't guessed already
assert!(!game.guesses.contains(&number), E_ALREADY_GUESSED);
game.guesses.push_back(number);
// Check win condition
if (number == game.number) {
game.game_over = true;
}
}
/// Resets the game when it's done
entry fun reset_game(caller: &signer, new_num: u8) acquires Game {
let caller_addr = signer::address_of(caller);
// Check that the game exists
assert!(exists<Game>(caller_addr), E_NO_GAME);
let game = &mut Game[caller_addr];
// Check that the game is over
assert!(game.game_over, E_GAME_NOT_OVER);
game.game_over = false;
game.guesses = vector[];
game.number = new_num;
}
/// Deletes the game state
entry fun remove_state(caller: &signer) acquires Game {
let caller_addr = signer::address_of(caller);
assert!(exists<Game>(caller_addr), E_NO_GAME);
let Game {
..
} = move_from<Game>(caller_addr);
}
#[view]
public fun is_game_over(addr: address): bool acquires Game {
assert!(exists<Game>(addr), E_NO_GAME);
Game[addr].game_over
}
#[view]
public fun number(addr: address): u8 acquires Game {
assert!(exists<Game>(addr), E_NO_GAME);
assert!(is_game_over(addr), E_GAME_NOT_OVER);
Game[addr].number
}
#[view]
public fun guesses(addr: address): vector<u8> acquires Game {
assert!(exists<Game>(addr), E_NO_GAME);
Game[addr].guesses
}
}