Pac-Man On-Chain Game

Arcade Transitional
ecs

Transitional example: This example uses an older Cougr pattern and is preserved for compatibility reference. For the current recommended approach, see snake.

A practical example demonstrating how to use the cougr-core package to implement on-chain game logic on the Stellar blockchain via Soroban.

Overview

This example implements a complete Pac-Man game as a Soroban smart contract. The focus is exclusively on the smart contract logic (without graphical interface), showcasing how cougr-core simplifies on-chain game development.

Features

  • 10x10 maze with walls, pellets, and power pellets
  • Pac-Man movement with direction control
  • 4 ghosts with simplified AI (chase and frightened modes)
  • Score tracking and lives system
  • Win/lose conditions
  • Complete test coverage

Architecture

The example follows a modular architecture, separating domain types, ECS components, game systems, and contract entrypoints.

Module Layout

  • src/lib.rs: Main contract entrypoints and orchestration
  • src/types.rs: Game enums, state structures, and storage keys
  • src/components.rs: ECS-inspired data structures (Position, Ghost)
  • src/maze.rs: Maze layout and generation logic
  • src/systems.rs: Core game loop and logic
  • src/test.rs: Comprehensive test suite
  • README.md: This file
  • Cargo.toml: Soroban SDK dependencies
┌─────────────────────────────────────────────────────────────┐
│                    Pac-Man Contract (lib.rs)                │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  GameState  │  │   Maze      │  │    Ghosts[4]        │  │
│  │  (types.rs) │  │  (maze.rs)  │  │  (components.rs)    │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                    GameSystems (systems.rs)           │  │
│  │  - move_pacman                                        │  │
│  │  - move_ghosts                                        │  │
│  │  - check_collisions                                   │  │
│  └───────────────────────────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                    Contract Functions                       │
│  ┌──────────────┐ ┌────────────────┐ ┌──────────────────┐  │
│  │  init_game   │ │change_direction│ │   update_tick    │  │
│  └──────────────┘ └────────────────┘ └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Maze Layout

The game uses a fixed 10x10 maze:

##########
#P......P#
#.##.##..#
#.#...#..#
#...#....#
#.#.#.##.#
#.#......#
#.##.###.#
#P......P#
##########

Legend:
  # = Wall
  . = Pellet (10 points)
  P = Power Pellet (50 points)

Prerequisites

Required Tools

  1. Rust (1.88.0 or later)

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    rustup update
    
  2. Stellar CLI

    cargo install stellar-cli --locked
    stellar --version
    
  3. WASM Target

    rustup target add wasm32v1-none
    

Verify Installation

rustc --version    # Should be 1.88.0 or later
cargo --version
stellar --version  # Should show Stellar CLI version

Setup

1. Clone the Repository

git clone https://github.com/salazarsebas/Cougr.git
cd Cougr/examples/pac_man

2. Update Dependencies

cargo update

3. Build the Contract

# Debug build
cargo build

# Release build (optimized for WASM)
cargo build --release

4. Build for Soroban

stellar contract build

This generates the WASM file at:

target/wasm32v1-none/release/pac_man.wasm

Testing

Run All Tests

cargo test

Run Tests with Output

cargo test -- --nocapture

Run Specific Test

cargo test test_init_game
cargo test test_pellet_collection
cargo test test_ghost_ai

Expected Output

running 25 tests
test test::test_init_game ... ok
test test::test_maze_layout ... ok
test test::test_ghosts_initialized ... ok
test test::test_change_direction_up ... ok
test test::test_change_direction_down ... ok
...
test result: ok. 25 passed; 0 failed; 0 ignored

Code Walkthrough

1. Contract Types

The game uses Soroban-compatible types with #[contracttype]:

#![allow(unused)]
fn main() {
#[contracttype]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u32)]
pub enum Direction {
    Up = 0,
    Down = 1,
    Left = 2,
    Right = 3,
}

#[contracttype]
#[derive(Clone, Debug)]
pub struct GameState {
    pub pacman_pos: Position,
    pub pacman_dir: Direction,
    pub ghosts: Vec<Ghost>,
    pub maze: Vec<CellType>,
    pub score: u32,
    pub lives: u32,
    pub game_over: bool,
    // ...
}
}

2. Storage Pattern

Game state is stored using Soroban's instance storage:

#![allow(unused)]
fn main() {
#[contracttype]
pub enum DataKey {
    GameState,
    Initialized,
}

// Writing state
env.storage().instance().set(&DataKey::GameState, &state);

// Reading state
let state: GameState = env.storage()
    .instance()
    .get(&DataKey::GameState)
    .expect("Game not initialized");
}

3. Cougr-Core Integration

The contract uses cougr-core for ECS patterns and event handling:

#![allow(unused)]
fn main() {
use cougr_core::component::{ComponentTrait, Position as CorePosition};
use cougr_core::event::{CollisionEvent, Event, EventTrait};
}

Position Component: Game positions integrate with cougr_core's Position:

#![allow(unused)]
fn main() {
impl Position {
    /// Convert to cougr_core Position for ECS integration
    pub fn to_core_position(&self) -> CorePosition {
        CorePosition::new(self.x, self.y)
    }
}
}

Collision Events: Ghost collisions use cougr_core's event system:

#![allow(unused)]
fn main() {
// Each ghost has an entity_id for collision tracking
pub struct Ghost {
    pub entity_id: u64,
    // ...
}

// Create collision events using cougr_core
let collision_event = CollisionEvent::new(
    PACMAN_ENTITY_ID,
    ghost.entity_id,
    symbol_short!("ghost"),
);
}

Query Functions: Get serialized component data:

#![allow(unused)]
fn main() {
// Get Pac-Man's position as serialized cougr_core component
pub fn get_serialized_pacman_position(env: Env) -> Bytes {
    let core_pos = state.pacman_pos.to_core_position();
    core_pos.serialize(&env) // Uses ComponentTrait
}
}

This provides:

  • Component patterns: Position, Velocity components with serialization
  • Entity IDs: Unique identifiers for Pac-Man and ghosts
  • Event system: CollisionEvent for standardized collision handling
  • ComponentTrait: Serialization/deserialization for on-chain storage

4. Game Loop

The update_tick function implements the main game loop:

#![allow(unused)]
fn main() {
pub fn update_tick(env: Env) -> GameState {
    let mut state = Self::get_state(&env);

    // 1. Move Pac-Man
    Self::move_pacman(&env, &mut state);

    // 2. Check pellet collection
    Self::check_pellet_collection(&env, &mut state);

    // 3. Move ghosts
    Self::move_ghosts(&env, &mut state);

    // 4. Check collisions
    Self::check_ghost_collisions(&env, &mut state);

    // 5. Update timers
    if state.power_mode_timer > 0 {
        state.power_mode_timer -= 1;
    }

    // 6. Check win condition
    if state.pellets_remaining == 0 {
        state.game_over = true;
        state.won = true;
    }

    env.storage().instance().set(&DataKey::GameState, &state);
    state
}
}

Deployment

1. Generate a Stellar Identity

stellar keys generate --global pacman-deployer --network testnet

2. Fund the Account

stellar keys fund pacman-deployer --network testnet

Or use Friendbot:

curl "https://friendbot.stellar.org?addr=$(stellar keys address pacman-deployer)"

3. Deploy the Contract

stellar contract deploy \
  --wasm target/wasm32v1-none/release/pac_man.wasm \
  --source pacman-deployer \
  --network testnet

Save the returned contract ID (e.g., <CONTRACT_ID>).

4. Initialize the Game

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source pacman-deployer \
  --network testnet \
  -- \
  init_game

5. Play the Game

Change direction:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source pacman-deployer \
  --network testnet \
  -- \
  change_direction \
  --direction 2  # 0=Up, 1=Down, 2=Left, 3=Right

Update game state:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source pacman-deployer \
  --network testnet \
  -- \
  update_tick

6. Query Game State

Get score:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  -- \
  get_score

Get full state:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  -- \
  get_game_state

Check if game is over:

stellar contract invoke \
  --id <CONTRACT_ID> \
  --network testnet \
  -- \
  check_game_over

Troubleshooting

Common Errors

"Game not initialized"

Call init_game before any other function.

"Game already initialized"

Each contract instance can only be initialized once. Deploy a new instance to start a fresh game.

"Game is over"

The game has ended. Deploy a new contract to play again.

Rust Version Errors

rustup update
rustup default stable

WASM Build Fails

rustup target add wasm32v1-none

Dependency Resolution Issues

cargo clean
cargo update
cargo build

Stellar CLI Not Found

cargo install stellar-cli --locked

Debug Tips

  1. Verbose Build

    cargo build --verbose
    
  2. Check Contract Size

    ls -la target/wasm32v1-none/release/pac_man.wasm
    

    Should be under 64KB.

  3. Simulate Before Deploy

    stellar contract invoke \
      --id <CONTRACT_ID> \
      --source pacman-deployer \
      --network testnet \
      --sim \
      -- \
      init_game
    

Game Mechanics

Scoring

ActionPoints
Regular Pellet10
Power Pellet50
Eat Ghost200

Lives

  • Start with 3 lives
  • Lose 1 life when caught by a ghost in chase mode
  • Game over when lives reach 0

Power Mode

  • Lasts 10 ticks
  • All ghosts enter frightened mode
  • Eating a ghost awards 200 points and respawns the ghost

Ghost AI

  • Chase Mode: Ghosts move toward Pac-Man using Manhattan distance
  • Frightened Mode: Ghosts move away from Pac-Man

Win Condition

Collect all pellets (regular and power) to win.

Testnet Deployment

The contract has been successfully deployed to Stellar Testnet:

Contract ID: <CONTRACT_ID>

Explorer Link: https://stellar.expert/explorer/testnet/contract/<CONTRACT_ID>

Verified Invocations

  1. init_game - Successfully initialized with:

    • Score: 0
    • Lives: 3
    • Pellets: 47
    • 4 ghosts in chase mode
  2. change_direction - Successfully changed Pac-Man direction to Down

  3. update_tick - Successfully processed game tick:

    • Pac-Man moved from (1,1) to (1,2)
    • Collected pellet (+10 points)
    • Score updated to 10
    • Ghosts moved toward Pac-Man
  4. get_score - Read-only query returned: 10

References

License

MIT OR Apache-2.0