Welcome to Cougr

Cougr is an on-chain game engine built for the Stellar network and Soroban smart contracts. It provides a full Entity-Component-System (ECS) runtime alongside account abstraction and zero-knowledge primitives in a single crate.

This documentation site is generated automatically from the salazarsebas/Cougr repository.

Start

Welcome to the Start section. If you're new to Cougr, this is the right place to begin.

  • Getting Started - install the toolchain and run your first test in under two minutes.
  • Build Your First Game - a step-by-step walkthrough from an empty directory to a deployed game on Stellar Testnet.

Getting Started

This page is synced from the main Cougr repository. To edit it, open a PR against salazarsebas/Cougr.


Prerequisites

Before you can build a Cougr game, you need the following tools installed:

ToolInstall command
Rust`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs
Stellar CLIcargo install stellar-cli --features opt
wasm32 targetrustup target add wasm32-unknown-unknown

Clone and run the starter example

git clone https://github.com/salazarsebas/Cougr
cd Cougr/examples/spawn_and_move
cargo test

All tests should pass within two minutes on a machine that has Rust installed.

Next step

Once the tests pass, head over to Build Your First Game for the full sequential tutorial.

Build Your First Game

Welcome to Cougr! This tutorial will take you from an empty directory to a fully tested, testnet-deployed game.

This guide is designed for developers who already know Rust but have no prior experience with Cougr, Soroban, or Stellar. By the end, you'll understand how Cougr's Entity-Component-System (ECS) architecture translates into safe, efficient smart contracts.

We are going to build a simple 2D grid game where a player can spawn into the world and walk in four directions.


1. Project Setup

Since we're building a smart contract, we start with a standard Rust library rather than a binary.

cargo new --lib my_first_game
cd my_first_game

Add cougr-core and the soroban-sdk to your Cargo.toml. We enable the testutils feature to get access to Cougr's built-in testing harness later.

[package]
name = "my_first_game"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
soroban-sdk = "25.3.2"
cougr-core = "1.1.0" # or the latest version from crates.io

[features]
testutils = ["soroban-sdk/testutils", "cougr-core/testutils"]

2. The Mental Model: ECS on Soroban

If you've used an ECS in a game engine like Bevy or Unity, you already know the basics: Entities are just IDs, Components hold data, and Systems run logic.

However, building for a blockchain introduces new constraints you must consider:

[!WARNING] Soroban-Specific Constraints

  1. Storage is not a normal database: You cannot freely iterate over millions of rows. State must be loaded into memory, modified, and saved back efficiently.
  2. Execution costs money: Every instruction, memory allocation, and storage write costs "gas". Infinite loops or massive arrays will cause your transaction to exceed resource limits and fail.
  3. Instance Storage vs Persistent Storage: Cougr uses Soroban's "Instance Storage" by default for your hot-loop game state. This means all active gameplay components are loaded in a single read, making operations extremely cheap and fast, but it requires you to be mindful of total state size.

Cougr abstracts the heavy lifting of storage management, but you still need to write code with these constraints in mind.


3. Defining Components

Let's open src/lib.rs. First, we clear out the default code and define our game's data.

We need two components: a Position to track where the player is, and Moves to track how many steps they have left.

#![no_std]

use cougr_core::game::SorobanGame;
use cougr_core::{impl_component, impl_component_observed, impl_soroban_game};
use soroban_sdk::{contract, contractimpl, contracttype, Env};

// `Position` emits an indexed event on every change (so a UI can watch it)
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct Position {
    pub x: i32,
    pub y: i32,
}
impl_component_observed!(Position, "position", Table, { x: i32, y: i32 });

// `Moves` is kept private; it doesn't need to emit an event on every step
#[contracttype]
#[derive(Clone, Debug, PartialEq)]
pub struct Moves {
    pub remaining: u32,
}
impl_component!(Moves, "moves", Table, { remaining: u32 });

Notice the two different macros:

  • impl_component_observed! tells Cougr to emit a Soroban event every time this component is modified. This is crucial for off-chain clients (like your web frontend) to track movement in real-time without polling the blockchain.
  • impl_component! is for standard data that doesn't need to be broadcasted to indexers, saving you gas on event emissions.

4. Writing the Game Contract (Systems)

Next, we define our contract. In Cougr, the contract acts as the outer shell that loads the ECS world, runs your system logic (the functions), and saves the world back.

Add this to the bottom of src/lib.rs:

#[contract]
#[derive(Clone)]
pub struct MyFirstGame;

// This macro wires up the `load_world` and `save_world` boilerplate.
impl_soroban_game!(MyFirstGame, "world");

#[contractimpl]
impl MyFirstGame {
    
    /// Spawns a new player entity into the world.
    pub fn spawn(env: Env) -> u32 {
        // 1. Load the ECS world from Soroban storage
        let mut world = MyFirstGame::load_world(&env);

        // 2. Spawn an entity and attach our components
        let entity = world.spawn_entity();
        world.set_typed_observed(&env, entity, &Position { x: 0, y: 0 });
        world.set_typed(&env, entity, &Moves { remaining: 10 });

        // 3. Save the ECS world back to Soroban storage
        MyFirstGame::save_world(&env, &world);
        
        entity
    }

    /// Moves a player entity in a given direction (0=North, 1=East, 2=South, 3=West).
    pub fn move_entity(env: Env, entity_id: u32, direction: u32) {
        let mut world = MyFirstGame::load_world(&env);

        let mut pos = world.get_typed::<Position>(&env, entity_id).unwrap();
        let mut moves = world.get_typed::<Moves>(&env, entity_id).unwrap();

        if moves.remaining == 0 {
            panic!("no moves left");
        }

        match direction {
            0 => pos.y += 1, // North
            1 => pos.x += 1, // East
            2 => pos.y -= 1, // South
            3 => pos.x -= 1, // West
            _ => panic!("invalid direction"),
        }
        
        moves.remaining -= 1;

        // Apply changes
        world.set_typed_observed(&env, entity_id, &pos);
        world.set_typed(&env, entity_id, &moves);

        MyFirstGame::save_world(&env, &world);
    }
    
    /// Query a player's current position.
    pub fn get_position(env: Env, entity_id: u32) -> Option<Position> {
        let world = MyFirstGame::load_world(&env);
        world.get_typed::<Position>(&env, entity_id)
    }
}

This pattern - Load World -> Query/Modify -> Save World - is the backbone of every Cougr contract entry point.


5. Local Testing

Testing smart contracts on an actual network is slow. Cougr provides a powerful local GameHarness to run tests instantly in memory.

Create a new file src/test.rs and add it to your module tree by adding #[cfg(test)] mod test; to the very bottom of src/lib.rs.

In src/test.rs:

#![cfg(test)]

use super::*;
use cougr_core::test::{GameHarness, Scenario};
use soroban_sdk::Env;

#[test]
fn test_spawn_and_move() {
    let env = Env::default();
    
    // Register our contract using Cougr's test harness
    let harness = GameHarness::new(env, MyFirstGame);

    // The macro generated a "MyFirstGameClient" for us automatically
    let client = MyFirstGameClient::new(harness.env(), harness.contract_id());
    
    // 1. Spawn the entity
    let entity_id = client.spawn();
    
    let pos = client.get_position(&entity_id).unwrap();
    assert_eq!(pos.x, 0);
    assert_eq!(pos.y, 0);

    // 2. Use Cougr's Scenario builder to simulate turns/moves
    Scenario::new("move north")
        .turns(1)
        .run(&harness, |_player, _turn, h| {
            let c = MyFirstGameClient::new(h.env(), h.contract_id());
            
            // Move North (direction = 0)
            c.move_entity(&entity_id, &0);
            
            let pos = c.get_position(&entity_id).unwrap();
            assert_eq!(pos.x, 0);
            assert_eq!(pos.y, 1);
        });
}

Run your tests to verify your game works locally:

cargo test

If it passes, you are ready to deploy!


6. Deploying to Testnet

To deploy, we need to compile our game to a WebAssembly (WASM) binary and use the Stellar CLI.

[!TIP] If you haven't installed the Stellar CLI yet, check out the Stellar Quickstart.

1. Build the WASM file:

cargo build --target wasm32-unknown-unknown --release

Your compiled game is now located at target/wasm32-unknown-unknown/release/my_first_game.wasm.

2. Configure your testnet identity:

stellar keys generate alice --network testnet

3. Deploy the contract:

stellar contract deploy \
  --wasm target/wasm32-unknown-unknown/release/my_first_game.wasm \
  --source alice \
  --network testnet

If successful, the CLI will return a C... contract address. Congratulations! Your game is live on the Stellar Testnet.


7. Next Steps

You've built and deployed a basic ECS contract. However, real games require more advanced mechanics like access control, hidden information (Fog of War), or multi-contract plugin architectures.

  • Learn the architecture: Read Cougr Patterns to understand how to structure larger, production-ready games.
  • Get inspired: Browse the Showcase (like murdoku or battleship) in the examples/ directory to see full-stack implementations.

Learn

The Learn section takes you from a working first game to understanding the design decisions that make Cougr work the way it does.

GuideStatus
Architectureโœ… Available
Game Patternsโœ… Available
On-Chain / Off-Chain Boundary Guide๐Ÿ”œ Coming soon
Smart Contract Patterns๐Ÿ”œ Coming soon
Testing Guide๐Ÿ”œ Coming soon
Deployment Guide๐Ÿ”œ Coming soon

Architecture

High-level overview of how Cougr is organized. For usage, see README.md.

Layers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           game::SorobanGame  (contract integration)          โ”‚  Contract layer
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚               app::GameApp                                   โ”‚  Default runtime surface
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  ECS      โ”‚  Accounts     โ”‚  Standards      โ”‚  ZK Proofs     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  soroban-sdk 25.1.0  (no_std, WASM)                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

game::SorobanGame (src/game.rs) bridges the ECS and Soroban contract models. The SorobanGame trait provides load_world and save_world as default methods, eliminating repetitive storage-key boilerplate from contract entrypoints. Wire up once with impl_soroban_game!(MyContract, "key").

The companion helpers SimpleWorld::load_from_instance and save_to_instance are the underlying primitives when you want finer control.

GameApp (src/plugin/mod.rs) is the default onboarding layer for complex games. It owns a SimpleWorld, the scheduler, plugin registration, and runtime resources in one place.

ECS

Two storage backends, same ComponentTrait interface:

BackendFileStrategyBest for
SimpleWorldsrc/simple_world/Map<(EntityId, Symbol), Bytes> with dual Table/Sparse indexesGeneral use, small entity counts
ArchetypeWorldsrc/archetype_world/Groups entities by component signatureLarge entity counts, batch queries

Both support typed access (get_typed<T>, set_typed<T>) and raw access (get_component, add_component).

Supporting systems:

  • Query cache (src/query/) - version-tagged, invalidates on world mutation
  • Hooks (src/hooks.rs) - callbacks on component add/remove
  • Observers (src/observers.rs) - event-driven reactions
  • Commands (src/commands.rs) - deferred mutations during system execution
  • Scheduler (src/scheduler/) - stage-based, dependency-aware system ordering
  • Change tracker (src/change_tracker.rs) - per-component dirty flags
  • Plugins (src/plugin/) - modular game logic bundles
  • Incremental storage (src/incremental/) - only persist dirty entities

Component definition

Three macros cover every component case:

MacroWhen to use
impl_component!Fixed-size primitives (i32, u32, u64, u128, u8, bool, bytes32)
impl_component_observed!Same as above, plus structured Soroban events on every set
impl_rich_component!Complex types via XDR codec: Address, Vec, String, Option, nested structs

impl_rich_component! requires #[contracttype] on the struct. The XDR serialisation is handled entirely by the Soroban SDK - no manual serialize/deserialize implementation is needed.

Rich components are stored in Soroban instance storage (not the ECS Map) but share the same entity ID space.

ZK Proofs (src/zk/)

All ZK operations use Stellar Protocol 25 (X-Ray) host functions - the heavy crypto runs on the host, not in WASM.

  • Groth16 (groth16.rs) - proof verification via BN254 pairing
  • BLS12-381 (bls12_381.rs) - G1 add/mul/MSM, pairing checks
  • Poseidon2 (crypto.rs) - ZK-friendly hashing, behind hazmat-crypto feature
  • Merkle trees (merkle/) - SHA256 and Poseidon variants, sparse trees, on-chain proofs
  • Pedersen (commitment.rs) - commitment scheme for hidden state
  • Game circuits (circuits.rs, traits.rs) - GameCircuit trait + pre-built circuits (Movement, Combat, Inventory, TurnSequence) + CustomCircuitBuilder
  • ECS integration (components.rs, systems.rs) - CommitReveal, HiddenState, ProofSubmission components with verification systems

Accounts (src/accounts/)

Account abstraction layer with pluggable implementations:

CougrAccount (trait)
โ”œโ”€โ”€ ClassicAccount - standard Stellar keypair
โ””โ”€โ”€ ContractAccount - smart contract wallet
     โ”œโ”€โ”€ SessionStorage - persistent session keys
     โ”œโ”€โ”€ RecoveryStorage - guardian-based recovery
     โ”œโ”€โ”€ DeviceStorage - multi-device key management
     โ””โ”€โ”€ Secp256r1Storage - WebAuthn/Passkey keys

Key traits: CougrAccount, SessionKeyProvider, RecoveryProvider, MultiDeviceProvider.

SessionBuilder provides a fluent API for constructing scoped session keys. authorize_with_fallback handles graceful degradation from session keys to direct authorization. See ADR 0005.

Standards (src/standards/)

Reusable contract standards for integrations that need explicit operational controls:

  • Ownable and Ownable2Step for owner-managed authority
  • AccessControl for role-based authorization with delegated admins
  • Pausable for emergency stops
  • ExecutionGuard for serialized critical sections
  • RecoveryGuard for blocking sensitive paths during recovery windows
  • BatchExecutor for bounded multi-operation flows
  • DelayedExecutionPolicy for time-delayed operation queues

Each standard instance is keyed by a caller-supplied Symbol, which keeps storage deterministic and avoids collisions when a contract composes multiple modules.

Competitive Layers (workspace subcrates)

Three layers ship inside the single cougr-core crate. Implementation lives in src/{circuits,session,test}/; internal/cougr-core-* workspace members use stubs for isolated cargo check -p runs.

Public moduleSourceMaturityFeature
cougr_core::circuitssrc/circuits/Experimentalalways
cougr_core::sessionsrc/session/Betaalways
cougr_core::testsrc/test/Betatestutils

Circuit builders: hidden_cards, fog_of_war, fair_dice, sealed_bid โ†’ GameCircuitSpec. Examples: hidden_hand, fog_explorer, dice_duel, blind_auction. See ADR 0006.

The test sandbox uses no_std + alloc with Soroban testutils - not std. Enable with cougr-core feature testutils. Modules: GameHarness, Scenario, WorldFixture, ReplayLog, SnapshotAssert. See ADR 0004 and ADR 0007.

Feature Flags

FlagEnables
hazmat-cryptoPoseidon2 hash, BN254 curve ops (via soroban-sdk/hazmat-crypto)
testutilscougr_core::test sandbox, MockAccount, Soroban test helpers
debugRuntime introspection, metrics, state snapshots (src/debug/)

Build

Release builds are configured with LTO, opt-level = "z", and overflow-checks = true to keep artifacts optimized for constrained execution environments.

Primary target: wasm32v1-none.

Cougr Patterns

Purpose

This document captures the recommended architectural patterns for new Soroban game contracts built on Cougr.

The goal is to standardize how teams structure worlds, systems, stages, and storage choices instead of relying on ad-hoc example interpretation.

Find a pattern by problem

Start here if you know what you're trying to build but not which Cougr module answers it. Each row links to the module-level doc for full detail, and to a concrete, current example that demonstrates it.

I want...UseExampleRead more
Fairness (a roll, a draw, an outcome no one can predict or bias)circuits::FairDiceBuilder - on-chain Groth16-verified randomness (Experimental)dice_duelHidden Information Guidance below, PRIVACY_MODEL.md
Hidden information (cards, ship positions, sealed bids - state some players shouldn't see)privacy::stable commit-reveal + Merkle primitivesbattleship (canonical), rock_paper_scissors, hidden_hand, blind_auctionHidden Information Guidance below, PRIVACY_MODEL.md
To gate an action behind a role (admin-only, minter-only, etc.)AccessControl - role-based authorization with per-role admin delegation-STANDARDS_LAYER.md ยง AccessControl
Off-chain-friendly real-time movement (clients track state without polling)impl_component_observed! - emits a (COUGR, set, <component>) event on every changespawn_and_move (start here), snakeSystem Design and Query Guidance below, docs/ECS_CORE.md
A passwordless sign-in (Face ID / Touch ID instead of a seed phrase)secp256r1 passkey signer, composed through AccountKernelguild_arenaACCOUNT_KERNEL.md ยง Signers
A session players approve once, not per-transactionSession signer + SessionPolicy (scope, expiry, operation budget)session_arenaACCOUNT_KERNEL.md ยง Session Model
Account recovery if a device is lostGuardianPolicy + ActiveDevicePolicyguild_arenaACCOUNT_KERNEL.md ยง Policies
An emergency stop / pause switchPausable-STANDARDS_LAYER.md ยง Pausable
To serialize mutations / guard against reentrancy-like issuesExecutionGuard-STANDARDS_LAYER.md ยง ExecutionGuard
Delayed or timelocked executionDelayedExecutionPolicy-STANDARDS_LAYER.md ยง DelayedExecutionPolicy
To batch several operations safelyBatchExecutor-STANDARDS_LAYER.md ยง BatchExecutor
To decide what belongs on-chain at all (which state and rules justify their cost, and which should stay client-side)The five-question boundary framework, applied per piece of statebattleship, snake, blind_auctionONCHAIN_OFFCHAIN_BOUNDARY.md
To know whether I even need ECSDirect contract model for small/config-driven contracts-When Not To Use ECS below
To pick table vs. sparse storageTable for hot-loop state, sparse for infrequent markers-Storage Guidance below
A thin, explicit contract entrypoint / gameplay loopGameApp + explicit stage placementspawn_and_move, snakeDefault Entry Point and Stage Layout below

Everything below this point is the module-level architectural guidance the table above links into - organized by Cougr's internal structure rather than by problem, for readers who already know which area they're working in.

Default Entry Point

Use GameApp as the default runtime entrypoint.

Recommended shape:

  1. build the app
  2. register plugins and startup systems
  3. register tick systems into explicit stages, preferably with named_system(...) / named_context_system(...)
  4. run one schedule tick per contract invocation that advances gameplay

This keeps the "contract entrypoint" thin and the gameplay loop explicit.

Stage Layout

Cougr's recommended schedule is:

  • Startup: one-time entity/resource setup
  • PreUpdate: input decoding, action validation, turn preparation
  • Update: core gameplay state transitions
  • PostUpdate: scoring, derived-state maintenance, indexing side effects
  • Cleanup: despawns, expiry handling, transient marker removal

Do not use cross-stage before / after dependencies. Stage order is already the primary contract between phases.

System Design

Prefer small systems with one responsibility:

  • validation systems should reject or mark invalid intent
  • update systems should apply game-state transitions
  • cleanup systems should remove expired markers or entities

Use context-aware systems when you need deferred structural changes:

  • queue spawns during iteration
  • queue despawns after collision passes
  • queue marker additions that should apply after the current scan

Use plain world/env systems when the system only needs direct mutation and no command buffering.

Query Guidance

Prefer SimpleQueryBuilder for gameplay queries that need:

  • multiple required components
  • negative filters
  • sparse-component inclusion
  • "any-of" matching

Guidelines:

  • default to table-only queries for tight loops
  • opt into sparse inclusion only when marker/tag data must participate
  • choose required components carefully so the scheduler can use the narrowest candidate set

Hidden Information Guidance

For hidden-state or commit-reveal contracts:

  • keep the contract entrypoints thin and verification-oriented
  • use privacy::stable Merkle and commit-reveal primitives instead of example-local crypto formats
  • treat proof verification as a boundary concern, not as something every gameplay system needs to understand
  • keep public derived state separate from private commitments and Merkle roots

battleship is the canonical reference for this pattern.

Storage Guidance

Use table storage for:

  • frequently scanned gameplay state
  • canonical state that participates in core loops
  • components used by Update systems on most ticks

Use sparse storage for:

  • infrequent markers
  • administrative tags
  • components mostly accessed by targeted lookups instead of broad scans

If a component becomes part of the hot loop, move it to table storage instead of compensating with more complex query logic.

Keep modules separated by concern:

  • ECS/gameplay core
  • account/auth flows
  • privacy/ZK
  • standards/operational controls

Do not let auth or ZK concerns leak into every system by default. Compose them at the boundaries where they are needed.

When Not To Use ECS

Do not force ECS into contracts that are:

  • tiny and single-entity
  • mostly configuration/state-machine driven
  • dominated by one-off administrative flows

If the problem is closer to a fixed state machine than a world simulation, a direct contract model may be simpler and cheaper.

On-Chain / Off-Chain Boundary Guide

โณ This page is being written.

Named as the second-highest-priority missing document in docs/strategy/08-ux-strategy.md and docs/strategy/12-documentation-architecture.md.

Tracked in: salazarsebas/Cougr issues


What this guide will cover

Developers coming from traditional game development or web backends often hit the same wall: what logic actually needs to be on-chain, and what doesn't? Getting this wrong is expensive - literally, in gas fees.

This guide will answer:

  • Which game state must live in Soroban contract storage vs. what can stay off-chain
  • How Cougr's observed components (impl_component_observed!) bridge the gap with real-time events
  • Patterns for off-chain movement with on-chain settlement
  • Resource cost intuition: what makes a transaction cheap vs. expensive

Check back soon - or watch the repository to be notified when this page goes live.

Smart Contract Patterns

โณ This page is being written.

Will consolidate Soroban-specific patterns from PATTERNS.md and STANDARDS_LAYER.md into one place, distinguishing "Cougr patterns" from "general Soroban patterns."

Tracked in: salazarsebas/Cougr issues


What this guide will cover

  • Access control with AccessControl and Ownable
  • Emergency stop patterns with Pausable
  • Time-delayed operations with DelayedExecutionPolicy
  • Batch operations with BatchExecutor
  • How these standards compose with the ECS world

Check back soon - or watch the repository.

Testing Guide

โณ This page is being written.

GameHarness, Scenario, and SnapshotAssert exist in the codebase and are used across thousands of lines of tests, but have no standalone guide yet. Named explicitly in docs/strategy/08-ux-strategy.md Stage 5.

Tracked in: salazarsebas/Cougr issues


What this guide will cover

  • Setting up GameHarness for unit tests
  • Writing Scenario-based integration tests
  • Using SnapshotAssert to lock in expected world state
  • How the Soroban test sandbox works (and how it differs from running cargo test for a normal library)
  • Estimating resource costs before you deploy

Check back soon - or watch the repository.

Deployment Guide

โณ This page is being written.

The README has dev commands, but there's no standalone guide walking through production deployment to Stellar Testnet and Mainnet.

Tracked in: salazarsebas/Cougr issues


What this guide will cover

  • Configuring the Stellar CLI for Testnet and Mainnet
  • Building a release .wasm binary with the correct flags (LTO, opt-level = "z")
  • Deploying with stellar contract deploy
  • Understanding what a deployment costs (resource fees)
  • Upgrading a contract after deploy

Check back soon - or watch the repository.

Reference

The Reference section contains low-level documentation for Cougr's modules, the API surface, and architectural decision records.

DocumentDescription
ECS CoreThe core ECS primitives: Entity, Component, Query, System, Scheduler
Account KernelAccount abstraction layer - session keys, recovery, passkeys
Standards LayerReusable contract standards: AccessControl, Pausable, Ownable, etc.
Privacy ModelZK proofs, Pedersen commitments, hidden state
Feature Flagshazmat-crypto, testutils, debug - what each enables
Performance GuideResource cost intuition, benchmarks, optimization patterns
API ContractPublic API guarantees and stability promises
Compatibility PromisesWhat Cougr will and won't break between releases
Migration GuideHow to update your game for new cougr-core versions
CLI Reference๐Ÿ”œ Ships with the CLI
Client SDK Reference๐Ÿ”œ Ships with the TypeScript SDK
ADRsArchitecture Decision Records

Cougr ECS Core

Purpose

This document defines the defended conceptual model for Cougr's ECS runtime.

It is the answer to "what are the actual core primitives?" and "which path is the one new users should learn first?"

Core Model

The stable conceptual model is:

  • Entity: an opaque runtime identity
  • Component: typed or raw data attached to entities
  • Query: a declarative selection over entities by component presence
  • System: logic that reads or mutates the world
  • CommandQueue: deferred structural mutations
  • GameApp: app-level orchestration over world + scheduler + plugins
  • RuntimeWorld / RuntimeWorldMut: the shared backend contract for Soroban-first worlds

For Soroban gameplay contracts, the recommended path is:

  • app
  • SimpleWorld
  • SimpleQuery
  • SimpleScheduler
  • GameApp

ArchetypeWorld is the alternate backend for heavier query workloads.

The shared stable overlap between those backends lives in:

  • ecs::RuntimeWorld
  • ecs::RuntimeWorldMut

Backend Roles

SimpleWorld

Use when:

  • entity counts are modest
  • table-backed scans dominate
  • operational simplicity matters more than archetype migration costs

Cost profile:

  • cheap add/remove/update
  • indexed table and all-storage component lookups
  • predictable query path for common gameplay loops

ArchetypeWorld

Use when:

  • multi-component queries dominate
  • entity composition is relatively stable
  • migration cost is acceptable in exchange for tighter query scopes

Cost profile:

  • more expensive structural changes
  • more selective scans for multi-component queries

Learnability Rule

A new user should be able to learn the main Cougr runtime from:

  1. README.md
  2. GameApp
  3. SimpleWorld
  4. SimpleQueryBuilder
  5. one or two canonical examples

If a concept requires diving outside the Soroban-first runtime path to understand basic gameplay flow, that is a product bug.

Account Kernel

Purpose

The goal is to make authorization explicit, modular, and replay-safe while keeping the accounts namespace outside Cougr's frozen 1.0 stable contract.

Core Model

The account subsystem is now organized around:

  • AccountKernel
    • the orchestrator that runs signer verification, policy checks, and replay protection
  • signer interfaces
    • AccountSigner
    • base implementations: direct owner auth, session auth, secp256r1 passkey auth
  • policy interfaces
    • generic Policy<C>
    • base implementations for intent expiry, session enforcement, active device checks, and guardian checks
  • signed intent schema
    • SignedIntent, SignerRef, IntentProof
  • structured auth results
    • AuthResult, AuthMethod

Signed Intent Schema

SignedIntent binds:

  • target account
  • signer reference
  • action payload
  • nonce
  • expiry
  • deterministic action_hash
  • proof material

The deterministic hash is derived from:

  • nonce
  • expiry
  • signer identity fields
  • action system name
  • action bytes

Replay Protection

Cougr uses two replay domains:

  • per-account nonce tracking for direct owner auth and passkey auth
  • per-session nonce tracking for session intents

The replay implementation lives in:

Session Model

Session state now includes:

  • unique key_id
  • scoped allowed actions
  • operation budget
  • expiration timestamp
  • next_nonce

Session enforcement requires all of:

  • session exists
  • action is in scope
  • session is not expired
  • operation budget remains
  • intent nonce matches next_nonce

On success the session consumes one operation and advances next_nonce.

Signers

Current base signer implementations:

  • direct owner signer
    • uses require_auth
  • session signer
    • explicit non-fallback session path evaluated by the kernel
  • secp256r1 passkey signer
    • verifies signatures against registered passkeys

Policies

The policy layer is intentionally reusable across account features.

Current base policies:

  • IntentExpiryPolicy
  • SessionPolicy
  • ActiveDevicePolicy
  • GuardianPolicy

This is how device and recovery support now live under the same policy model instead of ad hoc checks.

Auth Results

AuthResult returns structured information instead of only Result<(), AccountError>.

Current fields:

  • method used
  • nonce consumed
  • session key id, when applicable
  • remaining operations, when applicable

Integration Note

The account kernel is now consumed through the curated accounts / auth surface directly.

The previous GameWorld wrapper was removed so 1.0.0 does not freeze an extra orchestration layer. Authorization should be composed explicitly at the application layer around GameApp, SimpleWorld, and the account primitives.

Standards Layer

Purpose

The standards layer introduces reusable, storage-aware contract primitives in the style of OpenZeppelin building blocks, but shaped for Cougr's Soroban-oriented single-crate model.

These modules are meant to be composed into application contracts and account flows without depending on any example project.

Included Standards

Ownable

  • single-owner access primitive
  • explicit initialization
  • direct transfer and renounce flows
  • typed ownership transition events

Ownable2Step

  • staged ownership handoff
  • pending-owner tracking in storage
  • explicit acceptance requirement before ownership changes
  • cancellation support for abandoned handoffs

AccessControl

  • role-based authorization keyed by Symbol
  • per-role admin delegation
  • explicit grant, revoke, and renounce semantics
  • default admin role for bootstrapping new modules

Pausable

  • storage-backed emergency stop flag
  • explicit paused and unpaused transitions
  • guard methods for mutating entrypoints

ExecutionGuard

  • storage-backed execution lock
  • suited for reentrancy-like protection and mutation serialization
  • can be used as explicit enter/exit calls or as a scoped closure wrapper

RecoveryGuard

  • blocks sensitive flows while a recovery window is active
  • generic enough to compose with account recovery or application-defined incident response

BatchExecutor

  • reusable batch length validation
  • single-path execution semantics for collections of operations
  • explicit empty and oversize rejection

DelayedExecutionPolicy

  • storage-backed delayed operation queue
  • deterministic operation IDs
  • readiness and expiry checks
  • cancellation and execution events

Storage and Namespacing

Each standards module is instantiated with a Symbol identifier.

That identifier becomes part of the storage key, which allows a single contract to host multiple independent instances of the same standard without collisions.

Authorization Model

These modules do not assume hidden caller semantics.

Where authorization matters:

  • Ownable and Ownable2Step require an explicit caller address
  • AccessControl checks the caller against the relevant admin role
  • Pausable, RecoveryGuard, and similar state machines leave the surrounding authorization decision to the integrating contract

This is intentional. Cougr keeps authorization visible at the integration boundary instead of burying it in generic helpers.

Error Semantics

The standards layer uses StandardsError for consistent negative-path behavior across integrations.

Important failure modes include:

  • unauthorized caller
  • duplicate initialization
  • missing or mismatched pending owner
  • duplicate role grant or missing role during revoke
  • paused versus not-paused guard failures
  • execution lock contention
  • recovery-active guard failure
  • empty or oversized batches
  • delayed operation not ready, expired, already executed, or missing

Maturity

Status: Stable

The standards layer is part of Cougr's frozen 1.0 stable contract. Integrators should still supply their own caller-auth composition where required, but the module interfaces and documented failure semantics are now part of the defended public surface.

Cougr Privacy Model

Purpose

This document defines Cougr's privacy and proof-verification contract after the 1.0 release gate.

Its job is to separate the stable privacy subset from experimental proof systems so that the repository can make a smaller, stronger claim about what is safe to depend on in the stable contract.

Stable Privacy Surface

The stable privacy subset in Cougr is:

  • commitments
  • commit-reveal flows
  • hidden-state encoding interfaces
  • Merkle inclusion verification
  • sparse Merkle utilities
  • privacy interfaces:
    • CommitmentScheme
    • MerkleProofVerifier
    • HiddenStateCodec
    • ProofVerifier as an interface contract only

These are exposed through:

  • cougr_core::privacy::stable
  • cougr_core::zk::stable as the compatibility alias

Experimental Privacy Surface

The following remain Experimental:

  • Groth16 proof verification flows
  • proof-submission execution helpers
  • prebuilt verification circuits
  • fog-of-war Merkle exploration orchestration
  • multiplayer ZK state-channel transition contracts
  • recursive proof-composition descriptors
  • advanced hidden-state automation
  • hazmat Poseidon-based privacy helpers
  • broader confidential-state abstractions

These are exposed through:

  • cougr_core::privacy::experimental
  • cougr_core::zk::experimental as the compatibility alias

Compatibility note:

Experimental modules may still be re-exported from cougr_core::zk for transition convenience, but they are not part of Cougr's stable privacy promise. New application code should prefer cougr_core::privacy::experimental so the product-level intent is obvious at the import site.

1.0 Privacy Freeze

The frozen 1.0 privacy contract is exactly:

  • commitments
  • commit-reveal flows
  • hidden-state codec interfaces
  • Merkle inclusion verification
  • sparse Merkle utilities
  • the interface contracts re-exported from cougr_core::privacy::stable

The following are explicitly excluded from the 1.0 stable privacy contract:

  • zk::experimental
  • proof-submission orchestration that depends on experimental verification
  • Groth16 verifier implementations
  • prebuilt advanced circuit helpers
  • state-channel, recursive, and fog-of-war orchestration helpers

Privacy Maturity Table

SurfaceStatusNotes
CommitmentsStableExplicit interface and verification contract
Commit-revealStableExplicit component semantics and deadline behavior
Hidden-state encodingStableStable codec interface; fixed-width codecs can be defended
Merkle inclusion and sparse Merkle utilitiesStableMalformed proof behavior and inclusion semantics are explicit
Proof submission systemsBetaUseful orchestration, but still coupled to experimental verification flows
Groth16 verification and prebuilt circuitsExperimentalAssumptions are explicit, but not yet strong enough for a stable promise

Proof Verification Contract

Cougr's experimental Groth16 verifier makes these explicit guarantees:

  • verification keys must satisfy vk.ic.len() == public_inputs.len() + 1
  • malformed verification-key shape returns ZKError::InvalidVerificationKey
  • malformed pairing inputs return ZKError::InvalidInput
  • a well-formed but invalid proof returns Ok(false) only when the pairing check fails

Cougr does not currently claim stronger guarantees for Groth16 around:

  • subgroup validation beyond Soroban host-type decoding
  • normalization guarantees beyond fixed-width typed wrappers
  • broader proof-system maturity for production confidentiality claims

That is why the implementation remains Experimental even though the verifier interface is explicit.

Merkle Verification Contract

Cougr's stable Merkle verification guarantees:

  • malformed proofs with siblings.len() != depth return ZKError::InvalidProofLength
  • well-formed but non-matching proofs return Ok(false)
  • sparse Merkle utilities produce the same on-chain proof representation used by the stable SHA256 verifier

Hidden-State Encoding Contract

Stable hidden-state codecs must:

  • define an exact byte-level representation
  • reject malformed encoded state with ZKError::InvalidInput
  • avoid silent truncation or padding

The built-in Bytes32HiddenStateCodec satisfies this by requiring an exact 32-byte payload in both directions.

Relationship to Public Surface

This model works with:

Any future claim that advanced proof verification is Stable should add stronger input-validation guarantees, clearer host-assumption boundaries, and tighter negative-path coverage than exists today.

Cougr Feature Flags

Purpose

This document groups Cougr feature flags by maturity and intended usage.

Current Flags

FlagMaturityIntended useNotes
debugSupport-onlyLocal diagnostics and introspectionExposes runtime snapshots and metrics that are not part of the stable product contract
hazmat-cryptoExperimentalAdvanced ZK and cryptographic integrationsEnables low-level host crypto helpers; do not treat as part of the stable privacy promise
testutilsNon-contract support surfaceTests and explicit test-utility consumersEnables testing helpers such as MockAccount

Policy

  • feature flags do not automatically promote a surface into the stable contract
  • test-only or support-only flags remain outside compatibility guarantees
  • new security-sensitive flags should default to Beta or Experimental until their contracts are written down

Relationship To Public Surface

The maturity of the feature flag should be interpreted together with:

In the product-level facade:

  • auth mirrors the Beta accounts surface
  • privacy::stable and privacy::experimental mirror the split inside zk
  • ops mirrors the stable standards surface

Cougr Performance Guide

Purpose

This document explains the current performance model for Cougr's Soroban-first ECS path.

It is not a promise of fixed gas costs. It is a guide to the data structures and tradeoffs that determine query and scheduling behavior.

The practical question it should answer is:

  • which backend should I use
  • where should a component live
  • what kinds of mutations are cheap versus expensive

SimpleWorld Query Model

SimpleWorld now maintains direct component indexes:

  • table_index for table-backed components
  • all_index for table + sparse lookups

That changes the expected behavior of the common query paths:

  • get_table_entities_with_component() uses the direct table index
  • get_all_entities_with_component() uses the all-storage index
  • SimpleQuery selects the narrowest available required component index before filtering

This is the default performance story for gameplay loops.

Use SimpleWorld by default when:

  • your hot loop is dominated by one- or two-component scans
  • you mutate entity composition often
  • you rely on table vs sparse placement to control scan scope

Use ArchetypeWorld when:

  • your hot loop is dominated by repeated multi-component queries
  • entity compositions are relatively stable after setup
  • you are willing to pay more for add/remove migrations to get tighter query scopes

Storage Tradeoffs

Table storage:

  • optimized for repeated scans
  • should back components that appear in hot gameplay loops

Sparse storage:

  • better for infrequent markers or tags
  • excluded from table-only scans by default

If a sparse component starts showing up in tick-critical queries, it is usually a signal that the component belongs in table storage.

Prescriptive rule:

  • if you scan it every tick, it probably belongs in table storage
  • if you mostly address it directly or use it as a sparse marker, keep it sparse

Scheduler Tradeoffs

SimpleScheduler now validates stage-local dependencies before execution.

Costs introduced by the stronger model:

  • dependency validation during run planning
  • topological ordering within each stage

Benefits:

  • explicit execution order
  • early detection of invalid schedules
  • safer composition as system counts grow

This is a good trade in Soroban-oriented contracts because schedule size is typically small relative to the cost of incorrect execution order.

Benchmark Focus Areas

Benchmarks should answer these practical questions:

  • how many entities can the indexed query path scan efficiently
  • when does ArchetypeWorld outperform SimpleWorld
  • what is the cost of adding/removing indexed components
  • what is the cost of stage validation and deferred command application

The current benchmark suite in benches/ecs_bench.rs covers these paths directly.

It now includes:

  • entity spawn cost
  • component insert / lookup cost
  • indexed query vs sparse-inclusive query cost
  • cache warm-read vs invalidated-read behavior
  • scheduler validation + execution cost
  • SimpleWorld vs ArchetypeWorld multi-component query comparison
  • SimpleWorld vs ArchetypeWorld structural mutation comparison

Reading The Current Benchmarks

Interpret the benchmark output in this order:

  1. Query Paths If plain indexed queries and cached queries are already cheap enough, stay on SimpleWorld.
  2. Backend Query Comparison If ArchetypeWorld is materially better on your real multi-component query shape, it may be worth adopting.
  3. Backend Structural Mutation Comparison If archetype migration is significantly more expensive for your workload, do not switch just because query numbers look better in isolation.
  4. Query Cache Invalidation If your world mutates every tick, cache benefits may collapse; optimize data shape first.

Decision Heuristics

Choose SimpleWorld when:

  • gameplay writes are frequent
  • entity compositions change often
  • table/sparse separation gives you enough control
  • your queries are broad but predictable

Choose ArchetypeWorld when:

  • the same multi-component query runs constantly
  • compositions are mostly fixed after startup
  • entity migration cost is amortized over many reads

Keep GameApp, SimpleWorld, and SimpleQuery as the default performance story for new Soroban gameplay code.

Interpretation Rules

Use benchmark output to compare patterns, not to claim universal throughput numbers.

For real contracts, evaluate:

  • data shape
  • component cardinality
  • table vs sparse placement
  • how often the world mutates between repeated queries

Performance guidance should always be tied back to those conditions.

If benchmark results and your data shape disagree, trust the data shape first.

This guide answers where a component should live once you have decided it belongs on-chain. For the prior decision, whether a piece of state or logic justifies being on-chain in the first place, see ONCHAIN_OFFCHAIN_BOUNDARY.md.

Cougr Public API Contract

Purpose

This document defines how Cougr presents its public Rust API for 1.0.

It answers four practical questions:

  • which entrypoints are central to the product
  • which surfaces are usable but still evolving
  • which modules should not be interpreted as production commitments
  • which compatibility shims or testing helpers are intentionally outside the long-term contract

API Positioning

Cougr exposes a broad crate surface, but only a scoped subset is part of the defended 1.0 contract.

The current product story is:

  • cougr-core is primarily an ECS framework for Soroban-compatible applications
  • app is the default gameplay runtime surface for new projects
  • auth, privacy, and ops are the clearest product-level domain namespaces
  • accounts remain Beta, while privacy is split between a stable primitive subset and experimental proof systems
  • ECS onboarding/runtime surfaces and standards are part of the 1.0 stable contract
  • helper APIs that exist only for compatibility or transition should remain clearly demoted

This file now serves as the explicit 1.0 stable API list for cougr-core.

Core entrypoints

These are the frozen entrypoints for the 1.0 stable contract:

  • SimpleWorld
  • ArchetypeWorld
  • ecs::{RuntimeWorld, RuntimeWorldMut, WorldBackend}
  • typed and raw component operations
  • command queues
  • scheduling primitives
  • events, hooks, and observers
  • incremental persistence utilities

Concrete frozen root-level contract:

  • SimpleWorld
  • ArchetypeWorld
  • CommandQueue
  • Component, ComponentTrait, ComponentStorage, ComponentId
  • SimpleQuery, SimpleQueryBuilder
  • RuntimeWorld, RuntimeWorldMut, WorldBackend
  • Resource
  • runtime::ChangeTracker, runtime::TrackedWorld
  • Plugin, PluginGroup, GameApp
  • ScheduleStage, SystemConfig, SimpleScheduler, SystemGroup
  • prelude
  • runtime
  • app
  • ops as the clearest Stable standards namespace
  • standards as a Stable namespace
  • privacy::stable as the clearest stable privacy namespace
  • zk::stable as the stable privacy namespace
  • auth as the clearest Beta account namespace
  • accounts as a Beta namespace
  • privacy::experimental as an explicitly non-contract namespace
  • zk::experimental as an explicitly non-contract namespace

Supported but evolving surfaces

These surfaces are useful and implemented, but should continue to be presented as Beta:

  • accounts
  • higher-level query helpers
  • higher-level scheduler helpers
  • proof-submission helpers in zk

Stable privacy subset

These privacy surfaces are intentionally narrower and can be presented as Stable:

  • commitments
  • commit-reveal
  • hidden-state codec interfaces
  • Merkle inclusion and sparse Merkle utilities
  • zk::stable

Non-contract surfaces

These surfaces are public today, but they must not be interpreted as stable commitments:

  • testing-only helpers
  • advanced proof-verification APIs whose assumptions are still being hardened
  • zk::experimental
  • compatibility shims retained for transition
  • internals-heavy modules whose invariants are not yet documented as stable guarantees

Top-Level Surface in src/lib.rs

Public modules

Current top-level modules:

  • app
  • auth
  • accounts
  • archetype_world
  • commands
  • component
  • debug behind feature flag
  • error
  • event
  • ops
  • privacy
  • plugin
  • query
  • resource
  • scheduler
  • simple_world
  • zk

Internal implementation modules such as hidden scheduler helpers, storage internals, and entity internals are no longer part of the intended default public surface. They may still exist in the repository, but the root crate is not meant to advertise them as onboarding entrypoints. Advanced runtime support such as hooks, observers, change tracking, and incremental storage is exposed through curated re-exports and runtime instead of direct top-level module entrypoints.

Public re-exports

Current top-level re-exports emphasize:

  • worlds: SimpleWorld, ArchetypeWorld
  • backend contracts: RuntimeWorld, RuntimeWorldMut, WorldBackend
  • ECS data: Component, ComponentId, ComponentStorage, ComponentTrait, Position, Resource
  • orchestration: CommandQueue, GameApp, schedulers
  • queries: SimpleQuery, SimpleQueryBuilder
  • domain access through explicit namespaces: auth, privacy, ops, accounts, zk::stable, zk::experimental

Public top-level helper functions

There are no root-level placeholder helper functions in the supported contract.

The sanctioned onboarding path is the curated root surface itself:

  • app
  • auth
  • privacy
  • ops
  • SimpleWorld
  • ArchetypeWorld
  • CommandQueue
  • GameApp
  • app::{named_system, named_context_system} and add_systems

Compatibility Exceptions

Public API Risks

The main public API risks before this cleanup were:

  • the crate exports more surface area than it can reasonably defend as stable
  • some internals-heavy modules are public before their long-term contract is clearly documented
  • some privacy and verification surfaces are easy to overread as production guarantees
  • accounts and privacy modules still include beta-grade behavior that is intentionally documented outside the stable story

Freeze Direction

The 1.0 freeze is intentionally narrower than the full public module graph:

  • app is the clearest default runtime namespace for new gameplay code
  • auth is the clearest Beta auth namespace for application code
  • privacy is the clearest domain namespace for privacy adoption, with stability determined by submodule
  • ops is the clearest stable namespace for operational standards in application code
  • root re-exports and prelude are the default onboarding path
  • runtime is the supported namespace for advanced ECS integrations that are not part of the smallest onboarding contract
  • query and archetype_world retain their cache/state helpers outside the smallest root onboarding surface
  • standards is a supported stable namespace
  • accounts remains a public Beta namespace
  • zk::stable is the only privacy namespace treated as Stable
  • zk::experimental remains public for explicit opt-in use, but outside compatibility guarantees

Cougr Compatibility Promises

Purpose

This document defines the compatibility story Cougr is prepared to defend at 1.0.

It turns the maturity model into explicit expectations for adopters, contributors, and maintainers.

1.0 Baseline

Cougr 1.0.0 freezes a scoped stable surface inside a broader public crate.

That means:

  • compatibility promises are scoped by maturity, not by visibility alone
  • stable, beta, and experimental namespaces can coexist in the same crate
  • the stable guarantee is the documented contract, not every public symbol

Stable Surfaces

The following surfaces are treated as Cougr's strongest 1.0 compatibility commitments:

  • root ECS onboarding and runtime entrypoints documented in API_CONTRACT.md
  • prelude
  • runtime
  • ops
  • standards
  • privacy::stable
  • zk::stable
  • the contracts documented in PRIVACY_MODEL.md for commit-reveal, hidden-state codecs, and Merkle verification

For these surfaces, maintainers should preserve:

  • type and function intent unless there is a documented breaking reason
  • documented failure behavior
  • documented malformed-input behavior where applicable
  • byte-level or proof-shape contracts already written in the privacy model

Beta Surfaces

The following surfaces are supported but intentionally not frozen:

  • higher-level ECS helpers outside the frozen root/runtime contract
  • auth
  • accounts
  • proof-submission orchestration that depends on experimental verification flows

For Beta surfaces, maintainers commit to:

  • keep the product direction coherent
  • document meaningful semantic changes
  • avoid gratuitous churn
  • preserve the curated onboarding path where practical

For Beta surfaces, maintainers do not yet promise:

  • SemVer-stable signatures
  • unchanged storage layouts for every helper
  • unchanged auth or orchestration semantics across all releases

Experimental Surfaces

The following surfaces are explicitly outside compatibility guarantees:

  • privacy::experimental
  • zk::experimental
  • hazmat cryptographic helpers
  • advanced proof-verification helpers and descriptors
  • any public support surface documented as test-only or transition-only

These may:

  • change shape
  • move namespace
  • be removed
  • gain stronger validation that changes edge-case behavior

Non-Contract Support Surfaces

Support-only surfaces such as MockAccount are not part of the default product contract.

They exist for tests and explicit utility consumers, not as long-term framework guarantees.

Change Management Rules

When changing a Stable or Beta public surface, update at minimum:

1.0 Freeze Decisions

The 1.0 release gate decisions are:

  • ECS onboarding and runtime surfaces are in the stable contract
  • ops is the stable domain alias for standards
  • standards is in the stable contract
  • auth is a Beta domain alias and is not part of the stable guarantee
  • accounts remains Beta and is not part of the stable guarantee
  • privacy::stable maps to the frozen privacy contract
  • zk::stable is the frozen privacy contract
  • privacy::experimental remains outside compatibility guarantees
  • zk::experimental remains outside compatibility guarantees

Migration Guide

Purpose

This guide explains how to move existing Cougr integrations toward the curated 1.0 product surface.

It is not a promise that every older pattern disappears immediately. It is the recommended direction for users who want to converge on the defended path.

Core Direction

Prefer these namespaces in new or updated code:

  • app for gameplay runtime
  • auth for account and session flows
  • privacy::stable for stable privacy primitives
  • ops for operational standards

Runtime Migration

From direct world/scheduler wiring

If you currently do something like:

let mut world = SimpleWorld::new(&env);
let mut scheduler = SimpleScheduler::new();

prefer:

let mut app = cougr_core::app::GameApp::new(&env);

and register systems through GameApp.

When multiple systems belong to the same phase, prefer the declarative path:

use cougr_core::app::{named_context_system, named_system, GameApp, ScheduleStage};

let mut app = GameApp::new(&env);
app.add_systems((
    named_system("spawn", |world, env| {
        let entity = world.spawn_entity();
        world.set_typed(env, entity, &Position::new(0, 0));
    })
    .in_stage(ScheduleStage::Startup),
    named_context_system("cleanup_tags", |context| {
        let entities = context
            .world()
            .get_entities_with_component(&symbol_short!("expired"), context.env());
        for i in 0..entities.len() {
            let entity = entities.get(i).unwrap();
            context
                .commands()
                .remove_component(entity, symbol_short!("expired"));
        }
    })
    .in_stage(ScheduleStage::Cleanup),
));

Why:

  • clearer lifecycle
  • explicit stages
  • one onboarding surface instead of several loose primitives
  • a single system registration model for plain and context-aware systems

From the removed pre-1.0 ECS model

If you were previously on the removed pre-1.0 World / System path, port directly to GameApp, SimpleWorld, and SimpleQuery.

Query Migration

If you still do ad-hoc scans or manual component filtering, prefer:

  • SimpleQueryBuilder
  • SimpleQueryState
  • SimpleQueryCache

Both SimpleQueryBuilder and ArchetypeQueryBuilder now support:

  • with_components(...)
  • without_components(...)
  • with_any_components(...)

If you need backend-agnostic gameplay helpers across Soroban-first worlds, prefer:

  • RuntimeWorld
  • RuntimeWorldMut

These are the shared contracts between SimpleWorld and ArchetypeWorld.

Domain Migration

Accounts

If you currently import from accounts directly in application code:

use cougr_core::accounts::SessionBuilder;

prefer:

use cougr_core::auth::SessionBuilder;

The semantics are the same today. The change is about product clarity.

Privacy

If you rely on stable privacy primitives, prefer:

use cougr_core::privacy::stable::...

instead of:

use cougr_core::zk::stable::...

If you rely on advanced proof tooling, prefer:

use cougr_core::privacy::experimental::...

and treat it as an explicit opt-in to non-frozen APIs.

Standards

If you currently import standards directly:

use cougr_core::standards::Pausable;

prefer:

use cougr_core::ops::Pausable;

Again, this is a namespace migration for clarity, not a semantic rewrite.

Example-Level Migration

Use these examples as references:

  • snake for app::GameApp and stage-based gameplay loops
  • battleship for privacy::stable and hidden-information patterns
  • guild_arena for account/session/recovery patterns

What Does Not Need Immediate Migration

You do not need to rewrite everything at once if:

  • the contract still needs a focused port from the removed pre-1.0 runtime path
  • you are preserving an older example or integration
  • your current code already sits behind a stable local abstraction

The main goal is to stop growing new code on top of older default imports.

Migration Checklist

  • move runtime entrypoints to app where practical
  • move account imports to auth
  • move stable privacy imports to privacy::stable
  • move standards imports to ops
  • update local docs/examples to use the curated namespaces

CLI Reference

โณ This page does not exist yet because the cougr-cli crate does not exist yet.

Per docs/strategy/12-documentation-architecture.md: "CLI reference - Does not exist because the CLI does not exist yet; ships alongside it."

When the CLI ships, this page will document all cougr new, cougr add, and cougr check subcommands.

Tracked in: salazarsebas/Cougr issues

Client SDK Reference

โณ This page does not exist yet because the TypeScript client SDK does not exist yet.

Per docs/strategy/12-documentation-architecture.md: "Client SDK reference - Ships alongside the SDK described in 06-product-strategy.md."

When the SDK ships, this page will document the TypeScript API for connecting a frontend to a Cougr game contract, including wallet integration and session key management.

Tracked in: salazarsebas/Cougr issues

Architecture Decision Records

Architecture Decision Records (ADRs) document significant technical decisions made in Cougr's design. They are kept in the main salazarsebas/Cougr repository under docs/adr/ and synced here automatically.

Each ADR follows the format: context โ†’ decision โ†’ consequences.

ADRTitle
0001Public API Surface
0002Accounts Beta
0003Privacy Model Split
0004Standards Layer Stable
0006Game Circuit Suite
0007Workspace Subcrates

ADR 0001: Curated Public Surface

Status

Accepted

Context

Cougr exposes a broad API. Without curation, adopters can easily mistake public visibility for stable-contract inclusion.

Decision

Cougr keeps a curated onboarding path at the crate root and explicitly separates:

  • root-level ECS onboarding re-exports
  • standards as a Stable namespace
  • zk::stable as the stable privacy namespace
  • accounts as a Beta namespace
  • zk::experimental as the explicit non-contract privacy namespace

Consequences

  • docs can name the golden path without pretending the whole crate is frozen
  • advanced but useful namespaces remain available
  • public visibility alone is no longer the compatibility signal

ADR 0002: Keep Accounts Out Of The Stable 1.0 Contract

Status

Accepted

Context

The account kernel, typed intents, replay domains, passkey support, and session enforcement are implemented. However, account abstraction remains a security-sensitive area with meaningful design-space risk.

Decision

Cougr will keep accounts as a Beta namespace at 1.0 even though the kernel exists and is tested.

Consequences

  • the repo can truthfully document real implementation value
  • maintainers keep room to tighten signer, policy, and integration contracts
  • adopters are warned not to treat the current account API as SemVer-frozen

ADR 0003: Stable Privacy Subset With Experimental Verification

Status

Accepted

Context

Cougr contains both defensible privacy primitives and faster-moving proof-verification helpers. Treating them as one maturity tier would overclaim guarantees.

Decision

Cougr formally splits privacy into:

  • zk::stable for commitments, commit-reveal, hidden-state codecs, and Merkle verification
  • zk::experimental for advanced proof verification, circuits, channels, recursive layouts, and hazmat helpers

Consequences

  • privacy claims can stay narrow and defendable
  • advanced ZK work can continue without blocking the stable subset
  • compatibility promises can be scoped precisely by namespace

ADR 0004: Sandbox Design

Status

Accepted

Context

We need a secure testing and simulation environment for contracts to allow developers to validate logic (like ECS events and ZK proofs) locally without full node deployments. The sandbox should emulate the target environment accurately.

Decision

We introduce a test sandbox utilizing no_std and alloc alongside the Soroban testutils feature. This sandbox provides core modules for testing games (such as GameHarness, Scenario, WorldFixture, ReplayLog, and SnapshotAssert).

Consequences

  • Developers can write fast local tests with a familiar testing API.
  • We must maintain parity between sandbox behavior and on-chain execution.
  • Relies on the testutils feature flag being managed correctly in the crate.

ADR 0004: Include Standards In The Stable 1.0 Contract

Status

Accepted

Context

The standards layer is documented, integration-tested, and intentionally designed as a reusable framework surface rather than example glue.

Decision

Cougr includes standards in the stable 1.0 contract.

Consequences

  • integrators can treat standards as part of the defended public framework surface
  • future changes to these modules now carry stable-contract weight
  • authorization composition remains explicit at the integration boundary rather than hidden inside the primitives

ADR 0005: Session UX

Status

Accepted

Context

Repeatedly signing transactions degrades the user experience for on-chain games, especially those requiring frequent interactions (like real-time or turn-based strategy). Players expect a seamless experience akin to Web2 gaming without constantly engaging with a wallet prompt.

Decision

We introduce session keys with a fluent SessionBuilder API to authorize specific game actions over a time-bound window without requiring repeated user prompts. authorize_with_fallback will be used for graceful degradation, allowing operations to fall back to direct authorization when a session is expired or unavailable.

Consequences

  • Massively improved gameplay experience for end users.
  • Integration becomes slightly more complex to handle session lifecycles.
  • Wallets and client integrations must support and manage session key delegation.

ADR 0006: Game Circuit Suite (cougr_core::circuits)

Status

Accepted

Context

Game developers need fog-of-war, hidden cards, fair dice, and sealed-bid mechanics without authoring Circom circuits and wiring Groth16 verification by hand. Cougr already exposes low-level verifiers under zk::experimental, but the onboarding path requires weeks of ZK specialization.

Decision

  1. Ship four pre-built circuit builders under cougr_core::circuits (always available, Experimental maturity):

    BuilderPublic inputs
    hidden_cards(deck_size, hand_size)deck_root, hand_commitment, player_id, deck_size, hand_size
    fog_of_war(w, h, radius)map_root, prior/next explored roots, origin, tile, radius
    fair_dice(sides, seed_commitment)seed_commitment, roll_result, sides, nonce
    sealed_bid(max_bid)auction_id, bid_commitment, revealed_bid, max_bid
  2. Each builder returns GameCircuitSpec with a frozen PublicInputLayout, placeholder VK (correct IC length), and typed verify methods that delegate to zk::experimental::verify_groth16. Production deploys replace the VK via with_verification_key.

  3. fog_of_war reuses FogOfWarCircuit in zk::advanced - no duplicate verification logic.

  4. Circom scaffolds and off-chain scripts live in internal/cougr-core-circuits/ (publish = false). Rust implementation lives in src/circuits/.

  5. Canonical examples demonstrate each builder:

    • examples/hidden_hand/
    • examples/fog_explorer/
    • examples/dice_duel/
    • examples/blind_auction/

Consequences

  • Developers integrate common game privacy patterns in hours, not weeks.
  • Public-input layouts are versioned by CircuitId and must not change without a new ADR.
  • On-chain builders ship an unbound VK; load test/production keys from internal/cougr-core-circuits (bun run pipeline โ†’ exported/*_vk.json).
  • Circuits use Poseidon + game constraints (~325โ€“13.6k R1CS); pot14 trusted setup.
  • zk::stable remains unchanged; all new surface stays Experimental until external audit.

ADR 0007: Workspace Subcrates Compiled Into cougr-core

Status

Accepted

Context

Cougr is adding three competitive layers (ZK circuit builders, session UX, game testing sandbox). They need compile-time isolation without publishing separate crates.io packages, so download metrics stay unified under cougr-core.

Decision

  1. Add a Cargo workspace with three internal members under internal/:

    • cougr-core-circuits
    • cougr-core-session
    • cougr-core-test
  2. Each internal member sets publish = false.

  3. Each layer's implementation lives in src/{circuits,session,test}/inner.rs. The public modules include! that file; internal workspace members point their [lib] path at the same inner.rs for isolated cargo check -p runs. This avoids:

    • circular dependencies (session needs auth from the same crate)
    • cargo publish failures on unpublished path deps
    • missing files in the published tarball
  4. Public API:

    • cougr_core::circuits - always available
    • cougr_core::session - always available
    • cougr_core::test - testutils feature only
  5. The test sandbox uses no_std + alloc, not std. It runs in Soroban testutils environments the same way contract tests do today.

Consequences

  • One cargo add cougr-core for all capabilities
  • Internal folders can still be checked with cargo check -p cougr-core-session
  • cargo publish ships internal/** sources inside the cougr-core tarball
  • Feature testutils keeps sandbox code out of contract WASM builds

ADR 0008: Include Standards In The Stable 1.0 Contract

Status

Accepted

Context

The standards layer is documented, integration-tested, and intentionally designed as a reusable framework surface rather than example glue.

Decision

Cougr includes standards in the stable 1.0 contract.

Consequences

  • integrators can treat standards as part of the defended public framework surface
  • future changes to these modules now carry stable-contract weight
  • authorization composition remains explicit at the integration boundary rather than hidden inside the primitives

Showcase

The Showcase is a live directory of games and demos built with Cougr. It is generated automatically from the example catalog and each example's own README.md โ€” no manual duplication.

  • Example Gallery โ€” filter by category and maturity to find the right reference for your use case.
  • Click any card to see the full detail page, pulled directly from that example's README.md.

How it works

The gallery is a static, build-time-generated set of pages โ€” zero backend, zero database. The generator (cougr-site/generate-showcase.py) reads:

  1. examples/catalog.toml โ€” structured metadata (category, maturity, Cougr features, optional screenshot/testnet contract).
  2. Each example's README.md โ€” the description and full documentation.
  3. packages/tokens/tokens.json โ€” design tokens (colors, typography, spacing) consumed as CSS custom properties, so the showcase never visually diverges from the docs site.

To generate locally:

python3 cougr-site/generate-showcase.py
mdbook serve cougr-site

Preview images

Examples with a preview.svg in their directory display it in the gallery card and detail page. Examples without a preview image render cleanly without any broken image tags.

Submit your game

Once your example is cataloged in examples/catalog.toml and meets the quality standard, it will appear in the gallery automatically. A "Cougr Verified" badge can be earned by opening a PR โ€” the catalog's verified field controls whether the badge renders.

account_kernel_counter

Other Canonical
authsession-auth

Canonical example proving that cougr add account-kernel produces a module that is usable in a real Soroban game, not just importable.

The game is a simple per-owner counter. Every increment must be authorized through the account_kernel module that was scaffolded by cougr add account-kernel.


How this project was generated

# 1. Scaffold the project from the session-auth template.
cougr new account_kernel_counter --template session-auth
cd account_kernel_counter

# 2. Drop in the account-kernel piece (writes src/account_kernel.rs
#    and adds `pub mod account_kernel;` to lib.rs).
cougr add account-kernel

# 3. Build and test.
cargo test
stellar contract build

src/account_kernel.rs has one change from the file written by step 2: the testutils::Address as _ import is gated with #[cfg(test)] instead of #[cfg(any(test, feature = "testutils"))]. The testutils feature is not declared by this crate, so the original attribute causes Clippy to emit an unexpected_cfgs error under -D warnings. Moving the import under #[cfg(test)] is the correct fix; everything else in the file is verbatim piece output.


Purpose and pattern

account_kernel_counter is the end-to-end proof that the account-kernel piece integrates cleanly into a real game contract. It strips away all game mechanics so you can focus on:

  1. Register โ€“ owner calls register_session once to approve a scoped session key via account_kernel::build_scope and SessionStorage::store.
  2. Authorize โ€“ each increment call delegates to account_kernel::authorize_action, which runs the full AccountKernel โ†’ SessionPolicy pipeline (expiry check, op-budget decrement, nonce advancement).
  3. Reject โ€“ expired sessions, replayed nonces, and exhausted budgets all surface as panics from authorize_action, preventing the counter from advancing.

Authorization flow

register_session(owner, key_id, max_ops, expires_at)
  โ””โ”€ account_kernel::build_scope()          โ† unmodified piece
     โ””โ”€ SessionBuilder โ†’ SessionScope
  โ””โ”€ SessionStorage::store()

increment(owner, key_id, nonce, intent_expires_at)
  โ””โ”€ account_kernel::authorize_action()     โ† unmodified piece
     โ””โ”€ AccountKernel::authorize_session()
        โ”œโ”€ SessionPolicy: expiry check
        โ”œโ”€ SessionPolicy: action-name allow-list
        โ”œโ”€ SessionPolicy: nonce advancement
        โ””โ”€ SessionPolicy: op-budget decrement
  โ””โ”€ counter += 1  (only reached on Ok)

Public contract API

FunctionParametersReturnsDescription
register_sessionowner, key_id, max_ops, expires_at()Approve a scoped session for this owner (requires owner auth)
incrementowner, key_id, nonce, intent_expires_atu32Increment counter if the session authorizes the tap action
countowneru32Current counter value for the owner

Cougr APIs used

  • account_kernel::build_scope โ€“ piece helper that calls SessionBuilder
  • account_kernel::counter_action โ€“ piece helper that constructs the tap GameAction
  • account_kernel::authorize_action โ€“ piece helper that calls AccountKernel::authorize_session
  • AccountKernel โ€“ separates signer verification, policy evaluation, and replay protection
  • SessionStorage โ€“ load / store session keys on-chain
  • SessionBuilder โ€“ declare allowed actions, max operations, and expiry

Test coverage

TestPath exercised
in_policy_action_increments_counterAllow โ€“ valid session, correct nonces, counter advances
expired_session_blocks_incrementExpiry โ€“ ledger timestamp past expires_at, increment panics
replayed_nonce_blocks_incrementReplay โ€“ same nonce submitted twice, second increment panics
budget_exhaustion_blocks_further_incrementsOp-budget runs to zero, next increment panics
count_returns_zero_before_any_incrementBaseline โ€“ uninitialized counter reads as 0

The piece's own unit tests (account_kernel::tests::*) also run as part of cargo test and cover build_scope, expiry, and nonce isolation at the function level.


Build and test

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
stellar contract build

When to use which example

Use account_kernel_counter whenโ€ฆUse session_arena whenโ€ฆ
Proving cougr add account-kernel works end-to-endLearning SessionManager lifecycle
Understanding the raw AccountKernel / authorize_action pathNeed approve / renew / fallback patterns
Writing tests that exercise the piece's own helpersNeed MockSession testutils

License

MIT

AI Dungeon Master Arena Example

Other Transitional
authzk-circuits

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

This example demonstrates a complex gameplay loop on Stellar/Soroban using:

  1. x402-style Paid Actions: Premium encounters or rerolls purchased in-game via a request-driven model.
  2. stellar-zk (via Cougr-Core): Proof-backed validation of sensitive encounter progression and reward eligibility.

Features

  • Run Initialization: Start an entity-backed arena run for a player.
  • Turn-based Combat: Attack or defend in procedural encounters.
  • x402 Hooks: Premium rerolls or hints appearing at specific progression points.
  • ZK Verification: Submit Groth16 proofs to validate off-chain state or hidden attributes on-chain.
  • Reward System: Track and claim rewards earned from verified milestones.

Architecture

The example follows an ECS-like structure (Entity Component System):

  • RunStateComponent: Core session data.
  • EncounterComponent: Current enemy and challenge data.
  • PremiumActionComponent: Tracks availability of x402-driven premium options.
  • ProofStateComponent: Stores verification status for ZK-backed progression.
  • RewardComponent: Management of in-game assets and rewards.

stellar-zk Integration

This contract uses cougr-core::zk which is built for high-performance on-chain verification compatible with stellar-zk generated circuits.

  • The verify_run_proof function takes a Groth16Proof and public inputs.
  • It validates the proof against a stored VerificationKey (which would correspond to the circuit built for your arena logic).
  • Successful validation unlocks pending rewards and confirms floor progression.

x402 Flow

  1. Player completes a floor.
  2. At every 3rd floor, a PremiumActionComponent is attached to their run.
  3. This signals an HTTP 402 Payment Required state to the frontend/off-chain DM.
  4. Player pays the fee (XLM/tokens) to purchase the purchase_premium_action.
  5. Contract verifies payment and grants a high-tier reward or easier encounter path.

Development

Prerequisites

  • Soroban CLI
  • Rust toolchain with wasm32-unknown-unknown target.

Build

cargo build --target wasm32-unknown-unknown --release

Test

cargo test

On-Chain Deployment

  1. Deploy the contract: soroban contract deploy ...
  2. Initialize VK: soroban contract invoke --id <ID> --fn set_vk --vk <VK_JSON>
  3. Start run: soroban contract invoke --id <ID> --fn init_run --player <ADDR>

Acceptance Criteria

  • Includes x402 representation in purchase_premium_action.
  • Real stellar-zk verification path in verify_run_proof.
  • Deterministic run loop and full test coverage.
  • Soroban-compatible build (wasm32v1-none).

Angry Birds on Stellar Soroban

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 turn-based physics puzzle implementation using the Cougr ECS framework on Stellar/Soroban. This contract demonstrates complex state transitions, entity relationships, and bounded on-chain computation suitable for blockchain gaming.

Game Design

Angry Birds is a physics-based puzzle game where players launch projectiles (birds) at structures to eliminate targets (pigs). This implementation focuses on turn-based gameplay with deterministic physics simulation.

Core Mechanics

  • Turn-based shooting: Players submit angle and power parameters for each shot
  • Deterministic physics: Fixed-point mathematics ensure reproducible results
  • Material system: Different materials (wood, glass, stone) have varying damage resistance
  • Bounded computation: Maximum 50 simulation steps per shot prevents gas limit issues
  • Win/loss conditions: Win by destroying all targets, lose by running out of birds

ECS Architecture

This implementation showcases the Cougr-Core ECS framework with the following components and systems:

Components

ComponentFieldsPurpose
PositionComponentx: i32, y: i32Grid position using fixed-point math (scaled by 1000)
HealthComponenthp: u32, max_hp: u32Hit points for structures and pigs
MaterialComponentkind: enum {Wood, Glass, Stone}Damage resistance multiplier
ProjectileComponentbird_type: enum, angle: i32, power: i32, active: boolShot parameters for trajectory resolution
ScoreComponentpoints: u32, birds_remaining: u32Running score tracker
LevelConfigComponentlevel_id: u32, status: enum, player: AddressLevel metadata and game state

Systems

  1. ShotResolutionSystem - Resolves projectile trajectory using discrete steps (max 50 iterations)
  2. DamageSystem - Applies damage based on material resistance (wood < glass < stone)
  3. ScoreSystem - Calculates points from destroyed entities and remaining birds
  4. WinConditionSystem - Checks if all pigs are eliminated (win) or no birds remain (loss)

Technical Constraints

Fixed-Point Mathematics

  • All positions and velocities use fixed-point math scaled by 1000
  • Eliminates floating-point inconsistencies across different environments
  • Ensures deterministic behavior crucial for blockchain applications

Bounded Computation

  • Maximum 50 simulation steps per shot
  • Prevents unbounded loops and gas limit issues
  • Grid-based collision detection for efficiency

Deterministic Behavior

  • Same input (angle, power, bird_type) always produces the same output
  • Critical for fair gameplay and blockchain verification

Contract API

init_level(env: Env, player: Address, level_id: u32) -> LevelState

Initializes a new game level with default configuration:

  • 3 birds available
  • Empty game field
  • Score reset to 0
  • Game status set to InProgress

shoot(env: Env, player: Address, angle: i32, power: i32) -> ShotOutcome

Executes a shot with specified parameters:

  • angle: Launch angle in degrees (scaled by 1000)
  • power: Launch power (scaled by 1000)
  • Returns shot outcome (hit, miss, win, loss)
  • Validates player authorization and game state

get_state(env: Env) -> LevelState

Returns current game state including:

  • Level ID and status
  • Current score and birds remaining
  • Number of entities in the game

reset_level(env: Env, player: Address) -> LevelState

Resets the current level to initial state:

  • Maintains same level ID and player
  • Resets score, birds, and game status
  • Clears all game entities

Building and Testing

Prerequisites

  • Rust 1.88+ with wasm32 target
  • Stellar CLI for contract deployment
  • Git for cloning the repository

Setup

# Clone the Cougr repository
git clone https://github.com/salazarsebas/Cougr.git
cd Cougr/examples/angry_birds

# Install Rust targets
rustup target add wasm32v1-none

# Install Stellar CLI
brew install stellar-cli  # macOS
# or follow https://github.com/stellar/stellar-cli for other platforms

Build Commands

# Standard Rust build
cargo build

# Release build with optimizations
cargo build --release

# Stellar contract build (produces .wasm)
stellar contract build

# Format code
cargo fmt

# Lint code
cargo clippy --all-targets --all-features -- -D warnings

Testing

# Run all tests
cargo test

# Run specific test
cargo test test_level_initialization

# Run tests with output
cargo test -- --nocapture

Deployment

Testnet Deployment

# Set up testnet environment
stellar network testnet

# Deploy contract
stellar contract deploy \
  --wasm target/wasm32v1-none/release/angry_birds.wasm \
  --source <your-account> \
  --network <NETWORK>

# Note the contract ID for subsequent interactions

Contract Interaction

# Initialize a level
stellar contract invoke \
  --id <contract-id> \
  --source <player-account> \
  --network <NETWORK> \
  -- init_level \
  --player <player-address> \
  --level_id 1

# Take a shot (45 degrees, medium power)
stellar contract invoke \
  --id <contract-id> \
  --source <player-account> \
  --network <NETWORK> \
  -- shoot \
  --player <player-address> \
  --angle 45000 \
  --power 500000

# Get game state
stellar contract invoke \
  --id <contract-id> \
  --source <any-account> \
  --network <NETWORK> \
  -- get_state

Game Example

#![allow(unused)]
fn main() {
// Initialize game
let state = AngryBirdsContract::init_level(env, player, 1);
// state: LevelState { level_id: 1, status: InProgress, score: 0, birds_remaining: 3, ... }

// Take first shot
let outcome = AngryBirdsContract::shoot(env, player, 45000, 500000);
// outcome: ShotOutcome::Miss (no entities to hit yet)

// Check state after shot
let state = AngryBirdsContract::get_state(env);
// state: LevelState { ..., birds_remaining: 2, ... }
}

Design Tradeoffs

Fixed-Point vs Floating-Point

  • Choice: Fixed-point mathematics scaled by 1000
  • Rationale: Ensures deterministic behavior across different execution environments
  • Tradeoff: Limited precision compared to floating-point, but sufficient for game physics

Bounded Simulation

  • Choice: Maximum 50 simulation steps per shot
  • Rationale: Prevents gas limit issues and ensures predictable execution cost
  • Tradeoff: May limit complex trajectory calculations, but adequate for turn-based gameplay

Grid-Based Collisions

  • Choice: Simple circular collision detection with fixed radius
  • Rationale: Computationally efficient and easy to verify
  • Tradeoff: Less precise than continuous collision detection, but sufficient for puzzle gameplay

Integration with Cougr Framework

This example demonstrates several key benefits of the Cougr ECS framework:

  1. Component Modularity: Each game aspect (position, health, material) is a separate, reusable component
  2. System Separation: Game logic is organized into discrete systems with clear responsibilities
  3. Scalability: Adding new features (power-ups, different bird types) requires minimal code changes
  4. Testability: Components and systems can be tested independently
  5. Clarity: The ECS architecture makes game state and logic immediately understandable

Future Enhancements

Potential extensions that showcase Cougr's flexibility:

  • Multiple bird types with unique behaviors
  • Power-up system using additional components
  • Level editor for creating custom scenarios
  • Multiplayer support with turn management
  • Leaderboard system for high scores
  • Visual themes using component-based rendering

Contributing

When contributing to this example:

  1. Follow the existing ECS patterns and component structure
  2. Maintain deterministic behavior for all game logic
  3. Add comprehensive tests for new features
  4. Update documentation for API changes
  5. Ensure all build commands pass without errors

License

This example is licensed under MIT OR Apache-2.0, consistent with the main Cougr project.

Arkanoid 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 Arkanoid game logic on the Stellar blockchain via Soroban smart contracts.

Overview

This example showcases:

  • ECS Architecture: Component-based design using Cougr-Core patterns
  • On-Chain Game Logic: Complete Arkanoid implementation without graphical interface
  • Soroban Integration: Smart contract deployment on Stellar blockchain
  • Comprehensive Testing: Full test coverage of game mechanics

Game Components

Following the Cougr-Core ECS pattern:

  • PaddleComponent: Horizontal paddle position
  • BallComponent: Ball position and velocity
  • BricksComponent: Grid of breakable bricks (10x5)
  • ScoreComponent: Score, lives, and game state

Prerequisites

Required Tools

  1. Rust (1.88.0 or later)

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    rustup update
    
  2. Cargo (comes with Rust)

  3. Stellar CLI

    cargo install stellar-cli --locked
    
  4. WASM Target

    rustup target add wasm32v1-none
    

Verify Installation

rustc --version
cargo --version
stellar --version

Setup

1. Clone Repository

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

2. Install Dependencies

cargo update

Development

Build

# Standard build
cargo build

# Release build
cargo build --release

# WASM build
cargo build --target wasm32v1-none --release

# Stellar CLI build (recommended for deployment)
stellar contract build

Test

# Run all tests
cargo test

# Run tests with output
cargo test -- --nocapture

# Run specific test
cargo test test_init_game

Lint

# Format code
cargo fmt

# Run clippy
cargo clippy --all-features -- -D warnings

Deployment

1. Setup Testnet Account

Get test XLM from Friendbot:

# Generate a new keypair
stellar keys generate alice --network <NETWORK>

# Fund the account
stellar keys fund alice --network <NETWORK>

Or use the web faucet: https://faucet-stellar.acachete.xyz

2. Deploy Contract

# Build the contract
stellar contract build

# Deploy to testnet
stellar contract deploy \
  --wasm target/wasm32v1-none/release/arkanoid.wasm \
  --source alice \
  --network <NETWORK>

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

3. Invoke Contract Functions

Initialize Game

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source alice \
  --network <NETWORK> \
  -- init_game

Expected Output:

{
  "paddle_x": 50,
  "ball_x": 50,
  "ball_y": 50,
  "ball_vx": 1,
  "ball_vy": -1,
  "score": 0,
  "lives": 3,
  "game_active": true,
  "bricks_remaining": 50
}

Move Paddle

# Move right (direction = 1)
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source alice \
  --network <NETWORK> \
  -- move_paddle --direction 1

# Move left (direction = -1)
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source alice \
  --network <NETWORK> \
  -- move_paddle --direction -1

Update Game Tick

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source alice \
  --network <NETWORK> \
  -- update_tick

This advances the game by one frame:

  • Moves the ball
  • Checks collisions (walls, paddle, bricks)
  • Updates score and lives
  • Checks win/loss conditions

Get Game State

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source alice \
  --network <NETWORK> \
  -- get_game_state

Check Game Over

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source alice \
  --network <NETWORK> \
  -- check_game_over

Returns true if game is over, false otherwise.

Game Flow

  1. Initialize: Call init_game() to set up the game state
  2. Play Loop:
    • Call move_paddle(direction) to control the paddle
    • Call update_tick() to advance the game
    • Call get_game_state() to check current state
  3. Game Over: When lives reach 0 or all bricks are broken

Game Mechanics

Constants

  • Field: 100x60 units
  • Paddle: Width 15, Speed 3
  • Ball: Speed 1
  • Bricks: 10 columns ร— 5 rows (50 total)
  • Lives: 3

Scoring

  • Breaking a brick: +10 points
  • Win condition: Break all bricks
  • Loss condition: Lives reach 0

Collisions

  • Top/Side Walls: Ball bounces
  • Bottom Wall: Lose 1 life, ball resets
  • Paddle: Ball bounces upward
  • Bricks: Brick breaks, ball bounces, score increases

Troubleshooting

Rust Version Issues

rustup update stable
rustup default stable

Dependency Resolution Errors

cargo clean
cargo update
cargo build

WASM Build Failures

Ensure WASM targets are installed:

rustup target add wasm32v1-none

Stellar CLI Not Found

cargo install stellar-cli --locked --force

Network Errors

Use --simulate flag to test without deploying:

stellar contract invoke --id <CONTRACT_ID> --simulate -- init_game

Architecture

Cougr-Core Benefits

This example demonstrates how Cougr-Core simplifies on-chain game development:

  1. Component Pattern: Modular game data (Paddle, Ball, Bricks, Score)
  2. System Pattern: Organized logic (Physics, Collision, Scoring systems)
  3. Scalability: Easy to add features (power-ups, multiple balls)
  4. Clarity: Clear separation of data and operations
  5. Reusability: Components can be used across different games

ECS Systems

  • PhysicsSystem: Updates ball position based on velocity
  • CollisionSystem: Handles wall, paddle, and brick collisions
  • ScoringSystem: Manages score, lives, and win/loss conditions

Testing

The test suite covers:

  • โœ… Game initialization
  • โœ… Paddle movement (left, right, bounds)
  • โœ… Ball physics
  • โœ… Wall collisions (top, sides, bottom)
  • โœ… Paddle collision
  • โœ… Brick breaking
  • โœ… Scoring
  • โœ… Lives system
  • โœ… Game over conditions
  • โœ… Inactive game state

Run tests with:

cargo test

CI/CD

GitHub Actions workflow automatically:

  • Checks code compilation
  • Runs formatting checks
  • Runs clippy lints
  • Executes all tests
  • Builds WASM contracts
  • Builds with Stellar CLI

License

MIT OR Apache-2.0

Resources

Asteroids (Soroban ECS Example)

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.

This example demonstrates a complete Asteroids game implementation using Cougr ECS patterns on Soroban.

ECS Architecture

This example showcases full Entity-Component-System design following Cougr-Core patterns:

Components

All game objects are represented as ECS components implementing ComponentTrait:

  • ShipComponent - Player ship with position (x, y), velocity (vx, vy), and rotation angle
  • AsteroidComponent - Asteroids with position, velocity, and size (splits on destruction)
  • BulletComponent - Projectiles with position, velocity, and lifetime counter
  • ScoreComponent - Game state tracking points and lives

Systems

Game logic is organized into discrete systems that operate on components:

  • MovementSystem - Updates positions based on velocities with screen wrapping
  • CollisionSystem - Detects bullet-asteroid and ship-asteroid collisions
  • ShootingSystem - Spawns bullet entities from ship position and angle
  • AsteroidSplitSystem - Splits large/medium asteroids into smaller ones on destruction

Why ECS?

Compared to vanilla Soroban structs, the ECS approach provides:

  1. Modularity - Components are reusable across different game types
  2. Clarity - Systems clearly separate concerns (movement, collision, scoring)
  3. Extensibility - Adding features (power-ups, enemies) requires only new components/systems
  4. Testability - Individual systems can be tested in isolation

See examples/pong/ and examples/arkanoid/ for similar ECS patterns.

Project Structure

.
โ”œโ”€โ”€ src
โ”‚   โ”œโ”€โ”€ lib.rs      # ECS components, systems, and contract implementation
โ”‚   โ””โ”€โ”€ test.rs     # Component serialization and gameplay tests
โ”œโ”€โ”€ Cargo.toml
โ””โ”€โ”€ README.md

Setup

  1. Install Rust + Cargo:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. Add the WASM target:
rustup update
rustup target add wasm32v1-none
  1. Install the Stellar CLI:
cargo install stellar-cli --locked

Development

Build and test locally:

cd examples/asteroids
cargo fmt
cargo build
cargo test
cargo clippy

Build Soroban WASM:

stellar contract build

Deployment

Deploy to Testnet:

stellar network add testnet \
  --rpc-url https://soroban-testnet.stellar.org \
  --network-passphrase "Test SDF Network ; September 2015"

stellar keys generate testnet --network <NETWORK>
stellar keys fund testnet --network <NETWORK>

stellar contract deploy \
  --wasm target/wasm32v1-none/release/asteroids.wasm \
  --source testnet \
  --network <NETWORK>

Invoke contract methods:

# Initialize game
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source testnet \
  --network <NETWORK> \
  -- \
  init_game

# Control ship
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source testnet \
  --network <NETWORK> \
  -- \
  rotate_ship --delta_steps 1

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source testnet \
  --network <NETWORK> \
  -- \
  thrust_ship

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source testnet \
  --network <NETWORK> \
  -- \
  shoot

# Update game state
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source testnet \
  --network <NETWORK> \
  -- \
  update_tick

# Query state
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source testnet \
  --network <NETWORK> \
  -- \
  get_game_state

Tests

The test suite covers:

  • Component serialization/deserialization
  • Ship rotation and thrust mechanics
  • Bullet spawning and lifetime cleanup
  • Asteroid splitting on destruction
  • Ship-asteroid collision and lives system
  • Game over conditions (no lives or no asteroids)

Run tests:

cargo test

Expected output:

test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Game Mechanics

  • Ship: Rotates in 8 directions, applies thrust in facing direction
  • Bullets: Travel in straight lines, despawn after 50 ticks
  • Asteroids:
    • Size 3 (large) splits into 2 size 2 asteroids
    • Size 2 (medium) splits into 2 size 1 asteroids
    • Size 1 (small) is destroyed completely
  • Scoring: +10 points per asteroid hit
  • Lives: Start with 3, lose 1 on ship-asteroid collision
  • Win: Destroy all asteroids
  • Lose: Lives reach 0

Verification (Feb 22, 2026)

All standard build commands pass:

cargo build    # โœ“ No errors
cargo test     # โœ“ 11 tests pass
cargo clippy   # โœ“ No warnings

Battleship with Hidden Board

Hidden Information Canonical
privacycommit-revealgame-appecs
battleship preview

A two-player Battleship game demonstrating hidden information using commit-reveal pattern and Merkle proofs on Stellar Soroban. Players commit their board layouts cryptographically, then prove hit/miss results without revealing unattacked positions.

This example is Cougr's canonical hidden-information reference. It intentionally leans on the stable privacy surface in cougr_core::privacy::stable instead of re-defining Merkle verification inside the example.

Status

Canonical - maintained reference implementation for commit-reveal + selective disclosure on Soroban. Uses cougr-core = "1.1.0", privacy::stable Merkle primitives, and impl_component! macros for standardized serialization.

The Hidden Information Problem

Traditional on-chain games face a challenge: all data is public. In Battleship, if boards are stored directly on-chain, opponents can see ship positions and cheat.

Solution: Commit-Reveal + Merkle Proofs

SETUP PHASE (hide boards)
โ”œโ”€ Player A: commitment = SHA256(board || salt)
โ”œโ”€ Player A: merkle_root = MerkleTree(board).root
โ”œโ”€ Submit (commitment, merkle_root) on-chain
โ””โ”€ Player B: same process

ATTACK PHASE (selective reveal)
โ”œโ”€ Attacker: attack(x, y)
โ”œโ”€ Defender: reveal_cell(x, y, value, merkle_proof)
โ”œโ”€ Contract: verify proof against merkle_root
โ””โ”€ Record hit/miss (other cells remain hidden)

END PHASE (anti-cheat)
โ”œโ”€ Winner declared when all ships sunk
โ””โ”€ Full board can be verified against commitment

Key Properties:

  • โœ… Hiding: Unattacked cells remain secret
  • โœ… Binding: Can't change board after commitment
  • โœ… Selective Reveal: Prove one cell without revealing others
  • โœ… Verifiable: Merkle proofs ensure honesty

Game Flow

1. Setup Phase

#![allow(unused)]
fn main() {
new_game(player_a, player_b)
}

Each player computes off-chain:

#![allow(unused)]
fn main() {
// 1. Create 10x10 board (0=water, 1=ship)
let board = [0u32; 100];
board[0] = 1; // Ship at (0,0)

// 2. Compute commitment
let commitment = SHA256(board || salt);

// 3. Build Merkle tree
let merkle_root = MerkleTree::new(board).root();

// 4. Submit on-chain
commit_board(player, commitment, merkle_root)
}

2. Attack Phase

Alternating turns:

Attacker:

#![allow(unused)]
fn main() {
attack(attacker, x, y)
}

Defender:

#![allow(unused)]
fn main() {
// Off-chain: get Merkle proof for cell (x,y)
let proof = merkle_tree.get_proof(x, y);

// On-chain: reveal with proof
reveal_cell(defender, x, y, value, proof)
}

Contract verifies:

  1. Proof is valid against stored merkle_root
  2. Records hit (value=1) or miss (value=0)
  3. Updates ship count
  4. Switches turn

3. Win Condition

Game ends when one player's ships are all sunk (17 hits total: 5+4+3+3+2).

Contract API

FunctionParametersDescription
new_gameplayer_a: Address
player_b: Address
Initialize game
commit_boardplayer: Address
commitment: BytesN<32>
merkle_root: BytesN<32>
Commit board layout
attackattacker: Address
x: u32, y: u32
Attack coordinates (0-9)
reveal_celldefender: Address
x: u32, y: u32
value: u32
proof: OnChainMerkleProof
Reveal cell with Merkle proof
get_state-Get current game state

Data Structures

Phase

#![allow(unused)]
fn main() {
enum Phase {
    Setup,    // Waiting for board commitments
    Attack,   // Game in progress
    Finished, // Winner declared
}
}

CellResult

#![allow(unused)]
fn main() {
enum CellResult {
    Unknown, // Not yet attacked
    Miss,    // Attacked, no ship
    Hit,     // Attacked, ship present
}
}

BoardCommitment

#![allow(unused)]
fn main() {
struct BoardCommitment {
    commitment: BytesN<32>,  // SHA256(board || salt)
    merkle_root: BytesN<32>, // Root of Merkle tree
}
}

Stable Merkle Verification

The contract uses Cougr's stable SHA256 Merkle proof contract:

#![allow(unused)]
fn main() {
use cougr_core::privacy::stable::{MerkleProofVerifier, Sha256MerkleProofVerifier};

let verifier = Sha256MerkleProofVerifier;
assert!(verifier.verify(&env, &proof, &merkle_root)?);
}

The leaf payload still binds index || value, but the inclusion proof format and verification rules come from Cougr's stable privacy API.

When to Use Commit-Reveal vs ZK Circuits

PatternBest ForCostComplexity
Commit-Reveal + MerkleHidden boards, card hands, fog-of-warO(log n) proof verificationLow - uses standard SHA256
ZK Circuits (Groth16/Poseidon)Private game logic evaluation, hidden card dealsSingle on-chain verificationHigh - requires circuit compilation

Use commit-reveal when you need to hide state but reveal it incrementally with verifiable proofs. Use ZK circuits when the game logic itself must remain private (e.g., proving a move is valid without revealing the move).

For a reference ZK implementation, see hidden_hand, which demonstrates circuit-based hidden card dealing with Groth16 proofs.

Storage Model

Committed State

Stored on-chain during setup:

  • commitment_a/b - SHA256 hash of each player's board with random salt
  • merkle_root_a/b - root of the Merkle tree built from cell hashes
  • has_commitment_a/b - flags tracking which players have committed

Both commitments must be submitted before the game transitions to Phase::Attack.

Revealed State

Updated during the attack phase:

  • attack_grid_a/b - maps cell indices to CellResult (hit/miss)
  • ship_status - tracks remaining ship cells per player (starts at 17)
  • turn_state - tracks current player, phase, and pending reveals

Proven State

The reveal_cell function verifies that disclosed cell values match the original commitment:

  1. Constructs the expected leaf hash from (index, value) using the same SHA256 scheme as the commit phase
  2. Validates the OnChainMerkleProof against the stored merkle_root using Sha256MerkleProofVerifier
  3. Only if the proof verifies is the hit/miss recorded on-chain

Component Serialization

BoardCommitment uses the impl_component! macro for standardized serialization:

#![allow(unused)]
fn main() {
impl_component!(BoardCommitment, "board", Table, {
    commitment: bytes32,
    merkle_root: bytes32
});
}

This replaces manual byte-level serialization with a type-safe macro that handles big-endian encoding/decoding automatically.

Building & Testing

Prerequisites

  • Rust 1.88.0+
  • Stellar CLI 25.0.0+ (optional)

Build

cargo build
cargo build --release --target wasm32v1-none

Test

cargo test

Test Coverage (12 tests):

Recommended Testing Approach: For verification of complex setup commitment sequences and attack/reveal turn-based interactions, utilize GameHarness and Scenario (see sandbox_tests.rs). This ensures cryptographic commitments and Merkle proofs verify correctly across turns.

Test Coverage (10 tests):

  • โœ… Game initialization
  • โœ… Board commitment
  • โœ… Attack and reveal (miss)
  • โœ… Attack and reveal (hit)
  • โœ… Invalid proof rejection
  • โœ… Cannot attack same cell twice
  • โœ… Turn enforcement
  • โœ… Win condition
  • โœ… Component trait serialization (via impl_component!)
  • โœ… Turn switching
  • โœ… Reveal without pending attack
  • โœ… Attack before commit phase

Example Usage

Off-Chain (Player)

#![allow(unused)]
fn main() {
// Build Merkle tree from hashed cell payloads
let mut leaves = Vec::new();
for (idx, &value) in board.iter().enumerate() {
    let leaf = sha256(idx || value);
    leaves.push(leaf);
}
let tree = MerkleTree::from_leaves(&env, &leaves)?;
let root = tree.root();

// Get proof for specific cell
let proof = to_on_chain_proof(&tree.proof(x * 10 + y)?, &env);

// Submit on-chain
client.reveal_cell(&player, &x, &y, &value, &proof);
}

On-Chain (Contract)

#![allow(unused)]
fn main() {
let verifier = Sha256MerkleProofVerifier;
assert!(verifier.verify(&env, &proof, &stored_merkle_root)?);
}

Security Considerations

Secure

  • Commitment binding: SHA256 prevents changing board
  • Selective reveal: Merkle proofs reveal only attacked cells
  • Proof verification: Invalid proofs rejected
  • Turn enforcement: Players alternate attacks

๏ธ Important

  • Salt randomness: Use 32 cryptographically random bytes
  • Merkle tree depth: 7 levels for 100 cells (padded to 128)
  • Proof ordering: Siblings must be in correct order

Best Practices

#![allow(unused)]
fn main() {
// โœ… Good: Random salt
let salt = generate_random_bytes(32);

// โŒ Bad: Predictable salt
let salt = BytesN::from_array(&env, &[0u8; 32]);

// โœ… Good: Verify proof before revealing
if !verify_proof(root, index, value, proof) {
    panic!("Invalid proof");
}
}

ECS Architecture

Components

ComponentFieldsPurpose
BoardCommitmentcommitment: BytesN<32>
merkle_root: BytesN<32>
Cryptographic board commitment
AttackGridcells: Map<u32, CellResult>Public record of attacks
ShipStatusremaining_a: u32
remaining_b: u32
Ship cell counts
TurnStatecurrent_player: Address
phase: Phase
has_pending: bool
Game state management

Systems

SystemResponsibility
CommitSystemValidates and stores commitments
AttackSystemRecords attack coordinates
RevealSystemVerifies stable OnChainMerkleProof, updates grid
WinConditionSystemDetects when all ships sunk

Why Merkle Proofs?

Merkle trees enable selective disclosure:

ApproachReveal CostPrivacy
Full board on-chainO(1)โŒ None
Reveal entire board per attackO(n)โŒ None
Merkle proofO(log n)โœ… Only attacked cells

For a 10x10 board:

  • Full reveal: 100 cells
  • Merkle proof: ~7 hashes (logโ‚‚ 128)

Deployment

# Deploy to testnet
stellar keys generate battleship-deployer --network <NETWORK> --fund
stellar contract deploy \
  --wasm target/wasm32v1-none/release/battleship.wasm \
  --source battleship-deployer \
  --network <NETWORK>

Resources

License

MIT OR Apache-2.0

blind_auction

Other Canonical
soroban-gamezk-circuitsprivacy

Canonical ZK circuit example demonstrating circuits::sealed_bid for sealed-bid reveals.

Purpose and pattern

This example demonstrates how to run a blind auction on-chain. Bidders commit their encrypted bids, and during the reveal phase, submit a ZK proof to verify their bid value matches the original commitment and is below the maximum allowed bid limits, without revealing bids early or leaking bid range details.

Public contract API

FunctionParametersReturnsDescription
init_auctionmax_bid: u32, auction_id: BytesN<32>AuctionConfigStarts a new auction config with maximum bid limits and an auction identifier.
reveal_bidbidder: Address, bid_commitment: BytesN<32>, revealed_bid: u32, proof: Groth16ProofboolVerifies a bidder's reveal proof, storing the bid record if valid.
bid_revealbidder: AddressBidRevealRetrieves the revealed bid details for a bidder.

Architecture overview

                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                         โ”‚    Bidder     โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Submits Bid & ZK Proof
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚    BlindAuction     โ”‚
                     โ”‚ (Soroban Contract)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Loads Spec
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ circuits::          โ”‚
                     โ”‚  sealed_bid         โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The bidder submits their bid value and proof that verifies their bid corresponds to the committed hash. The contract runs the sealed_bid verifier logic to register the bid.

Storage model

Auction configuration records and active bid listings are stored in Instance Storage on-chain.

Main gameplay flow

  1. Setup: Call init_auction to setup maximum bid constraints and the auction ID.
  2. Commit Phase: Bidders record hash commitments of their bids (handled off-chain or via standard storage).
  3. Reveal Phase: Bidders call reveal_bid with their bids and ZK proofs to unlock and register their bid weights.

Cougr APIs used

  • circuits::sealed_bid: Handles verify calculations for sealed on-chain bidding logic.
  • zk::Groth16Proof: Contains the proof struct representation.

Use GameHarness and standard test_fixtures to execute happy-path reveals and invalid proof rejection flows.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Simple single-item auction model.
  • Winner computation logic is not included (focuses on verification of bid validity).

Bomberman 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.

This example demonstrates how to implement a Bomberman game as a smart contract on the Stellar blockchain using Soroban and cougr-core.

Overview

This contract implements a simplified version of the classic Bomberman game where players can:

  • Initialize a game with a grid
  • Spawn and move players around the grid
  • Place bombs that have individually configurable blast power
  • Trigger chain reactions - a bomb overlapped by an explosion detonates instantly in the same tick
  • Pick up power-ups that improve stats (blast radius, bomb capacity, movement speed)
  • Handle collisions and scoring
  • Check for game-over conditions

Setup

Prerequisites

  • Rust (latest stable version)
  • Cargo
  • Stellar CLI (install with cargo install --locked stellar-cli)

Installation

  1. Install Rust from https://rustup.rs/
  2. Install Stellar CLI:
    cargo install --locked stellar-cli
    

Building the Contract

# Build the Rust code
cargo build

# Build the WASM file
stellar contract build

Testing and Validation

# Formatting check
cargo fmt --check

# Lint check
cargo clippy --all-targets --all-features -- -D warnings

# Run unit tests
cargo test

# Build WASM (target wasm32v1-none)
stellar contract build

Game Logic

ECS Components

ComponentFieldsNotes
PlayerComponentid, x, y, lives, bomb_capacity, score, bomb_power, speedAll player state
BombComponentx, y, timer, power, owner_idPower set from bomb_power at placement time
ExplosionComponentx, y, timerDespawned after EXPLOSION_DURATION ticks
GridComponentcells: Vec<CellType>Walls, destructible blocks
GameStateComponentcurrent_tick, game_over, winner_idGlobal match state
PowerUpComponentx, y, power_up_typeCapacity | Power | Speed

Power-ups

Three power-up types are supported and modelled as first-class ECS entities:

TypeEffect
Capacitybomb_capacity += 1 - player may place one more active bomb
Powerbomb_power += 1 - new bombs have a larger blast radius
Speedspeed += 1 - reserved for future movement throttling

Power-ups are spawned at game start (deterministic grid positions) and when a bomb destroys a destructible block (~25% chance via (x+y)%4==0). Walking over a PowerUpComponent entity silently applies the buff and removes the entity from the world (PickupSystem).

Chain Reactions

The ChainReactionSystem runs inside update_tick:

  1. Bombs whose timer hits 0 are placed in a detonation_queue.
  2. Each bomb is detonated in order - explosions are spawned immediately.
  3. After each detonation, every remaining live bomb is checked for overlap with a fresh explosion cell. A hit bomb is removed and pushed to the back of the queue, so it detonates in the same tick - forming a cascade.
  4. The process repeats until the queue is empty.

Systems at a Glance

SystemWhereWhat it does
BombTimerSystemupdate_tickDecrements bomb timers
ChainReactionSystemupdate_tickCascades detonations within one tick
ExplosionPropagationSystemdetonate_bombSpawns explosion entities in 4 directions
PowerUpSpawnSysteminit_game + detonate_bombCreates PowerUpComponent entities
PickupSystemmove_playerApplies buff when player steps on a power-up

Contract Functions

  • init_game(): Initializes the game state
  • move_player(player_id, direction): Moves a player in the specified direction
  • place_bomb(player_id): Places a bomb at the player's current position
  • update_tick(): Advances timers, triggers explosions, handles collisions
  • get_score(player_id): Returns the current score for a player
  • check_game_over(): Checks if the game has ended

Integration with Cougr-Core

This example demonstrates how cougr-core simplifies on-chain game development by providing:

  • Persistent storage management for game state
  • Transaction validation
  • Generic game logic utilities
  • Efficient data structures for complex game states

Deployment

Prerequisites for Deployment

  1. Test Account: Fund a test account using the Friendbot: https://faucet-stellar.acachete.xyz
  2. Stellar CLI: Install with cargo install --locked stellar-cli
  3. WASM Build: Ensure the contract builds successfully

To Testnet

  1. Build the WASM file:

    stellar contract build
    
  2. Deploy the contract:

    stellar contract deploy \
      --wasm target/wasm32v1-none/release/bomberman.wasm \
      --source <your-secret-key> \
      --network <NETWORK>
    
  3. the contract ID for future invocations (e.g., <CONTRACT_ID>)

https://stellar.expert/explorer/testnet/account/GAQAXKUQYNBHZYZ2OYISPXDZDP2HV57534VMGARGGIICH2BGNKDTKXOX

Testing the Contract

Invoke functions to test gameplay:

# Initialize game
stellar contract invoke \
  --id <contract-id> \
  --source <your-secret-key> \
  --network <NETWORK> \
  -- \
  init_game

# Move player (directions: 0=up, 1=right, 2=down, 3=left)
stellar contract invoke \
  --id <contract-id> \
  --source <your-secret-key> \
  --network <NETWORK> \
  -- \
  move_player \
  --player_id 1 \
  --direction 1

# Place bomb
stellar contract invoke \
  --id <contract-id> \
  --source <your-secret-key> \
  --network <NETWORK> \
  -- \
  place_bomb \
  --player_id 1

# Update game tick (advances timers, processes explosions)
stellar contract invoke \
  --id <contract-id> \
  --source <your-secret-key> \
  --network <NETWORK> \
  -- \
  update_tick

# Get player score
stellar contract invoke \
  --id <contract-id> \
  --source <your-secret-key> \
  --network <NETWORK> \
  -- \
  get_score \
  --player_id 1

# Check game status
stellar contract invoke \
  --id <contract-id> \
  --source <your-secret-key> \
  --network <NETWORK> \
  -- \
  check_game_over

Example Game Sequence

  1. Initialize: Call init_game to set up the world
  2. Spawn Players: In a full implementation, add player spawning functions
  3. Gameplay Loop:
    • Move players with move_player
    • Place bombs with place_bomb
    • Advance game state with update_tick
    • Check scores and game status periodically
  4. Game Over: Monitor check_game_over for completion

Cost Monitoring

Monitor transaction costs during testing:

  • Rent: Storage costs on Stellar ledger
  • Fees: Network transaction fees
  • CPU/RAM: Contract execution costs

Use --simulate flag to estimate costs before actual deployment:

stellar contract invoke --simulate [other flags]

Troubleshooting

Common Issues

  1. Rust version conflicts: Update Rust with rustup update
  2. Stellar CLI not found: Ensure it's in your PATH after installation
  3. Compilation errors: Check that all dependencies are correctly specified in Cargo.toml

Debug Commands

# Verbose build
cargo build --verbose

# Check Stellar CLI version
stellar --version

# Simulate contract deployment
stellar contract deploy --wasm target/wasm32v1-none/release/bomberman.wasm --simulate

Architecture

The contract uses the cougr-core ECS (Entityโ€“Componentโ€“System) pattern for all game state:

  • Entities: Numeric IDs created by world.spawn_entity() / removed by world.despawn_entity()
  • Components: Plain structs implementing ComponentTrait (typed serialize / deserialize)
  • Systems: Free functions / inline blocks inside contract entry-points that compose over component queries

All state is persisted in a single SimpleWorld under DataKey::World. The separation of components and systems makes it straightforward to add new mechanics without touching unrelated code.

Checkers

Board Transitional
ecs
checkers preview

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

A fully on-chain two-player Checkers game implemented as a Soroban smart contract.

Purpose and pattern

This example demonstrates a turn-based board game with non-trivial rule enforcement - diagonal grid movement, mandatory captures, multi-hop chain captures, king promotion, and win detection - implemented as plain Rust functions over #[contracttype] structs. It showcases the simplest possible Cougr-adjacent pattern: component-shaped data types with hand-written contract storage access, predating the ComponentTrait/GameApp conventions used by newer examples. It is a useful reference for understanding what a Soroban board-game contract looks like with no ECS framework involvement at all.

Public contract API

FunctionParametersReturnsDescription
init_gameplayer_one: Address, player_two: AddressResult<(), CheckersError>Seeds the standard 8ร—8 opening position; Player One moves first. Returns AlreadyInitialised if called more than once.
submit_moveplayer: Address, from_row: u32, from_col: u32, to_row: u32, to_col: u32Result<(), CheckersError>Validates and applies a step or capture, resolves chain captures, promotes kings, and advances or holds the turn. Returns one of the CheckersError variants on any rule violation.
get_state-Result<GameState, CheckersError>Full snapshot: board, turn, status, and both player addresses.
get_board-Result<BoardState, CheckersError>Raw 64-cell board array, row-major order.
get_current_player-Result<Address, CheckersError>The Address of the player whose turn it currently is.

Error codes

ErrorCodeMeaning
AlreadyInitialised1init_game called more than once
NotInitialised2Any call before init_game
NotAPlayer3Caller is not player_one or player_two
WrongTurn4Caller is a registered player but it is not their turn
NotYourPiece5Source square is empty or owned by the opponent
DestinationOccupied6Target square is already occupied
IllegalMove7Move is not a legal diagonal step or jump
MustCapture8A capture is available but a non-capture move was attempted
GameOver9The game has already ended
OutOfBounds10Row or column index โ‰ฅ 8
ChainCapturePieceMismatch11During a chain capture the origin square was not the chain square
NotDarkSquare12Destination square is a light square (row + col is even)

Architecture overview

There is no GameApp tick loop. submit_move runs a fixed validation/update pipeline synchronously on each call:

submit_move
  โ””โ”€ identify caller, check turn order, bounds, and dark-square rule
  โ””โ”€ MoveValidationSystem  (systems.rs: legal_steps / legal_captures)
       โ†’ confirms the requested move is a legal step or capture for this piece
  โ””โ”€ forced-capture check   (systems.rs: any_capture_available)
       โ†’ rejects a non-capture move if any capture exists for the player
  โ””โ”€ apply move + remove captured piece
  โ””โ”€ PromotionSystem        (systems.rs: maybe_promote)
       โ†’ crowns a man reaching the opponent's back rank
  โ””โ”€ chain-capture detection (systems.rs: legal_captures on the landing square)
       โ†’ holds the turn and records the active square if another capture is available
  โ””โ”€ EndConditionSystem     (systems.rs: check_winner)
       โ†’ declares a winner on piece-count exhaustion or no legal moves
  โ””โ”€ TurnSystem
       โ†’ advances current_player, or holds it during a chain capture

components.rs holds the data shapes (BoardComponent, TurnComponent, GameStatusComponent, GameStatus, the public GameState/BoardState view types, the internal ChainCapture tracker, and the small SmallVec4 fixed-capacity collection). systems.rs holds the pure, storage-free game-logic functions (move/capture legality, promotion, win detection, board initialisation). lib.rs is the only module that touches env.storage(); it loads components, calls into systems.rs, and writes the results back.

Storage model

All state lives in persistent storage, under five top-level Symbol keys:

KeyContents
BOARDBoardComponent - 64-cell flat grid
TURNTurnComponent - current player and move number
STATUSGameStatusComponent - active/finished and winner
P1Address of Player One
P2Address of Player Two
CHAINChainCapture - present only while a multi-hop capture sequence is in progress

Persistent storage (rather than instance storage) is used because each key is read and rewritten independently inside submit_move - the board, turn, and status are not bundled into one aggregate struct in this example, so per-key persistent entries map directly onto that access pattern. The CHAIN key is removed as soon as a turn fully ends; its absence is what tells submit_move that no multi-hop sequence is active, so it doubles as both data and a lightweight state flag.

Main gameplay flow

  1. Deployer calls init_game(player_one, player_two). The board is seeded with the standard 12-piece-per-side opening position; Player One moves first.
  2. Player One calls submit_move with a (from_row, from_col) โ†’ (to_row, to_col) pair. The contract checks turn order, bounds, the dark-square rule, piece ownership, and move geometry (diagonal step or jump).
  3. If any capture is available for the mover anywhere on the board, a non-capture move is rejected with MustCapture - captures are mandatory, not optional.
  4. On a legal capture, the jumped piece is removed. If the landing square has a further capture available, the turn is held: the same player must continue jumping with the same piece (tracked via the CHAIN key) until no further capture exists.
  5. A man reaching the opponent's back rank is promoted to a king immediately after the move that lands it there, before chain-capture continuation is checked.
  6. After each move, EndConditionSystem checks whether either side has no pieces left or no legal move available; if so, STATUS flips to Finished with a recorded winner.
  7. Players alternate submit_move calls until a winner is recorded; get_state, get_board, and get_current_player provide read-only views at any point.

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives each component (BoardComponent, TurnComponent, GameStatusComponent) a component_type() symbol and byte-level serialize/deserialize, chosen here because the game has a single fixed set of components per instance rather than a dynamic entity population that would benefit from SimpleWorld/SimpleQueryBuilder scanning.

Per the API usage guidance in EXAMPLE_STANDARD.md ยง8, GameApp/ScheduleStage, SimpleWorld, auth, and privacy APIs do not apply here: there is exactly one fixed-shape game state per contract instance, one synchronous validation pipeline per call, no session-key or multi-device flow, and no hidden information or proof submission. This example is kept as a transitional reference precisely because it predates Cougr's GameApp tick conventions - see snake for the current recommended approach.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp, ScheduleStage, or SimpleWorld - game logic is invoked directly from contract entrypoints rather than through a tick-based scheduler, since a two-player, one-decision-per-call board game does not require staged scheduling or entity queries.
  • No timeout/forfeit mechanism for an unresponsive player.
  • No draw detection (e.g., repetition or no-progress rules); a game can only end by piece exhaustion or a player having no legal move.
  • No spectator or replay API beyond the three read-only getters.

Verifiable Chess with ZK Move Validation

Board Transitional
authzk-circuitsprivacyecs

Transitional example: This example uses an older Cougr pattern and is preserved for compatibility reference. For the current recommended approach to ZK-backed game logic specifically, see hidden_hand, which uses cougr_core::circuits' pre-built circuit builders instead of constructing a CustomCircuit by hand.

A simplified chess implementation on Stellar Soroban that demonstrates zero-knowledge proof verification for move legality using the Cougr ZK framework. Move legality is checked off-chain by a circuit; the contract only verifies a compact Groth16 proof and applies the move.

Purpose and pattern

This example demonstrates how to keep move-validation logic entirely off-chain while still enforcing it trustlessly: instead of the contract checking piece-movement rules, path obstruction, and check/checkmate conditions on every move, the player generates a Groth16 proof off-chain that the move is legal given the current board state, and the contract verifies that proof in constant time. It showcases cougr_core::privacy::experimental's generic CustomCircuit builder for binding a proof's public inputs (state_hash, from, to) to a verification key, alongside the same ComponentTrait pattern used by the other ECS-lite examples for typed, byte-serializable game state.

Public contract API

FunctionParametersReturnsDescription
new_gamewhite: Address, black: Address-Initializes the standard 8x8 opening position, computes the initial state_hash, and sets White to move first.
submit_moveplayer: Address, from: u32, to: u32, proof: BytesMoveResultRequires player.require_auth(). Checks turn order and game status, verifies the supplied proof against the stored verification key and current state_hash, then applies the move, recomputes the hash, and switches turns.
resignplayer: Address-Marks the game Resigned if called by either registered player.
get_board-BoardStateCurrent piece map and state_hash.
get_state-GameStateFull game state: both players, board, turn state, and last proof record.
set_vkvk: VerificationKey-Admin function to set/replace the Groth16 verification key used for move proofs.

Architecture overview

There is no GameApp tick loop. submit_move runs a fixed pipeline synchronously on each invocation:

submit_move
  โ””โ”€ TurnSystem              โ†’ rejects calls from the wrong player or a finished game
  โ””โ”€ ProofVerificationSystem โ†’ verifies the Groth16 proof against state_hash/from/to via CustomCircuit
  โ””โ”€ BoardUpdateSystem       โ†’ applies the move to the piece map, recomputes state_hash
  โ””โ”€ TurnSystem              โ†’ increments move_count, switches current player
  โ””โ”€ EndGameSystem           โ†’ marks Checkmate if either king is no longer on the board

components.rs defines the data (Piece, BoardState, TurnState, ProofRecord, GameState, MoveResult) and the ComponentTrait implementations for BoardState and TurnState. systems.rs holds the pure, storage-free game logic (init_board, compute_state_hash, apply_move, check_endgame). zk.rs holds the proof-verification wrapper (build_move_circuit, decode_proof) built on cougr_core::privacy::experimental. lib.rs owns the single GameState aggregate, is the only module that touches contract storage, and wires the systems and the ZK verification step together inside submit_move.

Storage model

The entire game lives under one instance-storage key (GAME_KEY, symbol_short!("GAME")) as a single GameState struct bundling both player addresses, the board, turn state, and the last proof record. The verification key is stored separately under its own instance-storage key (VK_KEY, symbol_short!("VK")) so it can be rotated with set_vk independently of game state. Instance storage is used for both - not persistent or temporary - because there is exactly one game and one verification key per contract instance, with no per-entry TTL management needed.

Main gameplay flow

  1. Deployer calls set_vk(vk) once to register the Groth16 verification key matching the off-chain move-validation circuit.
  2. Deployer (or either player) calls new_game(white, black); the board is seeded with the standard opening position, White to move, and the initial state_hash is computed.
  3. Off-chain, White picks a move (from, to), runs the move-validation circuit against the current state_hash, and produces a Groth16 proof that the move is legal.
  4. White calls submit_move(white, from, to, proof). The contract checks turn order, verifies the proof via CustomCircuit, and on success applies the move, recomputes state_hash, records the proof, and switches the turn to Black.
  5. Players alternate steps 3-4. If a king is ever captured, EndGameSystem sets the status to Checkmate; either player may also call resign at any time to end the game early.
  6. Any caller reads get_board or get_state to render the current position or check status.

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives BoardState and TurnState a component_type() symbol and byte-level serialize/deserialize, chosen because chess has a single fixed-shape game state per contract instance rather than a dynamic entity population that would benefit from SimpleWorld/SimpleQueryBuilder scanning.
  • cougr_core::privacy::experimental (CustomCircuit) - chosen specifically because this example demonstrates Groth16 proof submission: the contract never evaluates chess rules itself, it only binds a proof's public inputs (state_hash, from, to) to a verification key and checks the proof verifies. This is the canonical use case the privacy::experimental guidance in the Cougr API table calls out (Groth16 proof submission / pairing-based verification), as opposed to privacy::stable, which is for commit-reveal and Merkle-proof patterns this example does not use. Newer examples (hidden_hand, fog_explorer, dice_duel, blind_auction) use the higher-level cougr_core::circuits pre-built builders instead of hand-rolling a CustomCircuit; this example predates that module and is kept as a reference for the lower-level builder API.
  • cougr_core::privacy::{Groth16Proof, VerificationKey} - the concrete proof and key types the CustomCircuit builder and verifier operate on; stored on-chain (VerificationKey) or passed per-call (Groth16Proof, decoded from the proof: Bytes argument) so the contract has everything it needs to verify a move without re-deriving any chess logic.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Proof decoding (zk::decode_proof) is not implemented - it intentionally panics, since wiring up a real off-chain Groth16 prover/serializer is out of scope for this example. Tests exercise the turn/resign/game-over paths that return before proof verification is reached.
  • The move-validation circuit itself (piece movement rules, path obstruction, check/checkmate detection) lives off-chain and is not part of this repository; only the on-chain verification wrapper is shown.
  • Checkmate detection is simplified to "a king is missing from the board" rather than full check/checkmate/stalemate rules. Castling, en passant, and pawn promotion are not modeled.
  • Does not use GameApp, ScheduleStage, or SimpleWorld - submit_move runs its pipeline directly from the contract entrypoint since each call has exactly one decision point and does not need staged scheduling.

Connect Four

Board Transitional
ecs

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

An on-chain Connect Four game built with the Cougr ECS framework on Stellar Soroban.

Purpose and pattern

This example demonstrates a turn-based, perfect-information board game driven by gravity-based piece placement: a move only specifies a column, and the contract resolves which row the piece lands in before checking for a win. It showcases Cougr's ComponentTrait pattern for typed, byte-serializable game state - components are plain Rust structs annotated with #[contracttype] that implement ComponentTrait directly, rather than being scanned through SimpleWorld/SimpleQueryBuilder. This is a lighter-weight integration suited to a single fixed-shape game state (one board, two players) with no dynamic entity population.

Public contract API

FunctionParametersReturnsDescription
init_gameplayer_one: Address, player_two: AddressGameStateInitializes an empty 7ร—6 board; player_one moves first.
drop_pieceplayer: Address, column: u32DropResultValidates the move, drops a piece into the lowest empty row of column (0โ€“6), checks for a win/draw, and advances the turn.
get_state-GameStateCurrent board, turn, move count, and status.
get_board-Vec<u32>Raw 42-cell board array (row-major, 0=empty/1=player one/2=player two).
is_valid_columncolumn: u32boolWhether column is in bounds, not full, and the game is still in progress.
is_finished-boolWhether the game has ended (win or draw).
get_winner-Option<Address>The winning player's address, or None if the game is a draw or still in progress.
reset_game-GameStateRe-initializes the board with the same two players.

Architecture overview

There is no GameApp tick loop - drop_piece runs all systems synchronously in a fixed pipeline on each invocation:

drop_piece
  โ””โ”€ validation_system     โ†’ rejects out-of-turn, out-of-bounds, full-column, or post-game moves
  โ””โ”€ gravity_system        โ†’ finds the lowest empty row in the target column
  โ””โ”€ execution_system      โ†’ places the piece and increments the move counter
  โ””โ”€ win_detection_system  โ†’ scans the board for horizontal/vertical/diagonal 4-in-a-row
  โ””โ”€ draw_system           โ†’ marks the game a draw if the board fills with no winner
  โ””โ”€ turn_system           โ†’ advances to the other player if the game is still active

Each system is a pure function over components.rs types (BoardComponent, PlayerComponent, GameStateComponent, bundled into ECSWorldState); lib.rs owns that aggregate and is the only module that touches contract storage.

Storage model

The entire game lives under one instance-storage key (WORLD_KEY, symbol_short!("WORLD")) as a single ECSWorldState struct bundling the board, both player addresses, and the game-state component (turn, move count, status, last move). Instance storage is used - not persistent or temporary - because there is exactly one game per contract instance and the state must survive for the lifetime of that instance with no per-entry TTL management.

Main gameplay flow

  1. Deployer calls init_game(player_one, player_two); the board is seeded empty, player_one to move, status in-progress.
  2. player_one calls drop_piece(player, column). The contract checks turn order, column bounds, and that the column isn't full.
  3. On a legal move: gravity_system finds the lowest empty row in that column, execution_system places the piece there and increments the move count, win_detection_system scans the whole board for a 4-in-a-row, and draw_system marks the game a draw if all 42 cells are filled with no winner.
  4. If the game is still in progress, turn_system switches to the other player; the updated ECSWorldState is written back to instance storage.
  5. Players alternate drop_piece calls until is_finished returns true. Either player reads get_winner to see who won (or None for a draw).
  6. reset_game can be called at any point to start a fresh game with the same two players.

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives each component (BoardComponent, GameStateComponent) a component_type() symbol and explicit byte-level serialize/deserialize. This was chosen because the game has one fixed-shape ECSWorldState per contract instance (a single board, two fixed player slots, one game- state record) rather than a dynamic population of entities that would benefit from SimpleWorld/SimpleQueryBuilder scanning - per the guidance in EXAMPLE_STANDARD.md ยง8, SimpleWorld and SimpleQueryBuilder are for examples that store and query entities by component type, which doesn't apply here. PlayerComponent is also #[contracttype] but does not implement ComponentTrait, since it is embedded directly in ECSWorldState rather than serialized standalone.
  • No GameApp, ScheduleStage, auth, privacy::stable/experimental, or ops standards are used - drop_piece has exactly one decision point per call, so there is no multi-stage tick to schedule, no hidden information to commit-reveal, and no pausability/ownership requirement for a two-player, always-on game.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp, ScheduleStage, or SimpleWorld - game logic is invoked directly from contract entrypoints rather than through a tick-based scheduler, since a turn-based game with one decision point per call does not need staged scheduling.
  • No timeout/forfeit mechanism for an unresponsive player.
  • win_detection_system re-scans the entire board on every move rather than checking only the cell that was just placed; this is simple and correct but not the most efficient approach for a larger board.
  • No spectator or replay API beyond the read-only getters.

Cross-Asset Racing League (Stellar + Soroban)

Other Transitional
authzk-circuitsprivacyecs

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

Cross-Asset Racing League is a multi-asset payment-driven racing game example for Soroban that combines:

  • payment-gated boost mechanics enabled by cross-asset payment flows
  • proof-backed race-state validation through stellar-zk
  • league standings and progression tracking
  • deterministic race result verification

This example is intentionally contract-only: no frontend, no real-time physics simulation, no multiplayer middleware.

Why This Is Stellar-Specific

This example demonstrates three Stellar-native patterns working together:

  1. Soroban contract state for deterministic race progression and league management.
  2. stellar-zk style Groth16 verification on-chain using Soroban BN254 pairing checks for anti-cheat and race integrity.
  3. x402-style payment credits represented as settled payment units tied to multi-asset payment flows.

References:

  • https://crates.io/crates/stellar-zk
  • https://github.com/salazarsebas/stellar-zk
  • https://developers.stellar.org/docs/build/apps/x402

Gameplay Flow

1. League Initialization

The owner initializes a racing league, establishing:

  • Season ID and tracking metadata
  • Initial contract state for race creation and standings

2. Race Creation

The owner creates a race instance with:

  • Unique race ID
  • Duration (in ledger blocks)
  • Registration phase for player entry
  • Support for up to 10 racers per race

3. Player Entry

Players join a race during the registration phase:

  • Entrant cap enforced (max 10 racers)
  • Vehicle state initialized for each player
  • Replay protection via tracking entered players

4. Race Activation

Owner starts the race, transitioning it to the active phase:

  • Entrants are locked in
  • Payment-gated boosts can now be activated
  • Race-state proofs can be submitted

5. Payment-Gated Boost Activation

Players consume pre-settled payment credits to activate boosts:

  • Standard Boost (cost: 10 credits) โ†’ +10 speed
  • Premium Boost (cost: 50 credits) โ†’ +30 speed
  • Legendary Boost (cost: 200 credits) โ†’ +60 speed

Payment flow:

  1. Off-chain settlement process (e.g., via x402 gateway) receives cross-asset payment
  2. Owner calls credit_payment(player, amount, receipt_hash) with proof of settlement
  3. Replay protection via receipt hash prevents double-crediting
  4. Player activates boost, consuming credits in-contract

6. Proof-Backed Race Validation

Players submit Groth16 proofs to validate race integrity:

  • Proof verifies correct race execution state
  • Public inputs bound to race commitments
  • Nullifier-based replay protection
  • BN254 pairing checks ensure anti-cheat properties

7. Race Completion and Standings Update

Owner completes the race:

  • Race transitions to completed phase
  • Points awarded by position: 1st=10, 2nd=6, 3rd=3, rest=1
  • League standings updated for each racer

Architecture

The example follows a modular architecture, separating data structures, ECS components, business systems, and contract entrypoints.

Module Layout

  • lib.rs: Contract entrypoints and orchestration.
  • types.rs: Shared domain models, enums, and storage keys.
  • components.rs: Cougr ECS component definitions.
  • systems.rs: Core gameplay transitions and logic.
  • helpers.rs: Validation and verification utilities.

Components

ComponentFieldsPurpose
LeagueComponentseason_id, current_race_id, league_activeTracks league state
RaceComponentrace_id, season_id, entrants_count, phase, durationTracks race instance
VehicleStateComponentspeed, boost_state_type, boost_active, penalty_countTracks racer state
PaymentActionComponentplayer_creditsTracks payment-gated access
ProofStateComponentcommitment, verification_statusTracks proof validation

Systems

  • RaceSystem: Handles create_race, enter_race, start_race, complete_race, and submit_race_proof.
  • PaymentSystem: Handles credit_payment logic.

stellar-zk Integration

Proof-Backed Race Integrity

This contract uses stellar-zk for a real validation path:

  1. Race Commitment: Off-chain race state committed via Merkle root
  2. Proof Generation: Player proves valid race state against commitment
  3. On-Chain Verification: BN254 pairing checks verify the Groth16 proof
  4. Nullifier Protection: Prevents replay attacks using commitment hashing

Proof Structure

#![allow(unused)]
fn main() {
pub struct ProofInput {
    pub proof: BytesN<256>,           // Groth16 proof (compressed)
    pub public_inputs: Bytes,          // Public inputs bound to race state
    pub commitment: BytesN<32>,        // Race state commitment root
    pub race_id: u32,                  // Associated race
    pub player_id: u32,                // Associated player
}
}

Verification Pattern

1. Extract public inputs from proof
2. Verify proof structure and length
3. Hash commitment for nullifier uniqueness
4. Check nullifier not previously used (replay protection)
5. Perform BN254 pairing checks (Groth16 verifier)
6. Mark nullifier as consumed

Contract API

Initialization

#![allow(unused)]
fn main() {
fn init_league(env: Env, owner: Address)
}

Initializes the racing league. Requires owner authorization.

Race Management

#![allow(unused)]
fn main() {
fn create_race(env: Env, owner: Address, duration: u32) -> u32
}

Creates a new race with specified duration. Returns race ID. Owner-only.

#![allow(unused)]
fn main() {
fn start_race(env: Env, owner: Address, race_id: u32)
}

Transitions race from registration to active phase. Owner-only.

#![allow(unused)]
fn main() {
fn complete_race(env: Env, owner: Address, race_id: u32)
}

Completes the race, updates standings, and awards points. Owner-only.

Player Actions

#![allow(unused)]
fn main() {
fn enter_race(env: Env, player: Address, race_id: u32)
}

Player enters a race during registration phase. Requires player authorization.

#![allow(unused)]
fn main() {
fn activate_boost(env: Env, player: Address, race_id: u32, boost_type: u32)
}

Player activates a boost by spending payment credits. Boost types: 1=Standard, 2=Premium, 3=Legendary.

Payment Integration

#![allow(unused)]
fn main() {
fn credit_payment(env: Env, owner: Address, player: Address, amount: u32, receipt_hash: BytesN<32>)
}

Owner credits player with payment settlement. Receipt hash prevents double-crediting. Owner-only.

#![allow(unused)]
fn main() {
fn get_player_credits(env: Env, player: Address) -> u32
}

Query player's available payment credits.

Proof Validation

#![allow(unused)]
fn main() {
fn submit_race_proof(env: Env, player: Address, proof: ProofInput) -> bool
}

Player submits proof for race-state validation. Returns true if proof is valid. Implements replay protection via nullifier.

Query Functions

#![allow(unused)]
fn main() {
fn get_game_state(env: Env) -> GameState
}

Returns current league state.

#![allow(unused)]
fn main() {
fn get_race(env: Env, race_id: u32) -> Race
}

Returns race details.

#![allow(unused)]
fn main() {
fn get_player_standing(env: Env, season_id: u32, player: Address) -> PlayerStanding
}

Returns player's season standings with points, completion count, and best finish.

Payment Model and Cross-Asset Integration

This example models payment as abstract credited-unit settlement:

Off-Chain Settlement (Illustrative)

  1. Player initiates cross-asset trade (e.g., USDC โ†’ stablecoin or asset)
  2. Payment gateway settles transaction
  3. Settlement proof returned to owner with receipt hash

On-Chain Credit

Owner calls credit_payment(player, units, receipt_hash):

  • Units represent settled payment value
  • Receipt hash ensures idempotent settlement
  • Credits are consumed by boost activation

Multi-Asset Representation

In production, payment units could represent:

  • USDC stablecoins
  • Native Stellar lumens (XLM)
  • Custom issued assets
  • Cross-chain bridged assets

The contract treats all as abstract credits, allowing flexible payment backends.

Testing

Run tests with:

cargo test

Test Coverage

  • League initialization
  • Race creation and player entry
  • Boost activation with various types
  • Payment credit system
  • Proof validation and replay protection
  • Race completion and standings
  • Multiple races and cumulative standings
  • Error conditions and edge cases

Building and Validation

Build the contract

cargo build --release

Format and lint

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings

Run tests

cargo test

Build to WASM

stellar contract build

Design Decisions

Why Abstract Payments?

The contract doesn't directly integrate with specific payment backends (Stripe, PayPal), but instead accepts settled credits. This allows flexibility:

  • The same contract can work with different payment gateways
  • Payment logic can be upgraded off-chain without contract changes
  • Anti-censorship: payment flow doesn't depend on specific custodian

Why Proof-Based Anti-Cheat?

Groth16 proofs enable:

  • Hidden tuning: Players prove valid race parameters without revealing them
  • Deterministic fairness: Proof ties race outcome to cryptographic commitment
  • Privacy: Only commitment revealed on-chain, detailed race state stays off-chain

Why Ledger-Based Timing?

Race duration uses ledger sequence numbers, not wall-clock time:

  • Stellar ledgers close ~every 5 seconds (deterministic)
  • No oracle dependency for timing
  • Replay protection via sequence-based race ID

Files

  • src/lib.rs: Main contract entrypoints and orchestration
  • src/types.rs: Data structures, enums, and storage keys
  • src/components.rs: ECS components
  • src/systems.rs: Core game logic and transitions
  • src/helpers.rs: Validation helpers
  • src/test.rs: Unit tests
  • README.md: Architectural overview and documentation
  • Cargo.toml: SDK dependencies

Compatibility

  • Soroban SDK: 25.1.0
  • Edition: 2021
  • Target: wasm32v1-none

License

MIT OR Apache-2.0

dice_duel

Other Canonical
soroban-gamezk-circuitsprivacy

Canonical ZK circuit example demonstrating circuits::fair_dice for verifiable dice rolls.

Purpose and pattern

This example showcases a verifiable on-chain dice rolling game. Players generate random numbers off-chain and submit ZK proofs to prove that their roll result is deterministic, within bounds, and bound to the initial committed seed, preventing manipulation of on-chain randomness.

Public contract API

FunctionParametersReturnsDescription
init_duelsides: u32, seed_commitment: BytesN<32>DuelConfigRegisters the dice parameters and the starting cryptographic seed commitment.
submit_rollplayer: Address, roll_result: u32, nonce: u32, proof: Groth16ProofboolVerifies a dice roll ZK proof and updates the roll record if valid.
roll_recordplayer: AddressRollRecordRetrieves the stored roll result for a player.

Architecture overview

                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                         โ”‚    Player     โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Rolls & Generates ZK Proof
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚      DiceDuel       โ”‚
                     โ”‚ (Soroban Contract)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Loads Spec
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ circuits::          โ”‚
                     โ”‚  fair_dice          โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The player commits a random seed and runs a deterministic calculation. They submit a Groth16 proof showing the calculation output matches the result of their roll.

Storage model

Dice duel config and player roll history are stored in Instance Storage on-chain via Soroban instance key-value associations.

Main gameplay flow

  1. Setup: Call init_duel to bind the dice properties and seed commitment.
  2. Roll: Players roll dice off-chain and calculate proof.
  3. Submit: Players call submit_roll to verify and record the roll on-chain.

Cougr APIs used

  • circuits::fair_dice: Implements the dice roll verification boundary specifications.
  • zk::Groth16Proof: Holds proof payload.

Integrate GameHarness and Scenario runner combined with test_fixtures::pipeline_proof to simulate individual rolls and multi-player roll scenarios.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Simple single-roll mechanics.
  • The seed is assumed to be cryptographically secure and generated off-chain.

Flappy Bird On-Chain Game

Arcade Transitional
game-appecs

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

Purpose and pattern

This example demonstrates a side-scroller gravity loop on Soroban with Cougr ECS concepts. It remains transitional while the arcade examples converge on the canonical snake GameApp architecture.

Public contract API

FunctionParametersReturn typeDescription
init_gamenone()Initializes bird, velocity, pipes, score, and tick state.
flapnone()Applies upward input velocity if the game is still active.
update_ticknone()Runs one scheduled tick for gravity, movement, pipes, collisions, and scoring.
get_scorenoneu32Returns the current score.
check_game_overnoneboolReturns whether the bird has crashed.
get_bird_posnone(i32, i32)Returns the bird position.

Architecture overview

contract entrypoint
  โ”œโ”€ reads game state from Soroban storage
  โ”œโ”€ applies input or tick systems
  โ””โ”€ writes updated state back to storage

Bird, pipe, position, velocity, and scoring markers are represented as ECS components in components.rs and updated by systems in systems.rs.

Storage model

Storage classDataWhy
Instance storagePer-contract game state where used by this example.Keeps small arcade state close to the contract instance.
Persistent storagePlayer- or world-scoped state where the example needs durable keyed state.Keeps game progress available across invocations.
Temporary storageNot used.The examples favor deterministic recalculation over ephemeral caches.

Main gameplay flow

  1. Call the initialization function to create the starting state.
  2. Submit an input action such as movement, jump, flap, rotation, or shoot.
  3. Call the tick/update function to run deterministic simulation logic.
  4. Query public getters for score, position, active state, or terminal status.
  5. Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.

Cougr APIs used

  • ComponentTrait and custom component modules document the ECS data boundary.
  • SimpleWorld, SimpleQueryBuilder, GameApp, ScheduleStage, or SystemConfig are used where this transitional example has already adopted the maintained runtime shape.
  • Auth, privacy, ZK, and standards APIs are intentionally not used; these arcade examples focus on deterministic game logic.

Build and test commands

cargo test
stellar contract build

Known limitations

The tests cover:

  • Game initialization
  • Bird flapping mechanics
  • Gravity and physics
  • Collision detection
  • Score tracking
  • Game over conditions

Deploying to Testnet

1. Create and Fund Account

Generate a new keypair:

stellar keys generate test-account --network <NETWORK>

Get the account address:

stellar keys address test-account

Fund the account using Friendbot:

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

2. Deploy Contract

stellar contract deploy \
  --wasm target/wasm32v1-none/release/flappy_bird.wasm \
  --source test-account \
  --network <NETWORK>

Save the returned CONTRACT_ID for later use.

3. Play the Game

Initialize Game

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source test-account \
  --network <NETWORK> \
  -- init_game

Make Bird Flap

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source test-account \
  --network <NETWORK> \
  -- flap

Advance Game Tick

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source test-account \
  --network <NETWORK> \
  -- update_tick

Check Score

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source test-account \
  --network <NETWORK> \
  -- get_score

Check Game Status

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source test-account \
  --network <NETWORK> \
  -- check_game_over

Get Bird Position

stellar contract invoke \
  --id <CONTRACT_ID> \
  --source test-account \
  --network <NETWORK> \
  -- get_bird_pos

Game Mechanics

Constants

  • GRAVITY: 2 pixels per tick
  • FLAP_VELOCITY: -15 pixels per tick (upward)
  • PIPE_SPEED: 3 pixels per tick (leftward)
  • GROUND_Y: 400 pixels
  • BIRD_SIZE: 20 pixels (hitbox radius)
  • PIPE_GAP: 100 pixels

How to Play

  1. Initialize a new game with init_game()
  2. Make the bird flap with flap() to gain upward velocity
  3. Advance the game with update_tick() - this applies:
    • Gravity to the bird
    • Movement to all entities
    • Collision detection
    • Score updates
  4. Repeat steps 2-3 to keep the bird alive and score points
  5. Game ends when bird hits ground, ceiling, or pipes

Scoring

  • Score increases by 1 for each pipe successfully passed
  • Bird must pass through the gap without touching the pipe

Code Examples

Creating a Custom Component

#![allow(unused)]
fn main() {
#[contracttype]
#[derive(Clone, Debug)]
pub struct BirdState {
    pub is_alive: bool,
}

impl ComponentTrait for BirdState {
    fn component_type() -> Symbol {
        symbol_short!("birdstate")
    }

    fn serialize(&self, env: &Env) -> Bytes {
        let mut bytes = Bytes::new(env);
        let value: u8 = if self.is_alive { 1 } else { 0 };
        bytes.append(&Bytes::from_array(env, &[value]));
        bytes
    }

    fn deserialize(env: &Env, data: &Bytes) -> Option<Self> {
        if data.len() != 1 {
            return None;
        }
        let is_alive = data.get(0).unwrap() != 0;
        Some(Self { is_alive })
    }
}
}

Implementing a Game System

#![allow(unused)]
fn main() {
pub fn apply_gravity(world: &mut World, env: &Env) {
    for entity in world.entities.iter_entities() {
        let entity_id = entity.id();
        if entity.has_component(&symbol_short!("birdstate")) {
            if let Some(vel_comp) = world.get_component(entity_id, &symbol_short!("velocity")) {
                if let Some(mut velocity) = Velocity::deserialize(env, vel_comp.data()) {
                    velocity.y += GRAVITY;
                    let new_vel_comp = Component::new(
                        symbol_short!("velocity"),
                        velocity.serialize(env)
                    );
                    world.storage.add_component(entity_id, new_vel_comp);
                }
            }
        }
    }
}
}

Troubleshooting

Common Issues

Issue: error: failed to compile

  • Solution: Ensure you have the correct Rust version (1.88.0+) and wasm32 target installed
  • Run: rustup update && rustup target add wasm32v1-none

Issue: stellar: command not found

Issue: Contract size too large

  • Solution: Ensure you're using release mode and the optimizations in Cargo.toml:
    [profile.release]
    opt-level = "z"
    lto = true
    codegen-units = 1
    

Issue: Transaction fails with "budget exceeded"

  • Solution: On-chain games are resource-intensive. Consider simplifying game logic or reducing the number of entities

Debugging Tips

  1. Use cargo test to verify logic before deploying
  2. Use --simulate flag to test transactions without broadcasting:
    stellar contract invoke --id <CONTRACT_ID> --simulate -- init_game
    
  3. Check contract logs in Stellar Explorer
  4. Use smaller test scenarios to isolate issues

Performance Considerations

On-Chain Constraints

  • WASM Binary Size: Keep contract under 1MB (optimized binary should be ~200KB)
  • Transaction Costs: Each transaction consumes fees - minimize storage operations
  • Execution Budget: Soroban has CPU and memory limits - keep game ticks simple

Optimizations Used

  1. Opt-level "z": Optimize for size in release builds
  2. LTO: Link-time optimization reduces binary size
  3. Minimal Storage: Only store essential game state
  4. Efficient Queries: Cache entity lookups when possible

Next Steps

Extending the Game

  1. Add Power-ups: Create new components for temporary abilities
  2. Difficulty Scaling: Increase pipe speed over time
  3. Multiplayer: Store multiple game states with player IDs
  4. Leaderboard: Track high scores across players
  5. Visual Output: Create a frontend that reads contract state and renders graphics

Learning More

License

This example is part of the Cougr project and follows the same license.

Contributing

  • Transitional code may preserve older storage or scheduling patterns for compatibility reference.
  • No authentication, matchmaking, real-time rendering, or production randomness is included.
  • One contract instance generally represents one game or one keyed set of player games.
  • For new work, prefer the canonical snake module split and GameApp tick wiring.

fog_explorer

Other Canonical
soroban-gamezk-circuitsprivacy

Canonical ZK circuit example demonstrating circuits::fog_of_war for private exploration.

Purpose and pattern

This example showcases a verifiable "fog of war" map exploration. Players move around a private map commitment, proving that they only reveal cells within their current line-of-sight visibility radius, without publishing the full map layout or their exact positions on the public blockchain.

Public contract API

FunctionParametersReturnsDescription
init_mapwidth: u32, height: u32, visibility_radius: u32MapConfigInitializes the map configuration and builds the ZK circuit specs.
register_explorerplayer: Address, explored_root: BytesN<32>ExplorerStateRegisters a player with their starting empty explored map commitment.
exploreplayer: Address, map_root: BytesN<32>, prior_explored_root: BytesN<32>, next_explored_root: BytesN<32>, origin_x: i32, origin_y: i32, tile_x: i32, tile_y: i32, proof: Groth16ProofboolVerifies a transition proof, updating the player's explored map root if valid.
explorer_stateplayer: AddressExplorerStateRetrieves the current exploration state of the given player.

Architecture overview

                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                         โ”‚    Player     โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Moves & Generates ZK Proof
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚     FogExplorer     โ”‚
                     โ”‚ (Soroban Contract)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Loads Spec
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ circuits::          โ”‚
                     โ”‚  fog_of_war         โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The player proves off-chain that the update from prior_explored_root to next_explored_root only uncovers the tile (tile_x, tile_y) within a visibility radius of their player coordinates. The contract verifies this transition against the map commitment.

Storage model

Player exploration commitments and map layouts are stored in Instance Storage on-chain. Using instance storage guarantees fast state reads and writes during hot loops.

Main gameplay flow

  1. Map Config: Call init_map to configure map dimensions and visibility boundaries.
  2. Registration: Explorer calls register_explorer to record their initial map state.
  3. Exploration: Explorer calls explore with a Groth16 proof to update their visible tiles and progress.

Cougr APIs used

  • circuits::fog_of_war: Configures and handles verification of private line-of-sight map calculations.
  • zk::experimental::{FogOfWarSnapshot, FogOfWarTransition}: Input structs for ZK state transition logic.
  • zk::Groth16Proof: Cryptographic proof data container.

Use GameHarness and test_fixtures to mock ZK inputs and pipeline proof bytes. This allows testing successful proof verification paths without running full proving ceremonies in unit tests.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Grid coordinates are simplified.
  • Map generation and parsing are managed off-chain.

Geometry Dash On-Chain Game

Arcade Transitional
game-appecs

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

Purpose and pattern

This example demonstrates a rhythm-platformer fixed-tick runner on Soroban with Cougr ECS concepts. It remains transitional while the arcade examples converge on the canonical snake GameApp architecture.

Public contract API

FunctionParametersReturn typeDescription
init_gameplayer: Address, level_id: u32()Initializes a player level.
jumpplayer: Address()Applies the current mode input.
update_tickplayer: Address()Advances the level simulation by one tick.
get_stateplayer: AddressGameStatusReturns Playing, Crashed, or Completed.
get_posplayer: Address(i32, i32)Returns player position.
get_scoreplayer: Addressu32Returns distance-based score.
get_modeplayer: Addressu32Returns current mode identifier.

Architecture overview

contract entrypoint
  โ”œโ”€ reads game state from Soroban storage
  โ”œโ”€ applies input or tick systems
  โ””โ”€ writes updated state back to storage

Player physics, mode, obstacle, and status components live in components.rs; motion/collision behavior lives in systems.rs.

Storage model

Storage classDataWhy
Instance storagePer-contract game state where used by this example.Keeps small arcade state close to the contract instance.
Persistent storagePlayer- or world-scoped state where the example needs durable keyed state.Keeps game progress available across invocations.
Temporary storageNot used.The examples favor deterministic recalculation over ephemeral caches.

Main gameplay flow

  1. Call the initialization function to create the starting state.
  2. Submit an input action such as movement, jump, flap, rotation, or shoot.
  3. Call the tick/update function to run deterministic simulation logic.
  4. Query public getters for score, position, active state, or terminal status.
  5. Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.

Cougr APIs used

  • ComponentTrait and custom component modules document the ECS data boundary.
  • SimpleWorld, SimpleQueryBuilder, GameApp, ScheduleStage, or SystemConfig are used where this transitional example has already adopted the maintained runtime shape.
  • Auth, privacy, ZK, and standards APIs are intentionally not used; these arcade examples focus on deterministic game logic.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Transitional code may preserve older storage or scheduling patterns for compatibility reference.
  • No authentication, matchmaking, real-time rendering, or production randomness is included.
  • One contract instance generally represents one game or one keyed set of player games.
  • For new work, prefer the canonical snake module split and GameApp tick wiring.

Guild Arena

Other Canonical
authstandardsgame-app

PvP arena game on Soroban demonstrating guild-based social recovery and multi-device play using Cougr-Core.

Overview

On-chain gaming has a key risk: players losing access to accounts holding progress, items, and currency. Guild Arena solves this with two Cougr-Core account patterns:

  1. Social Recovery - guild members act as guardians who can collectively restore account access after a timelock period
  2. Multi-Device - players register multiple device keys (desktop, mobile) with per-device permission policies

How It Works

Account Setup

Player registers โ†’ sets 3 guild members as guardians (threshold 2-of-3)
                 โ†’ adds desktop key (Full permissions)
                 โ†’ adds mobile key (PlayOnly permissions)

Gameplay

Players queue for PvP matches. The challenger moves first, then the players alternate turns. Combat has three actions:

  • Attack - standard damage
  • Defend - reduced damage
  • Special - high damage

After each match, the winner gains 32 rating points and the loser loses 32 points (without dropping below zero). Every 3 wins triggers a level-up with stat boosts.

Recovery Flow

Player loses key โ†’ Guardian 1 initiates recovery
                 โ†’ Guardian 2 approves (threshold met)
                 โ†’ 7-day timelock starts
                 โ†’ After timelock: finalize_recovery()
                 โ†’ New key active, old key revoked
                 โ†’ All stats, rating, history preserved

Contract API

FunctionDescription
register_playerRegister with guardians and recovery config
add_deviceAdd a device key with policy (Full or PlayOnly)
remove_deviceRevoke a device key
start_matchQueue for or start a PvP match
submit_actionSubmit combat action (Attack/Defend/Special)
initiate_recoveryGuardian starts recovery process
approve_recoveryGuardian approves recovery
finalize_recoveryComplete recovery after timelock
get_playerQuery player profile
get_matchQuery current arena state

Device Policies

LevelPlayTrade/Admin
Fullโœ“โœ“
PlayOnlyโœ“โœ—

Building

cargo build
cargo test
stellar contract build

Testing

cargo test

Tests cover:

  • Player registration with guardians
  • Multi-device management
  • Device policy enforcement
  • Full combat match resolution
  • Rating updates after matches
  • Out-of-turn actions, active-match protection, duplicate registration, and device ownership
  • Complete recovery lifecycle (initiate โ†’ approve โ†’ timelock โ†’ finalize)
  • Recovery with insufficient approvals (rejected)
  • Game state preservation through recovery

Architecture

Uses Cougr-Core ECS patterns:

Components: Fighter, MatchRecord, GuildMembership, ArenaState

Systems: Matchmaking, Combat, Rating, Recovery, Device authorization

Storage: Soroban persistent storage keyed by player/device addresses. Recovery and device state managed through RecoverableAccount and DeviceManager from cougr-core.

Reference Role

This example is the canonical reference for Cougr's account-oriented flows:

  • social recovery
  • multi-device authorization
  • gameplay permissions separated from full admin authority

Unlike the arcade examples, this one is intentionally more account-centric than GameApp-centric.

Prerequisites

  • Rust 1.89+
  • rustup target add wasm32v1-none
  • Stellar CLI (optional, for deployment)

License

MIT OR Apache-2.0

๏ธ Guild Treasury Wars - DAO-Governed Factions with stellar-zk Commitments

Other Transitional
authprivacycommit-revealecs

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

Build Status Tests Stellar

A guild-based strategy game implemented as a Soroban smart contract using cougr-core's ECS framework. Guilds manage shared treasuries, vote on strategic actions through DAO mechanics, and compete through hidden strategic commitments powered by stellar-zk.

Why stellar-zk?

In strategy games, revealing your plans to the enemy is a losing move. stellar-zk enables sealed war plans - players commit to strategies without revealing them, then prove their commitments on-chain when it's time to resolve battles.

Without stellar-zkWith stellar-zk
All actions are public on-chainStrategies are hidden until reveal
Opponents can counter every moveSealed commitments prevent scouting
No strategic depthReal hidden-information gameplay
Generic DAO demoStellar-native governance game

Gameplay Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 1. GUILD CREATION                                        โ”‚
โ”‚    Admin creates a guild with INITIAL_TREASURY (1000)     โ”‚
โ”‚    โ†’ Members join the guild (up to 10)                    โ”‚
โ”‚    โ†’ cougr-core ECS World tracks entities                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 2. GOVERNANCE (DAO-style proposals)                       โ”‚
โ”‚    Members submit proposals: Defend, Attack, Upgrade,     โ”‚
โ”‚    or Allocate resources from the shared treasury          โ”‚
โ”‚    โ†’ 51% vote threshold for approval/rejection             โ”‚
โ”‚    โ†’ Inspired by Stellar governance (governance.script3)   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 3. TREASURY EXECUTION                                     โ”‚
โ”‚    Approved proposals deduct treasury and apply effects:   โ”‚
โ”‚    โ†’ Defend (100): +10 defense strength                   โ”‚
โ”‚    โ†’ Attack (200): Launch campaign against enemy guild     โ”‚
โ”‚    โ†’ Upgrade (150): +10 attack strength                   โ”‚
โ”‚    โ†’ Allocate: Custom resource allocation                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 4. SEALED WAR PLANS (stellar-zk commit-reveal)            โ”‚
โ”‚    Members commit: SHA256(action || target || amount ||    โ”‚
โ”‚    salt) โ†’ hash stored on-chain, strategy hidden           โ”‚
โ”‚    Reveal: provide preimage, contract verifies hash        โ”‚
โ”‚    โ†’ Nullifier consumed (anti-replay protection)           โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 5. BATTLE RESOLUTION                                      โ”‚
โ”‚    Attack strength vs defense strength                     โ”‚
โ”‚    โ†’ Winner plunders defender's treasury                   โ”‚
โ”‚    โ†’ Game round incremented                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

stellar-zk Integration

This example uses stellar-zk for real hidden-strategy gameplay, not just narrative flavor. The integration follows stellar-zk's on-chain verification patterns:

Where stellar-zk Fits

Phasestellar-zk RoleContract Function
CommitSHA256 hash seals the war plansubmit_strategy_commitment()
RevealPreimage verification proves the planreveal_strategy()
NullifierAnti-replay prevents double-revealDataKey::Nullifier(hash)
ResolveDeterministic outcome after verificationresolve_battle()

Commitment Hash

commitment = SHA256(action_type โˆฅ target_guild_id โˆฅ resource_amount โˆฅ salt)

The salt (32-byte random value) ensures that identical strategies produce different hashes, preventing pattern analysis by opponents.

Nullifier Pattern

Each commitment hash acts as a nullifier (following stellar-zk's verifier contract design). Once revealed:

  1. The commitment is marked as revealed = true
  2. The hash is stored in DataKey::Nullifier(hash) โ†’ true
  3. Any attempt to reuse the same commitment is rejected

References


Cougr-Core Integration

ECS Components

ComponentFieldsPurpose
Guildadmin, name, treasury, member_count, defense/attack_strengthOn-chain faction
Proposalproposer, action, votes_for/against, statusDAO governance
StrategyCommitmentcommitment_hash, committer, revealedSealed war plan
StrategyRevealaction_type, target_guild_id, resource_amount, saltRevealed preimage
GameStatetotal_guilds, total_proposals, active_campaigns, roundGlobal state

ECS World

#![allow(unused)]
fn main() {
// cougr-core ECS entity management
let mut world = cougr_core::SimpleWorld::new(&env);
let _guild_entity = world.spawn_entity();
}

Contract API

Guild Management

FunctionParametersReturnsDescription
init_guildguild_adminu32Create guild with treasury
join_guildguild_id, member-Join an existing guild

Governance

FunctionParametersReturnsDescription
submit_proposalproposer, proposal: ProposalInputu32Create a DAO proposal
votevoter, proposal_id, support: bool-Cast a vote
execute_proposalproposal_id-Execute approved proposal

Strategy (stellar-zk)

FunctionParametersReturnsDescription
submit_strategy_commitmentguild_member, proof_input: ProofInput-Seal a war plan
reveal_strategyguild_member, guild_id, reveal: StrategyReveal-Reveal and verify
resolve_battleattacker_member, defender_member, attacker_guild_id, defender_guild_id-Resolve outcome

Queries

FunctionParametersReturnsDescription
get_state-GameStateGlobal game state
get_guildguild_idGuildGuild info
get_proposalproposal_idProposalProposal info

๏ธ Quick Start

Prerequisites

ToolVersionInstallation
Rust1.88.0+rustup.rs
Stellar CLILatestStellar Docs
WASM Target-rustup target add wasm32v1-none

Build

# Standard Rust build
cargo build

# Build WASM for Soroban deployment
stellar contract build

Test

cargo test

Validation

# Full validation suite
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
stellar contract build

Test Results: 14 tests passing โœ…

TestDescription
test_init_guildGuild creates with correct treasury and state
test_join_guildMember joins and count increments
test_submit_proposalProposal created with correct fields
test_unauthorized_proposalNon-member rejected
test_vote_counting_and_threshold51% threshold approves
test_vote_rejectionMajority-against rejects
test_unauthorized_voteNon-member vote rejected
test_double_voteDouble-voting prevented
test_treasury_execution_approvedTreasury deducted, effects applied
test_execute_unapprovedNon-approved execution rejected
test_strategy_commitment_and_revealFull commit-reveal flow
test_invalid_revealWrong preimage rejected
test_double_reveal_preventionNullifier blocks re-reveal
test_battle_resolutionAttack resolves, resources transfer

Project Structure

examples/guild_treasury_wars/
โ”œโ”€โ”€ Cargo.toml          # Dependencies: cougr-core + soroban-sdk
โ”œโ”€โ”€ README.md           # This documentation
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ lib.rs          # Contract entry points
    โ”œโ”€โ”€ types.rs        # ECS Components (Guild, Proposal, Strategy, etc.)
    โ”œโ”€โ”€ governance.rs   # ProposalSystem, VotingSystem, TreasuryExecutionSystem
    โ”œโ”€โ”€ strategy.rs     # StrategyProofSystem, ResolutionSystem (stellar-zk)
    โ””โ”€โ”€ test.rs         # Unit tests (14 tests)

License

MIT OR Apache-2.0

hidden_hand

Card Canonical
soroban-gamezk-circuitsprivacy

Canonical ZK circuit example demonstrating circuits::hidden_cards for private card deals.

Purpose and pattern

This example demonstrates how to implement private card dealing (e.g., poker hand setups) on-chain. It uses ZK proofs to verify that a deck was shuffled and a hand of cards was dealt honestly, without revealing the cards to other players or the public on-chain ledger.

Public contract API

FunctionParametersReturnsDescription
init_tabledeck_size: u32, hand_size: u32TableConfigConfigures the game table with the specified deck and hand size, initializing the ZK verifier.
verify_dealplayer: Address, deck_root: BytesN<32>, hand_commitment: BytesN<32>, proof: Groth16ProofboolVerifies a Groth16 deal proof against the configured hidden-cards layout.

Architecture overview

                         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                         โ”‚ Card Dealer  โ”‚
                         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Generates ZK Proof
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚     HiddenHand      โ”‚
                     โ”‚ (Soroban Contract)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Loads Spec
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ circuits::          โ”‚
                     โ”‚  hidden_cards       โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The dealer generates a Groth16 proof off-chain, verifying that the hand commitment was indeed created from a valid slice of the shuffled deck. The contract verifies the proof using the hidden_cards circuit spec.

Storage model

The TableConfig containing the card configuration is stored in Instance Storage. The ZK verification keys are built dynamically or stored securely within the contract instance.

Main gameplay flow

  1. Setup: The contract calls init_table(deck_size, hand_size) to prepare the ZK circuit parameters.
  2. Dealing: The dealer generates a hand commitment and a proof off-chain.
  3. Verification: The contract calls verify_deal(player, deck_root, hand_commitment, proof) to validate that the deal is correct before proceeding with gameplay.

Cougr APIs used

  • circuits::hidden_cards: Fetches the circuit layout, verification key, and verify wrapper for Groth16 hidden cards.
  • zk::Groth16Proof: Data structure representing the cryptographic proof.

Utilize GameHarness and test_fixtures to mock ZK inputs and pipeline proof bytes. This allows testing successful proof verification paths without running full proving ceremonies in unit tests.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Simple single-deck configuration.
  • Proving is done entirely off-chain.

Memory Match

Puzzle Transitional
ecs

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

An on-chain Memory Match card game built with the Cougr ECS framework on Stellar Soroban.

Purpose and pattern

This example demonstrates a single-player, turn-based reveal-and-match game where pairs of cards are flipped and compared against each other rather than against fixed rules. It showcases Cougr's ComponentTrait pattern for typed, byte-serializable game state: cards, the board, and game progress are each modeled as a #[contracttype] struct with manual serialize/deserialize, rather than being scanned through SimpleWorld/SimpleQueryBuilder. This is a lighter-weight integration suited to a single fixed-shape game state (16 cards, one board, one player) with no dynamic entity population.

Public contract API

FunctionParametersReturnsDescription
init_gameplayer: AddressGameStateSeeds a fresh 16-card board (8 deterministic pairs) for player and returns the initial state.
reveal_cardplayer: Address, position: u32RevealInfoReveals the card at position. Resolves a pair once two cards are revealed (match, no-match, or game-over). Panics if not initialized, the caller isn't the registered player, the game is over, two cards are already revealed, the position is out of range, or the card is already revealed/matched.
get_game_state-GameStateCurrent board view, revealed/matched counts, move count, and game-over flag.
reset_gameplayer: AddressGameStateHides all cards and clears progress, keeping the same board layout. Panics if not initialized or the caller isn't the registered player.

Architecture overview

There is no GameApp tick loop - reveal_card runs a fixed pipeline synchronously on each invocation:

reveal_card
  โ””โ”€ ValidationSystem     โ†’ checks caller, game-over state, two-card limit, position bounds
  โ””โ”€ RevealSystem          โ†’ flips the card to Revealed, increments the move counter
  โ””โ”€ (when 2 cards are revealed)
       โ””โ”€ PairResolutionSystem โ†’ compares values; marks Matched + checks for game-over,
                                  or hides both cards again on a mismatch

reset_game runs a separate ResetSystem that rehides every card and zeroes progress counters without regenerating the board layout. components.rs defines the data (CardComponent, BoardComponent, GameStateComponent, the ECSWorldState aggregate, and the public GameState/RevealInfo/RevealResult types); systems.rs holds the pure validation, reveal, resolution, reset, and projection functions; lib.rs owns the single ECSWorldState and is the only module that touches contract storage.

Storage model

The entire game lives under one instance-storage key (WORLD_KEY, symbol_short!("WORLD")) as a single ECSWorldState struct bundling all 16 card components, the board component, and the game-state component. Instance storage is used - not persistent or temporary - because there is exactly one game per contract instance and the state must survive for the lifetime of that instance with no per-entry TTL management.

Main gameplay flow

  1. Deployer calls init_game(player); 16 cards are created with a deterministic layout (positions 0โ€“7 and 8โ€“15 hold matching values 0โ€“7), all starting Hidden.
  2. Player calls reveal_card(player, position) for a hidden card; it flips to Revealed and the move counter increments.
  3. Once two cards are revealed, the contract compares their values:
    • Match: both cards become Matched, matched_pairs increments, and reveals are re-enabled. If all 8 pairs are now matched, the game is marked over.
    • No match: both cards flip back to Hidden and reveals are re-enabled.
  4. Player repeats step 2โ€“3 until matched_pairs reaches 8 and game_over is true.
  5. Player (or anyone reading state) calls get_game_state at any point to read the board, or calls reset_game to rehide all cards and start over on the same layout.

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives each component (CardComponent, BoardComponent, GameStateComponent) a component_type() symbol and explicit byte-level serialize/deserialize. This is the only Cougr API the example uses: the game has one fixed-shape ECSWorldState per contract instance (16 cards, one board, one player) rather than a dynamic entity population, so SimpleWorld/SimpleQueryBuilder scanning would add overhead with no benefit. There is a single decision point per call (reveal or reset), so GameApp/ScheduleStage staged ticking is not needed either, and the game has no hidden information, multi-device auth, or proof-verification requirements that would call for auth, privacy::stable, privacy::experimental, or ops standards.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp, ScheduleStage, or SimpleWorld - game logic is invoked directly from contract entrypoints rather than through a tick-based scheduler, since a single-player game with one decision point per call does not need staged scheduling.
  • The card layout is fixed and deterministic (not shuffled), so the pairing is the same for every game; randomized layouts are out of scope for this example.
  • reset_game reuses the same fixed layout rather than generating a new one.
  • No timeout/forfeit mechanism, scoring across multiple games, or multiplayer support.

Minesweeper

Puzzle Transitional
ecs

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

An on-chain Minesweeper game built with the Cougr ECS framework on Stellar Soroban.

Purpose and pattern

This example demonstrates a single-player reveal-and-deduce puzzle over a fixed, deterministic mine layout - no player input or randomness influences where mines are placed, which keeps the board auditable and reproducible across deployments. It showcases Cougr's ComponentTrait pattern for typed, byte-serializable game state: the board, the (hidden) mine layout, and the game status are each their own component with manual serialize/deserialize, demonstrating explicit byte-layout control for a fixed-size 9ร—9 grid.

Public contract API

FunctionParametersReturnsDescription
init_game-GameStateInitializes a 9ร—9 board with mines placed at fixed, deterministic positions. Overwrites any existing game.
reveal_cellrow: u32, col: u32RevealResultReveals the cell at (row, col). Returns success, is_mine, adjacent_mines, and a status message symbol (ok, invalid, revealed, boom, or over).
get_state-GameStateCurrent status, revealed-cell count, and remaining safe cells.
get_visible_cellrow: u32, col: u32VisibleCellStateWhether the cell is revealed, whether it's a mine (only meaningful once revealed), and its adjacent-mine count.
is_finished-boolWhether the game has reached Won or Lost.
get_board-Vec<u32>Raw 81-cell board array (row-major); each entry is 0โ€“8 for a revealed count, 9 for hidden, or 10 for a revealed mine.
reset_game-GameStateRe-initializes the game (equivalent to calling init_game again).

Architecture overview

There is no GameApp tick loop - reveal_cell runs all logic synchronously in a fixed pipeline on each invocation:

reveal_cell
  โ””โ”€ status/bounds/already-revealed checks  โ†’ rejects finished games, out-of-bounds
     coordinates, and cells already revealed with the matching `message` symbol
  โ””โ”€ mine check                              โ†’ if the cell holds a mine, marks the cell,
     sets status to Lost, and returns immediately
  โ””โ”€ adjacency count + board update          โ†’ writes the computed adjacent-mine count
     into the board for a safe reveal
  โ””โ”€ CompletionSystem                        โ†’ sets status to Won once every safe cell
     has been revealed

components.rs defines the data (BoardComponent, MineLayoutComponent, GameStateComponent, plus the ECSWorldState aggregate and the public API types); systems.rs holds the pure functions (place_mines_deterministic, completion_system); lib.rs owns the single ECSWorldState per contract instance and is the only module that touches contract storage.

Storage model

The entire game lives under one instance-storage key (WORLD_KEY, symbol_short!("WORLD")) as a single ECSWorldState struct bundling the board, the mine layout, and the game status. Instance storage is used - not persistent or temporary - because there is exactly one game per contract instance and the state must survive for the lifetime of that instance with no per-entry TTL management.

Main gameplay flow

  1. Deployer (or any caller) calls init_game(). The board starts fully hidden and place_mines_deterministic seeds 10 mines at fixed coordinates across the 9ร—9 grid.
  2. Player calls reveal_cell(row, col) for a hidden cell.
  3. If the cell is a mine, the board marks it revealed-as-mine, status flips to Lost, and the call returns message = boom. No further reveals are accepted until reset_game.
  4. If the cell is safe, the contract counts mines among its up-to-8 neighbors, writes that count into the board, and increments the revealed-cell counter.
  5. After each safe reveal, CompletionSystem checks whether all 71 safe cells (81 total minus 10 mines) have been revealed; if so, status flips to Won.
  6. Caller reads get_state, get_visible_cell, or is_finished to check progress or terminal state; reset_game starts over with the same deterministic layout.

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives each component (BoardComponent, MineLayoutComponent, GameStateComponent) a component_type() symbol and explicit byte serialize/deserialize, chosen because the game has one fixed-shape 81-cell grid rather than a dynamic entity population that would benefit from SimpleWorld/SimpleQueryBuilder scanning.
  • No other cougr-core APIs are used. GameApp/ScheduleStage are unnecessary because reveal_cell has a single decision point per call with no multi-stage ordering; SimpleWorld/ SimpleQueryBuilder are unnecessary because the game has exactly three fixed components and never scans entities by type; auth, privacy::stable/experimental, and ops standards are unused because this example has no multi-party authorization, hidden-information disclosure, or pausability/ownership requirements - the entire board state is openly readable via get_board and get_visible_cell.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp, ScheduleStage, or SimpleWorld - reveal logic is invoked directly from contract entrypoints since a single-player puzzle with one decision point per call has no need for staged scheduling.
  • Mine placement is fixed and deterministic rather than randomized per game; this keeps the layout auditable but means every new game (or reset_game call) has mines in the same positions.
  • No flagging system - the contract only supports revealing cells, not marking suspected mines.
  • No multi-player or wagering support; any caller can call any entrypoint for the single shared game instance.

Murdoku

Puzzle Transitional
authsession-authzk-circuitsprivacygame-appecs

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

Murdoku is a murder mystery logic puzzle built with the Cougr ECS framework on Stellar Soroban. It demonstrates how to combine Entity Component System (ECS) game state, Pollar social logins, session key authorization, and zero-knowledge proof verification for privacy-preserving puzzle solving.

Purpose and Pattern

Murdoku is an on-chain logic puzzle that combines Sudoku-style Latin square constraints with murder mystery storytelling. Players place suspects (e.g., characters, weapons, rooms) on a grid while satisfying constraints provided by clues (e.g., "The suspect with the knife is not in the same row as the butler").

This example showcases the canonical reference architecture for a full-stack game within the Cougr ecosystem:

  • Entity Component System (ECS): Separation of game state, input validation, puzzle constraint checks, and win conditions using the Cougr ECS engine.
  • Social Authentication & Session Keys: Account abstraction utilizing Pollar for embedded wallet logins and scoped session key creation (SessionBuilder) to support non-intrusive gameplay transactions.
  • ZK Privacy Layer: The solution is never stored in plaintext on-chain. Instead, puzzle creators submit a cryptographic commitment (Poseidon2 hash of the solution + salt) and a Groth16 verifier key. Players prove they know a valid solution by submitting a Groth16 zero-knowledge proof, without revealing the solution itself.

The contract supports two build modes controlled by the zk Cargo feature:

  • Default (v1): Plaintext solution stored on-chain. Simpler but exposes the solution to on-chain observers.
  • zk feature enabled: Solution commitment + Groth16 proof verification. The solution remains private; the contract only stores a hash and validates ZK proofs.

Architecture Overview

Murdoku follows a decoupled design where all gameplay rules and state changes are handled by the Soroban contract, while the user interface and wallet management are facilitated by a React web app.

Browser (Vite + React)
  โ””โ”€โ”€ Pollar (@pollar/react) - embedded wallet, social login
  โ””โ”€โ”€ contract.ts - Stellar SDK contract client
        โ”‚
        โ–ผ
  Soroban Contract (examples/murdoku/src/)
  โ”œโ”€โ”€ lib.rs - entrypoints & GameApp wiring
  โ”œโ”€โ”€ components.rs - ECS components (Board, Suspect, GameState)
  โ”œโ”€โ”€ systems.rs - validation, move execution, and completion systems
  โ”œโ”€โ”€ types.rs - domain types (Clues, Suspects, GridConfiguration)
  โ””โ”€โ”€ auth.rs - session keys, CougrAccount wiring
        โ”‚
        โ–ผ
  Stellar Testnet (Soroban RPC)

The React frontend embeds the Pollar SDK for OAuth/social logins. Pollar creates a temporary keypair representing a session key. The frontend uses the session key to sign game moves (e.g., place_suspect). The Soroban contract verifies that the session key is valid and authorized to act on behalf of the player using the authorize_with_fallback module.


Public Contract API

Below is the entrypoint surface defined in the #[contractimpl] block of the Murdoku smart contract. Some entrypoints are only available with the zk feature enabled.

FunctionParametersReturn TypeDescription
init_gameenv: Env, admin: Address()Initializes the contract state and registers the game administrator.
submit_puzzle (v1)env: Env, creator: Address, size: u32, solution: Vec<u32>, clues: Vec<Clue>u32Registers a new puzzle in the catalog with plaintext solution. Validates Latin square constraints.
submit_puzzle (ZK)env: Env, creator: Address, grid_size: u32, suspects: Vec<String>, clues: Vec<Clue>, solution_commitment: BytesN<32>, verifier_key: Bytes, metadata: PuzzleMetadatau32Registers a new puzzle with a solution commitment and Groth16 verifier key. No plaintext solution is stored.
list_puzzlesenv: Env, offset: u32, limit: u32Vec<PuzzleSummary>Returns a paginated list of summaries of all registered puzzles.
get_puzzleenv: Env, puzzle_id: u32PuzzleRetrieves puzzle details. In ZK mode, returns the solution commitment (not the plaintext solution).
deactivate_puzzleenv: Env, caller: Address, puzzle_id: u32()Deactivates a puzzle. Only the creator can deactivate.
submit_proof (ZK)env: Env, player: Address, puzzle_id: u32, proof: Bytes, public_inputs: Vec<BytesN<32>>()Submits a Groth16 proof of a valid solution. Marks the puzzle as solved for the player on success.
is_solved (ZK)env: Env, puzzle_id: u32, player: AddressboolReturns true if the player has submitted a valid proof for the puzzle.

Storage Model

Murdoku uses Soroban's state storage model (Instance, Persistent, and Temporary) to optimize gas costs and storage lifetimes.

| Storage Type | Data Kept | Lifetime | Rationale | |---|---|---|---|---| | Persistent | Registered Puzzle catalog (by ID), Creator profiles, Passkey credentials, Verifier keys (ZK), Solver state (ZK) | Indefinite | Puzzle templates and user credentials must persist forever across sessions and are read-heavy. | | Instance | Active Player Game session, ECS World state (SimpleWorld), Admin configs | Extended (Renewed on play) | The active board state must persist during active play but can be garbage-collected or archived if the player abandons the game. | | Temporary | Active Session Key tokens, cryptographic challenges | Short-term (Expires in blocks) | Session authorization keys only need to last for the duration of the play session and should expire automatically to free state space. |

In ZK mode, the following additional storage keys are used:

Key PatternTypeDescription
(Symbol("VKEY"), puzzle_id)BytesXDR-serialized Groth16 VerificationKey
(Symbol("SOLVED"), puzzle_id, player)boolWhether a player has solved this puzzle
(Symbol("SOLVER_CNT"), puzzle_id)u32Number of unique solvers for this puzzle

Main Gameplay Flow

Play Flow

  1. Connect Wallet: The player logs into the frontend via Pollar using an email or social login. Pollar provisions a Stellar account and prepares a local session key.
  2. Browse Catalog: The frontend calls list_puzzles to fetch and render the list of available murder mystery puzzles.
  3. Open Puzzle: The player selects a puzzle. The frontend calls get_puzzle to retrieve the clues and grid dimensions, then calls start_game to initialize the ECS world state for this puzzle.
  4. Place Suspects: The player drags and drops suspects (e.g., characters, weapons) onto grid cells. Each placement calls place_suspect. This transaction is signed by the local session key and bypasses manual wallet confirmations.
  5. On-chain Validation: The contract executes the validation system:
    • Verifies the cell is editable.
    • Enforces Latin square constraints (no duplicate suspects in the same row or column).
    • Validates placement against the puzzle's clues.
  6. Completion: If all cells are filled correctly, the contract marks the game status as solved. is_solved returns true, and the frontend displays a victory screen.

Create Flow

  1. Connect Wallet: The creator authenticates on the creator portal via Pollar.
  2. Configure Puzzle: The creator designs a puzzle:
    • Selects grid size (4x4 or 5x5).
    • Enters the full solution grid.
    • Defines the clues (e.g., cell constraints, adjacency rules).
  3. Submit Puzzle (v1): The creator clicks "Publish", triggering a call to submit_puzzle with the plaintext solution.
  4. Submit Puzzle (ZK): The creator generates a Poseidon2 commitment of the solution and a Groth16 verifier key, then calls submit_puzzle with the commitment and key instead of the plaintext.
  5. Contract Validation:
    • v1 mode: The contract validates the Latin square constraints and clue consistency via ECS systems.
    • ZK mode: The contract validates puzzle structure (grid size, suspects, clue bounds) but cannot verify solution correctness - the ZK proof catches cheating at solve time.
  6. Publishing: If checks pass, the contract assigns a puzzle ID, stores the puzzle in persistent storage, and activates it.

Cougr APIs Used

Murdoku is built on the core APIs of the Cougr framework.

API / ModuleUsed ForLocation
GameAppOrchestrates systems execution flow during suspect placement and puzzle registration.src/lib.rs
ScheduleStageEnsures that validation systems execute strictly before update systems, and completion systems execute last.src/lib.rs
SimpleWorldStores the active ECS state including entity components (e.g. Board, MoveCount).src/lib.rs, src/systems.rs
impl_component!Macros defining serialized game components like BoardComponent and GameStatusComponent.src/components.rs
auth::SessionBuilderBuilds scoped session keys containing specific permissions (e.g., only allowing place_suspect).src/auth.rs
auth::authorize_with_fallbackAuthorizes transactions using either the active session key or the player's direct signature.src/auth.rs
ops::OwnableRestricts contract initialization and admin parameters to the contract owner.src/lib.rs

Frontend Setup

1. Requirements

  • Node.js v18 or later
  • Stellar CLI installed and configured

2. Run Locally

Navigate to the frontend directory:

cd examples/murdoku/frontend

Copy the environment template:

cp .env.example .env

Open .env and set the variables:

  • VITE_POLLAR_API_KEY: Obtain this from the Pollar Developer Console by registering an application.
  • VITE_CONTRACT_ID: The deployed contract ID from your testnet deployment (see below).

Install dependencies and run the Vite development server:

npm install
npm run dev

The frontend will run locally on http://localhost:5173.


Contract Build and Test Commands

Execute the following commands from the examples/murdoku directory to format, lint, test, and build the contract.

cd examples/murdoku
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo test --features zk   # run ZK-specific tests
stellar contract build

To build with the ZK privacy layer enabled:

cargo build --features zk
cargo test --features zk

Deploying to Testnet

To deploy and test the contract manually on Stellar Testnet, use the following commands:

# 1. Generate an identity for deployment
stellar keys generate murdoku_deployer

# 2. Fund the identity using Friendbot
stellar keys fund murdoku_deployer --network <NETWORK>

# 3. Deploy the compiled WASM to Testnet
CONTRACT_ID=$(stellar contract deploy \
  --wasm target/wasm32v1-none/release/murdoku.wasm \
  --network <NETWORK> \
  --source murdoku_deployer)

# 4. Initialize the contract game registry
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  --source murdoku_deployer \
  -- init_game \
  --admin murdoku_deployer

# 5. Submit a minimal 4x4 placeholder puzzle to the catalog
# (Solution grid contains flat representation of a 4x4 Latin square)
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  --source murdoku_deployer \
  -- submit_puzzle \
  --creator murdoku_deployer \
  --size 4 \
  --solution '[1,2,3,4,2,3,4,1,3,4,1,2,4,1,2,3]' \
  --clues '[]'

ZK Privacy Layer

Circuit Statement

The Groth16 circuit proves the following statement without revealing the solution:

"I know a set of values cells such that: (1) cells is a valid Latin square of size N, (2) Poseidon2(cells || salt) == commitment, (3) each cell value is in range 1..=N."

Public Inputs Format

IndexTypeDescription
0BytesN<32>Solution commitment (Poseidon2 hash of solution + salt)
1..NBytesN<32>Additional public inputs (e.g., grid size, clue hashes)

Player Solve Flow (ZK mode)

  1. Browse Puzzles: Frontend calls list_puzzles and get_puzzle to fetch puzzle metadata (grid size, suspects, clues, solution commitment - not the plaintext solution).
  2. Solve Off-Chain: The player solves the puzzle manually, arriving at a candidate grid arrangement.
  3. Generate Proof: The frontend (or a backend prover service) generates a Groth16 proof that the player knows a valid arrangement consistent with the commitment.
  4. Submit Proof: The player calls submit_proof(player, puzzle_id, proof, public_inputs). The contract verifies the Groth16 proof against the stored verifier key.
  5. Mark Solved: On valid proof, the contract marks the puzzle as solved for that player and increments the solver count.

Key Files

FilePurpose
src/zk.rsGroth16 proof verification logic, verifier key storage, solver state tracking
src/components.rsSolutionCommitment ECS component (ZK mode only)
src/lib.rsFeature-gated Puzzle struct, submit_proof entrypoint, is_solved query

Backward Compatibility

The v1 plaintext solution path is preserved behind the default (no zk feature) build. To switch between modes:

# v1 mode (default)
cargo build
cargo test

# ZK mode
cargo build --features zk
cargo test --features zk

Known Constraints

  • Trusted Setup: Groth16 requires a trusted setup ceremony for each circuit. The development verifier keys used in this implementation are not production-safe. A proper multi-party ceremony must be completed before mainnet deployment. See the module-level docs in src/zk.rs for details.
  • Client-Side Proof Generation: Generating Groth16 proofs in the browser requires a WASM-compiled prover. This is tracked as a separate enhancement and is not yet implemented in the frontend.
  • Circuit Size: A 5ร—5 Murdoku circuit has 25 cells. Each cell requires range checks (1..=N) and Latin square row/column uniqueness constraints. The constraint count should be verified against Stellar's Groth16 verification limits before production use.

Known Limitations

  • ZK trusted setup is development-only. The Groth16 verifier keys shipped here come from a development setup and are not production-safe. See Known Constraints above for the full ZK caveats (trusted setup, client-side proving, circuit size).
  • Difficulty is unverified creator metadata. The contract validates Latin-square correctness on submission but does not independently grade or rank difficulty - the difficulty field is taken from the creator at face value.
  • No ranked leaderboard or rewards. A per-puzzle solver count is tracked, but the example intentionally omits scoring, ranked leaderboards, and reward distribution to keep the reference focused on the ECS + auth + ZK pattern.
  • Puzzle catalog grows without an on-chain cap. Submissions increment PUZZLE_COUNT without an upper bound. Reads are paginated via offset/limit, but production deployments expecting very large catalogs should add submission limits and indexed lookups.

Classification

Marked as Canonical. Murdoku is the reference full-stack example for Cougr, showcasing how to build games that combine ECS, session-key-based social logins (via Pollar), and multi-step transaction authorization. Use this as the template for production-grade full-stack game contracts.

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

Pokemon Mini On-Chain Game

Other 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 Pokemon-style mini game implemented as a Soroban smart contract using the cougr-core ECS framework on the Stellar blockchain.

Overview

This example demonstrates how to build deterministic on-chain game logic with:

FeatureDescription
Tile Map8x8 grid with deterministic tile placement
MovementGrid-based with collision detection
EncountersDeterministic triggering on TallGrass tiles
BattleTurn-based 1v1 combat system

Why cougr-core?

AspectBenefit
ComponentTraitStandardized serialization for on-chain storage
Type SafetyUnique Symbol identifiers per component
Storage OptimizationTable vs Sparse storage strategies
Code ReusabilityProven ECS patterns

Prerequisites

ToolInstallation
Rustcurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
WASM Targetrustup target add wasm32v1-none
Stellar CLIbrew install stellar-cli (macOS)

Quick Start

Build

cd examples/pokemon_mini

# Development build
cargo build

# Build WASM contract
stellar contract build

Test

cargo test

Lint

cargo fmt --check
cargo clippy -- -D warnings

Contract Functions

Initialization

FunctionDescription
init_player()Create player at spawn (1,1) with starter creature

Movement

FunctionParametersReturnsDescription
move_playerdirection: u32u32Move player (0=blocked, 1=moved, 2=encounter)
get_tilex, y: i32u32Get tile type at position
get_map_size-(i32, i32)Returns (8, 8)

Direction Values: 0=Up, 1=Down, 2=Left, 3=Right

State Queries

FunctionReturnsDescription
get_player_state(x, y, moves, in_battle, hp)Player position and status
get_creature_stats(species, level, hp, max_hp, atk, def)Creature stats
get_battle_stats(wins, losses, escapes)Battle history

Combat

FunctionParametersReturnsDescription
get_battle_state-(in_battle, player_hp, enemy_hp, turn, result)Current battle
battle_actionaction: u32u32Execute action (0=invalid, 1=continue, 2=ended)

Action Values: 0=Attack, 1=Defend, 2=Run

Result Values: 0=None, 1=Win, 2=Lose, 3=Escaped


Map Layout

8x8 Grid:
+--------+
|WWWWWWWW|  W = Wall
|WS..WTTW|  S = Spawn
|W...WTTW|  T = TallGrass
|W..T.TTW|  . = Grass
|W......W|  ~ = Water
|WTTT.~~W|
|WTTT.~~W|
|WWWWWWWW|
+--------+

Game Mechanics

Encounter System (Deterministic)

Encounters trigger on TallGrass when: (x + y + move_count) % 5 == 0

Battle Damage Formula

damage = max(1, attacker_atk - defender_def)
  • Player always acts first
  • Defending adds +3 to defense for that turn
  • Winning heals creature fully

Deployed Contract (Testnet)

Contract ID: <CONTRACT_ID>

NetworkStatusExplorer
Stellar Testnetโœ… LiveView on Stellar Lab

Deployment

Testnet

# Generate keypair
stellar keys generate --global alice --network testnet

# Fund account
# Visit: https://friendbot.stellar.org/?addr=<YOUR_ADDRESS>

# Deploy
stellar contract deploy \
  --wasm target/wasm32v1-none/release/pokemon_mini.wasm \
  --source alice \
  --network testnet

Playing

# Use the deployed contract
CONTRACT_ID="<CONTRACT_ID>"

# Initialize player
stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- init_player

# Get player state (returns: [x, y, moves, in_battle, hp])
stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- get_player_state

# Move right (direction: 0=Up, 1=Down, 2=Left, 3=Right)
stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- move_player --direction 3

# Attack in battle (action: 0=Attack, 1=Defend, 2=Run)
stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- battle_action --action 0

Project Structure

examples/pokemon_mini/
โ”œโ”€โ”€ Cargo.toml          # Dependencies (cougr-core, soroban-sdk)
โ”œโ”€โ”€ README.md           # This documentation
โ”œโ”€โ”€ .gitignore          # Ignore rules
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ lib.rs          # Contract entry points + tests
    โ”œโ”€โ”€ components.rs   # ECS components using ComponentTrait
    โ”œโ”€โ”€ systems.rs      # Game logic (map, movement, battle)
    โ””โ”€โ”€ simple_world.rs # Entity-component storage

References

ResourceLink
Soroban Docsdevelopers.stellar.org
Stellar CLICLI Documentation
Cougr Repositorygithub.com/salazarsebas/Cougr

License

Part of the Cougr project. See main repository for license information.

Pong On-Chain Game

Arcade Transitional
game-appecs

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

Purpose and pattern

This example demonstrates a two-player paddle physics loop on Soroban with Cougr ECS concepts. It remains transitional while the arcade examples converge on the canonical snake GameApp architecture.

Public contract API

FunctionParametersReturn typeDescription
init_gamenoneGameStateInitializes paddles, ball, score, and active flag.
move_paddleplayer: u32, direction: i32GameStateMoves a paddle and returns state.
update_ticknoneGameStateAdvances ball physics, collisions, scoring, and win checks.
get_game_statenoneGameStateReturns current state.
reset_gamenoneGameStateResets the game to the initial state.

Architecture overview

contract entrypoint
  โ”œโ”€ reads game state from Soroban storage
  โ”œโ”€ applies input or tick systems
  โ””โ”€ writes updated state back to storage

Paddle, ball, and score components are in components.rs; paddle and ball systems are in systems.rs.

Storage model

Storage classDataWhy
Instance storagePer-contract game state where used by this example.Keeps small arcade state close to the contract instance.
Persistent storagePlayer- or world-scoped state where the example needs durable keyed state.Keeps game progress available across invocations.
Temporary storageNot used.The examples favor deterministic recalculation over ephemeral caches.

Main gameplay flow

  1. Call the initialization function to create the starting state.
  2. Submit an input action such as movement, jump, flap, rotation, or shoot.
  3. Call the tick/update function to run deterministic simulation logic.
  4. Query public getters for score, position, active state, or terminal status.
  5. Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.

Cougr APIs used

  1. Deploy the contract:
    stellar contract deploy \
      --wasm target/wasm32v1-none/release/pong.wasm \
      --source <your-secret-key> \
      --network <NETWORK>
    
  • ComponentTrait and custom component modules document the ECS data boundary.
  • SimpleWorld, SimpleQueryBuilder, GameApp, ScheduleStage, or SystemConfig are used where this transitional example has already adopted the maintained runtime shape.
  • Auth, privacy, ZK, and standards APIs are intentionally not used; these arcade examples focus on deterministic game logic.

Build and test commands


cargo test
stellar contract build

# Initialize a new game
stellar contract invoke \
  --id <contract-id> \
  --network <NETWORK> \
  -- init_game

# Move Player 1's paddle up
stellar contract invoke \
  --id <contract-id> \
  --network <NETWORK> \
  -- move_paddle --player 1 --direction -1

# Update game tick
stellar contract invoke \
  --id <contract-id> \
  --network <NETWORK> \
  -- update_tick

# Get current game state
stellar contract invoke \
  --id <contract-id> \
  --network <NETWORK> \
  -- get_game_state

Deployment Results

Test Account: GA5VOXGSGDQBIY7W2UJ2GD23V3566NA7OF4YIL4QCFAVM3PGN7QQQHZA

Test Invocations:

  1. Initialize Game:

    stellar contract invoke --id <CONTRACT_ID> --source pong-test --network <NETWORK> -- init_game
    

    Result: โœ… Success

    {
      "ball_vx": 1,
      "ball_vy": 1,
      "ball_x": 50,
      "ball_y": 30,
      "game_active": true,
      "player1_paddle_y": 30,
      "player1_score": 0,
      "player2_paddle_y": 30,
      "player2_score": 0
    }
    
  2. Move Paddle (Player 1 up):

    stellar contract invoke --id <CONTRACT_ID> --source pong-test --network <NETWORK> -- move_paddle --player 1 --direction -1
    

    Result: โœ… Success - Paddle moved from y=30 to y=28

    {
      "player1_paddle_y": 28,
      "player2_paddle_y": 30,
      ...
    }
    
  3. Update Tick (Physics simulation):

    stellar contract invoke --id <CONTRACT_ID> --source pong-test --network <NETWORK> -- update_tick
    

    Result: โœ… Success - Ball moved from (50,30) to (51,31)

    {
      "ball_x": 51,
      "ball_y": 31,
      ...
    }
    

Deployment Date: January 23, 2026

Transaction Hashes:

Architecture

This contract demonstrates the Cougr-Core ECS (Entity Component System) pattern adapted for Soroban smart contracts:

ECS Components

The game state is organized using Cougr-Core's component pattern:

  • PaddleComponent: Represents paddle entities with player ID and Y position
  • BallComponent: Represents the ball entity with position (x, y) and velocity (vx, vy)
  • ScoreComponent: Represents the score entity with both players' scores and game status
#![allow(unused)]
fn main() {
#[contracttype]
pub struct PaddleComponent {
    pub player_id: u32,
    pub y_position: i32,
}

#[contracttype]
pub struct BallComponent {
    pub x: i32,
    pub y: i32,
    pub vx: i32,
    pub vy: i32,
}

#[contracttype]
pub struct ScoreComponent {
    pub player1_score: u32,
    pub player2_score: u32,
    pub game_active: bool,
}
}

ECS Systems

Game logic is organized into systems following Cougr-Core's system pattern:

  1. PhysicsSystem: Updates ball position based on velocity

    #![allow(unused)]
    fn main() {
    fn physics_system(world: &mut ECSWorldState) {
        world.ball.x += world.ball.vx;
        world.ball.y += world.ball.vy;
    }
    }
  2. CollisionSystem: Handles wall and paddle collision detection

    • Detects wall bounces (top/bottom)
    • Detects paddle hits (left/right)
    • Updates ball velocity on collision
  3. ScoringSystem: Manages scoring and win conditions

    • Awards points when ball passes paddles
    • Checks for game over (first to 5 points)
    • Resets ball position after scoring

Storage Strategy

  • ECSWorldState: Serializable structure containing all game components
  • Soroban Instance Storage: Persists the ECS world between contract invocations
  • Component Pattern: Each game object (paddles, ball, score) is represented as a component
  • System Pattern: Game logic is organized into discrete, reusable systems

Why This Approach?

This implementation demonstrates how Cougr-Core's ECS architecture can be adapted for blockchain constraints:

  1. Component Organization: Clear separation of data (components) from logic (systems)
  2. Scalability: Easy to add new components or systems for game features
  3. Testability: Systems can be tested independently
  4. Soroban Compatibility: ECS patterns adapted to work with Soroban's storage model

For more complex games with many entities, the full Cougr-Core ECS framework provides additional features like entity queries, component archetypes, and parallel system execution.

Code Structure

examples/pong/
โ”œโ”€โ”€ Cargo.toml          # Dependencies and build configuration
โ”œโ”€โ”€ README.md           # This file
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ lib.rs          # Main contract implementation
    โ””โ”€โ”€ test.rs         # Comprehensive test suite

Troubleshooting

Build Errors

Error: can't find crate for 'core'

rustup target add wasm32v1-none

Error: Rust version too old

rustup update

Test Failures

If tests fail, ensure you're using the correct Rust version:

rustc --version  # Should be 1.89.0 or newer

Deployment Issues

Network errors: Use --simulate flag first to test without deploying:

stellar contract invoke --id <contract-id> --network <NETWORK> --simulate -- init_game

Known limitations

  • Transitional code may preserve older storage or scheduling patterns for compatibility reference.
  • No authentication, matchmaking, real-time rendering, or production randomness is included.
  • One contract instance generally represents one game or one keyed set of player games.
  • For new work, prefer the canonical snake module split and GameApp tick wiring.

Proof of Hunt (Stellar + Soroban)

Hidden Information Transitional
authzk-circuitsprivacy

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

A hidden-map treasure discovery game demonstrating zero-knowledge proof-backed exploration on Stellar Soroban. This example combines:

  • Hidden world state committed off-chain via Merkle roots
  • Proof-backed exploration using Groth16 verification and BN254 pairing checks
  • Premium actions modeled for x402 settlement flows
  • Nullifier-based replay prevention

Status

Transitional - uses cougr-core = "1.1.0" and Stellar-zk style Groth16 verification. See battleship for the canonical commit-reveal reference, and hidden_hand for the canonical ZK circuits reference.

Why This Is Stellar-Specific

This example demonstrates three Stellar-native patterns working together:

  1. Soroban contract state for deterministic gameplay and progression
  2. Stellar-zk style Groth16 verifier flow on-chain using Soroban BN254 pairing checks
  3. x402-style premium action credits represented as settled payment units before hint consumption

References:

  • https://crates.io/crates/stellar-zk
  • https://github.com/salazarsebas/stellar-zk
  • https://developers.stellar.org/docs/build/apps/x402

When to Use Commit-Reveal vs ZK Circuits

Use commit-reveal with Merkle proofs (e.g., battleship, treasure_hunt) when you need to hide state and reveal it incrementally with verifiable proofs. This is simpler and sufficient for most hidden-information games.

Use ZK circuits (this example and hidden_hand) when:

  • The game logic itself must remain private (e.g., proving a valid move without revealing it)
  • You need constant-size proofs regardless of map size
  • You want nullifier-based replay prevention without exposing cell indices

Hidden State and Proof Flow

Off-chain committed data

The hidden map is represented off-chain and committed by root hash:

  • Map commitment root (BytesN<32>)
  • Map dimensions (width, height)
  • Implicit treasure distribution encoded in proof public inputs

What is proven on-chain

For each exploration:

  • (x, y) belongs to a valid proof statement bound to the same commitment root
  • Leaf + sibling path resolves to the committed root
  • Groth16 proof verifies through BN254 pairing checks
  • Nullifier has not been replayed

Anti-cheat and privacy properties

  • Players cannot claim arbitrary discoveries because proof public inputs are tied to coordinates and root
  • Replay is blocked by nullifier storage
  • Full hidden map remains off-chain; only commitment and selective proof metadata are revealed

Contract API

Public Functions

FunctionParametersDescription
init_gameplayer: Address, map_commitment: BytesN<32>, width: u32, height: u32Initialize game with committed map
exploreplayer: Address, x: u32, y: u32, proof: ProofInputExplore a cell with ZK proof
purchase_hintplayer: Address, hint_type: u32Buy hint (0) or scan (1) using credits
get_state-Return GameState
is_finished-Check if game is won or lost
set_verification_keyowner: Address, vk_bytes: BytesSet Groth16 verification key
credit_x402_paymentowner: Address, player: Address, units: u32, receipt_hash: BytesN<32>Credit x402 payment units

Data Types

#![allow(unused)]
fn main() {
enum GameStatus { Active, Won, Lost }

struct ProofInput {
    proof: BytesN<256>,
    public_inputs: Bytes,
    nullifier: BytesN<32>,
    leaf_hash: BytesN<32>,
    sibling_hash: BytesN<32>,
    sibling_on_left: bool,
}

struct PlayerState {
    position_x: u32,
    position_y: u32,
    score: i128,
    health: u32,
    discoveries: u32,
}

struct GameState {
    player: Address,
    map_commitment: BytesN<32>,
    width: u32,
    height: u32,
    treasure_count: u32,
    discovered_cells: u32,
    status: GameStatus,
    player_state: PlayerState,
    hint_usage: HintUsage,
    x402_credits: u32,
}
}

Storage Model

Committed State

Stored on-chain during initialization:

  • map_commitment - Merkle root of the hidden map
  • Map dimensions (width, height)
  • Derived treasure_count = max(1, (width * height) / 8)
  • Verification key for Groth16 proofs

Revealed State

Updated during exploration:

  • player_state - position, score, health, discoveries
  • discovered_cells - count of explored cells
  • explored_cell(cell_idx) - persistent storage for replay prevention
  • Game status (Active/Won/Lost)

Proven State

The explore function performs multi-layer verification:

  1. Public input validation: Checks that (x, y) in proof inputs match claimed coordinates
  2. Merkle path verification: Reconstructs root from leaf + sibling path
  3. Groth16 verification: Verifies BN254 pairing check via env.crypto().bn254().pairing_check()
  4. Nullifier check: Rejects if nullifier was already used (replay prevention)

x402 Premium Action Model

purchase_hint(player, hint_type) consumes pre-settled premium credits.

This maps to an x402 backend flow where a payment gateway verifies and settles payment off-chain, then credits the user in-contract via credit_x402_payment(...).

  • hint_type = 0: hint action (cost 1 credit)
  • hint_type = 1: scan action (cost 2 credits)

Building & Testing

Prerequisites

  • Rust 1.88.0+
  • Stellar CLI 25.0.0+ (optional)

Build

cargo build
cargo build --release --target wasm32v1-none

Test

cargo test

Test Coverage:

  • โœ… Game initialization
  • โœ… Valid exploration proof acceptance
  • โœ… Invalid proof rejection
  • โœ… Progression updates after valid exploration
  • โœ… Premium hint action flow (x402 credits)
  • โœ… Game completion (win condition)
  • โœ… Failure condition (health depletion)
  • โœ… Nullifier replay prevention
  • โœ… Coordinate bounds validation
  • โœ… Timeout and claim logic

Security Considerations

โœ… Secure

  • Proof-backed exploration: Cannot claim arbitrary discoveries without valid Groth16 proof
  • Nullifier replay prevention: Each cell can only be explored once
  • BN254 pairing verification: Cryptographic proof of correct computation
  • Public input binding: Coordinates are bound to proof statement

โš ๏ธ Important

  • Zero-proof test path: CI tests use deterministic zero-proof path (#[cfg(test)]) - production requires real Groth16 proofs
  • Verification key: Must be set correctly before exploration; invalid VK causes verification failures

Deployment

stellar keys generate proof-of-hunt-deployer --network testnet --fund
stellar contract deploy \
  --wasm target/wasm32v1-none/release/proof_of_hunt.wasm \
  --source proof-of-hunt-deployer \
  --network testnet

Resources

License

MIT OR Apache-2.0

Reversi

Board Transitional
authecs

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

An on-chain Reversi (Othello) game built with the Cougr ECS framework on Stellar Soroban.

Purpose and pattern

This example demonstrates a turn-based, perfect-information board game where every move mutates shared board state and can trigger automatic turn-skipping. It showcases Cougr's ComponentTrait pattern for typed, byte-serializable game state - components are plain Rust structs annotated with #[contracttype] that implement ComponentTrait directly, rather than being scanned through SimpleWorld/SimpleQueryBuilder. This is a lighter-weight integration suited to a single fixed-shape game state (one board, two players) with no dynamic entity population.

Public contract API

FunctionParametersReturnsDescription
init_gameplayer_one: Address, player_two: Address-Initializes an 8ร—8 board with the standard opening position; player_one plays Black and moves first. Panics if already initialized.
submit_moveplayer: Address, row: u32, col: u32-Validates and applies a move (requires player.require_auth()), flips bracketed opponent pieces, recomputes score, and advances turn/pass state. Panics on illegal move, wrong turn, or finished game.
get_state-GameStateCurrent player, pass-count state, and active/finished status.
get_board-BoardStateRaw 64-cell board array (row-major, 0=empty/1=black/2=white), plus width/height.
get_score-ScoreStateBlack/white piece counts and winner (0=ongoing, 1=black, 2=white, 3=draw).

Architecture overview

There is no GameApp tick loop - submit_move runs all systems synchronously in a fixed pipeline on each invocation:

submit_move
  โ””โ”€ MoveValidationSystem   โ†’ rejects occupied cells or moves that flip nothing
  โ””โ”€ FlipResolutionSystem   โ†’ places the piece, flips bracketed pieces in all 8 directions
  โ””โ”€ ScoringSystem          โ†’ recomputes black/white piece counts from the board
  โ””โ”€ TurnSystem/PassSystem  โ†’ advances to the opponent, or auto-passes if they have no legal move
  โ””โ”€ EndConditionSystem     โ†’ marks the game finished when both players pass or the board is full

Each system is a pure function over components.rs types (BoardComponent, TurnComponent, GameStatusComponent, ScoreComponent); lib.rs owns the single ECSWorldState aggregate and is the only place that touches contract storage.

Storage model

The entire game lives under one instance-storage key (WORLD_KEY, symbol_short!("WORLD")) as a single ECSWorldState struct bundling the board, turn, status, score, and both player addresses. Instance storage is used - not persistent or temporary - because there is exactly one game per contract instance and the state must survive for the lifetime of that instance with no per-entry TTL management.

Main gameplay flow

  1. Deployer calls init_game(player_one, player_two); board is seeded with the four-piece opening position, Black (player_one) to move, status active.
  2. Black calls submit_move(player, row, col). The contract checks turn order and move legality (the move must bracket at least one opponent piece in a straight line).
  3. On a legal move: the piece is placed, bracketed opponent pieces flip, score is recomputed, and turn passes to White - unless White has no legal move, in which case Black continues and pass_count is set to 1.
  4. Players alternate submit_move calls. If neither player has a legal move (pass_count reaches 2) or the board fills, EndConditionSystem sets status to finished.
  5. Either player calls get_score to read the final piece counts and winner.

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives each component (BoardComponent, TurnComponent, GameStatusComponent, ScoreComponent) a component_type() symbol and byte-level serialize/deserialize, chosen here because the game has a single fixed set of components per instance rather than a dynamic entity population that would benefit from SimpleWorld/SimpleQueryBuilder scanning.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp, ScheduleStage, or SimpleWorld - game logic is invoked directly from contract entrypoints rather than through a tick-based scheduler, since a turn-based game with one decision point per call does not need staged scheduling.
  • No timeout/forfeit mechanism for an unresponsive player.
  • No spectator or replay API beyond the three read-only getters.

Rock Paper Scissors with Commit-Reveal

Other Transitional
authprivacycommit-revealecs

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

A two-player Rock Paper Scissors game demonstrating the commit-reveal pattern using cryptographic hashing on Stellar Soroban. This is the simplest example of hidden information in the Cougr framework.

Status

Transitional - uses cougr-core = "1.1.0" with partial migration to impl_component! macros. See battleship for the canonical hidden-information reference.

What is Commit-Reveal?

The commit-reveal pattern solves a fundamental problem in blockchain games: how can players make simultaneous secret choices when all transactions are public?

The Problem

In a naive implementation:

  1. Player A submits "Rock" โ†’ visible on-chain
  2. Player B sees A's choice, submits "Paper" โ†’ B always wins!

The Solution: Commit-Reveal

COMMIT PHASE (hide choices)
โ”œโ”€ Player A: hash(Rock + secret_salt) โ†’ 0x7a3f...
โ”œโ”€ Player B: hash(Paper + secret_salt) โ†’ 0x9b2e...
โ””โ”€ Both hashes stored on-chain (choices hidden)

REVEAL PHASE (prove choices)
โ”œโ”€ Player A: reveals (Rock, secret_salt)
โ”œโ”€ Contract: verify hash(Rock + secret_salt) == 0x7a3f... โœ“
โ”œโ”€ Player B: reveals (Paper, secret_salt)
โ”œโ”€ Contract: verify hash(Paper + secret_salt) == 0x9b2e... โœ“
โ””โ”€ Contract: compare choices โ†’ B wins!

Key Properties:

  • โœ… Binding: Can't change choice after committing (hash locks it in)
  • โœ… Hiding: Opponent can't see choice until reveal
  • โœ… Order-independent: Neither player gains advantage by going first/second

When to Use Commit-Reveal vs ZK Circuits

Use commit-reveal (this pattern) when you need to hide simple choices that are later revealed and verified by recomputing a hash. It's simple, efficient, and doesn't require external proving systems.

Use ZK circuits when the game logic itself must remain private - for example, proving that a move is valid without revealing what the move is. See hidden_hand for a canonical ZK circuit-based example using Groth16 proofs.

Game Flow

1. Initialize Match

#![allow(unused)]
fn main() {
new_match(player_a, player_b, best_of: 3)
}

Creates a best-of-N match (1, 3, 5, etc.)

2. Commit Phase

Both players compute and submit hashes:

#![allow(unused)]
fn main() {
// Off-chain: Player A
let salt = random_32_bytes();
let hash = sha256(choice || salt);

// On-chain
commit(player_a, hash)
}

When both players commit โ†’ automatically transitions to Reveal phase

3. Reveal Phase

Players reveal their choices with salts:

#![allow(unused)]
fn main() {
reveal(player_a, choice, salt)
}

Contract verifies: sha256(choice || salt) == stored_hash

When both players reveal โ†’ automatically resolves round

4. Resolution

Rock > Scissors
Scissors > Paper
Paper > Rock
Same choice = Draw

Updates scoreboard, checks if match winner determined (best-of-N), or starts next round.

5. Timeout Protection

If a player refuses to reveal after committing:

#![allow(unused)]
fn main() {
claim_timeout(honest_player)
}

After 100 ledgers, the honest player who revealed wins by forfeit.

Contract API

Public Functions

FunctionParametersDescription
new_matchplayer_a: Address, player_b: Address, best_of: u32Initialize a new match
commitplayer: Address, hash: BytesN<32>Submit commitment hash
revealplayer: Address, choice: u32, salt: BytesN<32>Reveal choice (0=Rock, 1=Paper, 2=Scissors)
claim_timeoutplayer: AddressClaim win if opponent doesn't reveal after 100 ledgers
get_state-Get current MatchState
get_score-Get ScoreBoard

Data Types

#![allow(unused)]
fn main() {
enum Choice { Rock = 0, Paper = 1, Scissors = 2 }
enum Phase { Committing, Revealing, Resolved }

struct MatchState {
    phase: Phase,
    winner: Option<Address>,
    round: u32,
}

struct ScoreBoard {
    wins_a: u32,
    wins_b: u32,
    draws: u32,
    best_of: u32,
}
}

Storage Model

Committed State

Stored on-chain during the commit phase:

  • hash_a/b - SHA256 hash of each player's choice + random salt
  • has_commit_a/b - flags tracking which players have committed
  • commit_ledger - ledger sequence when both players committed (for timeout calculation)

Both commitments must be submitted before the game transitions to Phase::Revealing.

Revealed State

Updated during the reveal phase:

  • revealed_a/b - flags tracking which players have revealed
  • choice_a/b - actual numeric choices (0, 1, or 2)
  • scoreboard - accumulated wins/draws across rounds

Proven State

The reveal function verifies that disclosed choices match the original commitment by recomputing SHA256(choice || salt) and comparing it to the stored hash.

Component Serialization

PlayerCommitment uses the impl_component! macro for standardized serialization:

#![allow(unused)]
fn main() {
impl_component!(PlayerCommitment, "commit", Table, {
    hash: bytes32,
    revealed: bool
});
}

MatchState implements ComponentTrait manually since its Phase enum and Option<Address> fields don't map cleanly to the macro's fixed-size type system.

Building & Testing

Prerequisites

  • Rust 1.88.0+
  • Stellar CLI 25.0.0+ (optional)

Build

cargo build
cargo build --release --target wasm32v1-none

Test

cargo test

Test Coverage:

  • โœ… Match initialization
  • โœ… Commit phase transitions
  • โœ… All 9 choice combinations (RR, RP, RS, PR, PP, PS, SR, SP, SS)
  • โœ… Hash mismatch rejection
  • โœ… Best-of-3 match flow
  • โœ… Double commit prevention
  • โœ… Premature reveal prevention
  • โœ… Component trait serialization (via impl_component!)

Security Considerations

Secure

  • Commitment binding: Hash function is collision-resistant
  • Choice hiding: Preimage resistance prevents guessing
  • Replay protection: Each round requires new commitments
  • Timeout protection: Prevents griefing by non-revealing players

๏ธ Important

  • Salt randomness: Use cryptographically secure random salts (32 bytes)

  • Salt uniqueness: Never reuse salts across rounds

  • Timeout value: 100 ledgers (~8 minutes on Stellar) - adjust for your needs

Best Practices

#![allow(unused)]
fn main() {
// โœ… Good: Random salt per round
let salt = generate_random_bytes(32);

// โŒ Bad: Predictable salt
let salt = BytesN::from_array(&env, &[0u8; 32]);

// โŒ Bad: Reused salt
let salt = player_address.to_bytes();
}

ECS Architecture

Components

ComponentFieldsPurpose
PlayerCommitmenthash: BytesN<32>
revealed: bool
Stores commitment hash
MatchStatephase: Phase
winner: Option<Address>
round: u32
Game phase tracking
ScoreBoardwins_a: u32
wins_b: u32
draws: u32
best_of: u32
Match scoring

All components implement cougr_core::component::ComponentTrait for type-safe serialization.

Systems

SystemResponsibility
CommitSystemAccepts hashes, transitions to reveal when both committed
RevealSystemVerifies `sha256(choice
ResolveSystemCompares choices, updates scoreboard
MatchSystemChecks best-of-N threshold, declares winner or starts next round

Deployment


stellar keys generate rps-deployer --network testnet --fund

# Generate funded account
stellar keys generate rps-deployer --network <NETWORK> --fund

# Build contract
cargo build --release --target wasm32v1-none

# Deploy

stellar contract deploy \
  --wasm target/wasm32v1-none/release/rock_paper_scissors.wasm \
  --source rps-deployer \
  --network <NETWORK>

Play a Game

CONTRACT_ID=<your_contract_id>

# Initialize match
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  -- new_match \
  --player_a <PLAYER_A_ADDRESS> \
  --player_b <PLAYER_B_ADDRESS> \
  --best_of 3

# Player A commits (compute hash off-chain first)
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  --source player-a \
  -- commit \
  --player <PLAYER_A_ADDRESS> \
  --hash <HASH_BYTES>

# Player B commits
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  --source player-b \
  -- commit \
  --player <PLAYER_B_ADDRESS> \
  --hash <HASH_BYTES>

# Player A reveals
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  --source player-a \
  -- reveal \
  --player <PLAYER_A_ADDRESS> \
  --choice 0 \
  --salt <SALT_BYTES>

# Player B reveals
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  --source player-b \
  -- reveal \
  --player <PLAYER_B_ADDRESS> \
  --choice 1 \
  --salt <SALT_BYTES>

# Check results
stellar contract invoke \
  --id $CONTRACT_ID \
  --network <NETWORK> \
  -- get_score

Learning Path

This example is the entry point for understanding Cougr's cryptographic primitives:

  1. Start here: Commit-reveal with SHA256 (this example)
  2. Next: Upgrade to Poseidon2 hashing (ZK-friendly)
  3. Advanced: Full ZK proofs with circuits (see examples/chess/)

Resources

License

MIT OR Apache-2.0

Season Ladder

Other Canonical
standards

Purpose and pattern

season_ladder runs a competitive season of head-to-head results: players register themselves, the season admin records each fixture, and an integer rating is updated after every result. It demonstrates the standards layer of cougr-core โ€” Ownable for admin authority and Pausable for an emergency stop โ€” instead of a private admin flag, and it shows integer-only rating math that stays deterministic inside a Soroban contract.

Public contract API

FunctionParametersReturnsDescription
initializeadmin: AddressAddressOne-time setup; binds the season admin through Ownable.
register_playerplayer: AddressPlayerRecordSelf-registration at the starting rating. Frozen while paused.
report_resultreporter, home, away: Address, match_no: u32, home_goals, away_goals: u32MatchResultOwner-only; records fixture match_no for the ordered pair and updates both ratings. Frozen while paused.
pauseadmin: Addressโ€“Owner-only emergency stop.
unpauseadmin: Addressโ€“Owner-only resume.
is_pausedโ€“boolCurrent pause state.
ownerโ€“Option<Address>Current season admin.
playerid: AddressPlayerRecordFull record (rating, matches, W/D/L).
ratingid: Addressi64Convenience view of a player's rating.
matches_playedhome, away: Addressu32Fixtures already recorded for the ordered pair.
fixturehome, away: Address, match_no: u32MatchResultA stored result.
standingsโ€“Vec<(Address, i64)>Ladder ordered by rating, highest first.

Architecture overview

The contract is intentionally monolithic (it has three small systems, well under the threshold where the standard asks for a split):

  • Standards layer โ€” Ownable and Pausable from cougr_core::standards own the authorization of report_result / pause / unpause and the freeze of register_player / report_result.
  • Ratings โ€” pure integer functions (expected_score_ppm, rating_delta, score_ppm) convert a result into a mirrored rating swing.
  • Storage โ€” player records, per-pair fixture counters, stored fixtures, and the roster all live in instance storage.

There is no tick loop: every entrypoint is a direct call driven by players (self-registration) and the admin (results, pause).

Storage model

KeyStorageWhy
Player(Address)instanceSmall example; ratings and W/D/L are read on every result.
PairMatches(Address, Address)instanceNext fixture number per ordered pair; enforces the duplicate guard.
Fixture(Address, Address, u32)instanceEach reported result, round-trippable through the fixture view.
RosterinstanceRegistration order, feeds standings.
Ownable / Pausable statepersistent (standards layer)Written by cougr_core::standards under its own namespaced keys.

Rating rule (integer-only)

  • Start rating: 1000. K-factor: 32.
  • Expected score for a player against an opponent, in parts-per-million: expected_ppm = 1_000_000 * rating / (rating + opponent) (integer division, truncated).
  • Result score: win 1_000_000, draw 500_000, loss 0.
  • Swing: delta = (32 * (score_ppm - expected_ppm)) / 1_000_000, truncated toward zero. The winner's swing is applied to the home side and mirrored (-delta) to the away side, so the total rating is conserved unless a floor clamp triggers.
  • Ratings are clamped to a floor of 100 so expected-score math always has a positive denominator.

Consequences that the tests pin down: an even first match pays ยฑ16; a favourite's win shrinks as the gap grows (15, 14, โ€ฆ); an upset win by the lower-rated side pays more than the even-rating 16; draws between even players change nothing.

What pause freezes

While paused (Pausable):

  • register_player is rejected,
  • report_result is rejected,
  • views (player, standings, fixture, โ€ฆ) keep working read-only,
  • only the Ownable owner can pause/unpause.

After unpause, reporting continues with the existing fixture counters โ€” the pause never renumbers or discards fixtures.

Main gameplay flow

  1. Deploy, then initialize(admin) โ€” binds the owner.
  2. Players call register_player themselves (starting rating 1000).
  3. The admin records fixtures in order: report_result(admin, home, away, 1, 2, 1) โ€” ratings update immediately.
  4. Repeat with match_no 2, 3, โ€ฆ for the same pair; duplicates or skipped numbers are rejected.
  5. If something is wrong, the admin calls pause; the season freezes. unpause resumes exactly where it left off.
  6. standings shows the ladder at any time.

Cougr APIs used

  • cougr_core::standards::Ownable โ€” initialize, require_owner, owner.
  • cougr_core::standards::Pausable โ€” require_not_paused, pause, unpause, is_paused.
  • cougr_core::standards::StandardsError โ€” surfaced through the standards layer's Results.

No ECS, GameApp, ZK, or session APIs are used: the issue this example answers scopes it to results, ratings, and the standards layer.

Build and test commands

cd examples/season_ladder
cargo test
stellar contract build

Known limitations

  • Fixtures are keyed by the ordered pair (home, away); a rematch with the sides swapped starts a new fixture counter.
  • match_no must be reported in order (1, 2, 3, โ€ฆ) per pair.
  • Everything lives in instance storage; a real season would move player records to persistent storage with TTLs.
  • The rating floor (100) can shrink a mirrored delta, so total-rating conservation holds only when no clamp triggers.
  • No tokens, wagers, or matchmaking: ratings are the only stake.

session_arena

Other Canonical
soroban-gamesession-authgame-app

feat/session-manager-fresh Canonical Cougr example for the SessionManager gameplay lifecycle: approve once, play frictionlessly, renew on expiry, fallback when stale.

Related example: For the full mobile-first flow with passkey auth, combos, and rounds, see tap_battle.

Purpose and pattern

session_arena is the minimal reference implementation of cougr-core session UX. It strips away game mechanics so you can focus on:

  1. Approve - owner signs once to create a scoped session key
  2. Play - many tap calls without wallet prompts
  3. Renew - extend session before expiry (owner re-approves)
  4. Fallback - continue playing via direct owner auth when the session expires

Session lifecycle

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ approve_     โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ tap (many    โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ renew_       โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ fallback_    โ”‚
โ”‚ session      โ”‚     โ”‚ times)       โ”‚     โ”‚ session      โ”‚     โ”‚ tap          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  owner auth           session key           owner auth          session or owner
  creates scope        no wallet prompt      extends expiry      auth fallback

Duration semantics: expires_in and expires_at use ledger timestamps (seconds). SessionBuilder::expires_in(n) sets expiry to ledger.timestamp() + n.

Public contract API

FunctionParametersReturnsDescription
approve_sessionowner, max_taps, expires_inActiveSessionOne-time owner approval creating a scoped session
tapowner, key_idu32Gameplay action via active session (no wallet prompt)
renew_sessionowner, key_id, expires_inActiveSessionExtend session lifetime (owner must re-approve)
fallback_tapowner, key_idu32Tap via session first, fall back to direct owner auth
scoreowneru32Current tap count for the owner

Cougr APIs used

  • SessionBuilder - declare allowed actions, max operations, and expiry
  • SessionManager::approve - create scoped session after owner auth
  • SessionManager::execute_action - gasless gameplay via session key
  • SessionManager::status - poll remaining ops and renewal hints
  • SessionManager::renew - extend absolute expires_at timestamp
  • SessionManager::fallback_execute - session-first with direct-auth fallback
  • SessionStorage - load session keys by owner and key ID
  • MockSession (testutils) - helper for unit tests

Build and test

Canonical example demonstrating SessionManager session lifecycles, scoped session keys, and fallback authorization.

Purpose and pattern

This example showcases the onboarding pattern for friction-free session keys on Soroban. A player approves a session key once, enabling them to play without wallet confirmation prompts for subsequent transactions. If the session expires, the client can fall back to direct owner authentication.

Public contract API

FunctionParametersReturnsDescription
approve_sessionowner: Address, max_taps: u32, expires_in: u64ActiveSessionApproves a new session key with specific action scopes and expiration constraints.
tapowner: Address, key_id: BytesN<32>u32Increments the player's score, verified using the active session key (no wallet prompt).
renew_sessionowner: Address, key_id: BytesN<32>, expires_in: u64ActiveSessionExtends the active session key expiration window (requires owner wallet authorization).
fallback_tapowner: Address, key_id: BytesN<32>u32Performs a tap action, falling back to direct owner wallet authorization if the session key is expired.
scoreowner: Addressu32Retrieves the current tap score of the player.

Architecture overview

                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                        โ”‚   Player Owner   โ”‚
                        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                 โ”‚ Approves
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚    Active Session     โ”‚
                     โ”‚ (Temporary Key File)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                 โ”‚ Authorizes (No Prompts)
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚     SessionArena      โ”‚
                     โ”‚  (Soroban Contract)   โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                 โ”‚ Updates
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚         Score         โ”‚
                     โ”‚      (Component)      โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The game utilizes Cougr's session authentication module. The owner delegates authority for a specified duration and subset of actions to a local keypair. The contract verifies each transaction signature against the delegated session key metadata.

Storage model

Session parameters and player score components are stored in Instance Storage on-chain. Session keys are designed to be temporary, so their lifecycle is optimized for minimum storage fee footprints.

Main gameplay flow

  1. Authorization: Owner calls approve_session from their wallet to authorize a temporary key for the tap action.
  2. Gameplay: Client signs and executes calls to tap using the session key, bypassing any ledger signature popups.
  3. Renewal / Fallback: If the session expires, the client calls renew_session (wallet-prompted) or calls fallback_tap (which handles direct-auth fallback).

Cougr APIs used

  • SessionManager: Manages approval, status, execution verification, and direct-auth fallbacks.
  • SessionBuilder: Scopes maximum operations and timestamps for the generated key.
  • SessionStorage: Loads active session state from the environment.
  • impl_component!: Declares the player's Score component.

Use the GameHarness and MockSession to simulate wallet authorization, session approvals, key rotation, and operation expirations. Verification covers the happy-path tapping, time-bound key expiry, and fallback routing checks.

Build and test commands

cargo test
stellar contract build

feat/session-manager-fresh

Test coverage

TestDescription
approve_and_tap_without_reauthMultiple taps after single approval
renew_session_extends_play_windowRenew increases absolute expires_at
fallback_tap_uses_direct_auth_after_session_expiresFallback after timestamp expiry
mock_session_helper_matches_manager_flowMockSession matches manager flow

When to use which example

Use session_arena whenโ€ฆUse tap_battle whenโ€ฆ
Learning SessionManager basicsBuilding a real game with passkeys
Prototyping session UX in a new gameNeed combo mechanics and rounds
Writing integration tests for sessionsDemonstrating mobile-first auth flow

License

MIT

Known limitations

  • Simple score counter used for demonstrating the session auth wrapper; no actual gameplay engine included.
  • Minimal session storage configuration.

Shadow Draft Card Game

Card Transitional
authzk-circuitsprivacycommit-revealecs

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

A Soroban smart-contract example demonstrating:

  • Hidden-hand draft gameplay - players commit to a card choice via a SHA-256 hash before either reveal is visible, preventing last-second strategy adaptation.
  • stellar-zk proof-backed card validation - when a verification key is registered, each card play must include a Groth16 proof that the chosen card is a member of the active allowed set, validated on-chain without revealing the card ID until both players have committed.
  • DAO-governed format rules - any player can propose banning or unbanning a card from the active season format; proposals execute immediately (modelling a passed governance vote).

Architecture

Components

ComponentFieldsPurpose
DeckComponentplayer, active_formatDeck context and active season format
HandCommitmentComponentcommitment, committed, revealed, revealed_cardHidden-hand state per player
BoardStateComponentround, score_one/two, last_played_*, round_stateVisible board after each resolution
FormatProposalComponentproposal_id, ban_card, unban_card, proposer, vote_count, statusDAO governance proposals
GameStatusComponentstatus, phaseTop-level match lifecycle

Systems

SystemTriggerResponsibility
DraftSystemBoth players committedAdvance PHASE_DRAFT โ†’ PHASE_PLAY
ProofValidationSystemplay_cardVerify SHA-256 commitment + optional Groth16 proof
CardPlaySystemplay_cardEnforce active format ban list
RoundResolutionSystemBoth players revealedCompare card powers, update scores, reset or end match
FormatGovernanceSystemsubmit_format_proposalApply accepted ban/unban to banned_cards

Game Flow

init_match(p1, p2)
โ”‚
โ””โ”€โ–บ Round N
     โ”œโ”€ PHASE_DRAFT
     โ”‚   โ”œโ”€ p1: submit_choice({ commitment: sha256(card_id || nonce) })
     โ”‚   โ””โ”€ p2: submit_choice({ commitment: sha256(card_id || nonce) })
     โ”‚         โ†’ DraftSystem advances to PHASE_PLAY
     โ”‚
     โ”œโ”€ PHASE_PLAY
     โ”‚   โ”œโ”€ p1: play_card({ card_id, nonce, proof, public_inputs })
     โ”‚   โ”‚       ProofValidationSystem: sha256 check + optional Groth16 verify
     โ”‚   โ”‚       CardPlaySystem: ban-list check
     โ”‚   โ””โ”€ p2: play_card(...)
     โ”‚         โ†’ RoundResolutionSystem fires
     โ”‚
     โ””โ”€ Resolution
          higher card_power(card_id) wins the round
          first to ROUNDS_TO_WIN (3) wins the match

stellar-zk Integration

The contract uses cougr-core::zk::groth16::verify_groth16 (compatible with stellar-zk / salazarsebas/stellar-zk) for on-chain proof verification.

When a verification key is registered via set_vk, each play_card call must supply a valid Groth16 proof attesting that the revealed card_id belongs to the active allowed set. This allows players to prove card legality in zero-knowledge without leaking which card they chose before the opponent reveals.

Public inputs: [ hash_of_commitment, format_id ]
Private inputs: [ card_id, nonce ]
Circuit: sha256(card_id || nonce) == commitment AND card_id โˆˆ allowed_set[format_id]

Format Governance

Governance references: governance.script3.io

A minimal DAO surface is provided via submit_format_proposal. In a production deployment this would gate execution on an on-chain quorum vote. For the reference implementation the proposal is auto-accepted upon submission to keep the flow testable without a separate governance contract.

#![allow(unused)]
fn main() {
// Propose a seasonal card ban
contract.submit_format_proposal(
    proposer,
    ProposalInput { ban_card: 8, unban_card: 0 },
);

// Lift a prior ban
contract.submit_format_proposal(
    proposer,
    ProposalInput { ban_card: 0, unban_card: 8 },
);
}

Contract API

#![allow(unused)]
fn main() {
fn init_match(env: Env, player_one: Address, player_two: Address)
fn submit_choice(env: Env, player: Address, choice: ChoiceInput)
fn play_card(env: Env, player: Address, play: PlayInput)
fn submit_format_proposal(env: Env, proposer: Address, proposal: ProposalInput)
fn get_state(env: Env) -> GameState
fn set_vk(env: Env, vk: VerificationKey)
}

Validation Commands

cd examples/shadow_draft_card_game
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
stellar contract build

Snake On-Chain Game

Arcade Canonical
game-appecs

Classification: Canonical example. This is the maintained arcade reference for Cougr examples. New arcade contracts should copy its GameApp wiring, components.rs / systems.rs split, README shape, and test coverage.

Purpose and pattern

Snake demonstrates a deterministic arcade loop on Soroban using cougr-core's basic ECS and GameApp tick model. The contract keeps persistent game state on chain, stores ECS entities in a SimpleWorld, and runs ordered systems for movement, collision, growth, and food spawning.

Public contract API

FunctionParametersReturn typeDescription
init_gamenone()Initializes a new game on the default 10ร—10 grid.
init_game_with_sizegrid_size: i32()Initializes a new game on a custom square grid.
change_directiondirection: u32boolChanges the snake direction (0 up, 1 down, 2 left, 3 right); returns false for invalid values, reversals, or game-over state.
update_ticknone()Advances the game by one GameApp tick.
get_scorenoneu32Returns the current score.
check_game_overnoneboolReturns whether the game has reached a terminal state.
get_head_posnone(i32, i32)Returns the current snake-head position.
get_snake_lengthnoneu32Returns the number of snake entities.
get_food_posnone(i32, i32)Returns the current food position.
get_snake_positionsnoneVec<(i32, i32)>Returns all snake segment positions.
get_grid_sizenonei32Returns the configured grid size.

Architecture overview

contract entrypoint
  โ”œโ”€ loads GameState + SimpleWorld from persistent storage
  โ”œโ”€ builds a GameApp around the world
  โ”œโ”€ schedules systems by stage
  โ”‚   โ”œโ”€ Update: move_snake
  โ”‚   โ””โ”€ PostUpdate: self_collision -> food_collision
  โ””โ”€ writes GameState + SimpleWorld back to storage
  • lib.rs contains the Soroban contract entrypoints, storage access, and GameApp wiring.
  • components.rs contains serializable ECS components such as Position, DirectionComponent, SnakeHead, SnakeSegment, and Food.
  • systems.rs contains reusable game systems for movement, direction validation, collision checks, growth, and food spawning.

Storage model

Storage classDataWhy
Instance storagenoneThe example does not need contract-wide configuration shared across games.
Persistent storagestate: GameState, world: SimpleWorldGame progress must survive across transactions. GameState stores compact scalar data; SimpleWorld stores entities and component bytes.
Temporary storagenoneNo per-ledger cache is needed for deterministic gameplay.

Within the SimpleWorld, dense components such as positions and directions use table-style access, while marker-style components such as food/head/segment are queried as needed.

Main gameplay flow

  1. A player calls init_game or init_game_with_size.
  2. Startup systems spawn the snake head at the grid center and create one food entity.
  3. The player calls change_direction to submit a valid non-reversing input.
  4. The player or a relayer calls update_tick.
  5. GameApp runs movement first, then collision and food checks.
  6. A wall/self collision sets game_over; eating food grows the snake, increments score, and spawns new food.
  7. Query functions expose score, positions, grid size, and terminal state.

Cougr APIs used

APIWhy it is used
GameAppProvides the maintained arcade-loop pattern and owns scheduled system execution per tick.
ScheduleStage / SystemConfigEnsures movement runs before post-update collision and food systems.
SimpleWorldStores snake, food, and component data in a Soroban-serializable ECS container.
SimpleQueryBuilderScans entities by component type for food, head, and segment queries.
ComponentTraitGives each custom component deterministic serialization and a stable component type.

This example does not use Cougr auth, privacy, ZK, or standards modules because Snake is intentionally a single-player arcade-loop reference.

Build and test commands

cargo test
stellar contract build

Known limitations

Recommended Testing Approach: For comprehensive testing, use the GameHarness and Scenario APIs provided by cougr-core's testutils feature (see sandbox_tests.rs). This allows writing replayable multi-turn scenarios to verify movement trajectories, direction change validation, and tick updates.

Expected Output:

running 31 tests
test result: ok. 31 passed; 0 failed; 0 ignored

4. Lint

cargo fmt --check
cargo clippy -- -D warnings

Contract Functions

Initialization Functions

FunctionParametersReturnsDescription
init_game--Start with 10ร—10 grid
init_game_with_sizegrid_size: i32-Start with custom grid

Control Functions

FunctionParametersReturnsDescription
change_directiondirection: u32boolChange movement direction
update_tick--Advance game one step

Direction Values:

ValueDirectionDelta (x, y)
0Up(0, -1)
1Down(0, +1)
2Left(-1, 0)
3Right(+1, 0)

Query Functions

FunctionReturnsDescription
get_scoreu32Current score
check_game_overboolGame ended status
get_head_pos(i32, i32)Head coordinates
get_snake_lengthu32Total length
get_food_pos(i32, i32)Food coordinates
get_snake_positionsVec<(i32, i32)>All positions
get_grid_sizei32Grid dimensions

Deployment

Testnet Deployment

# 1. Generate keypair
stellar keys generate --global alice --network <NETWORK>
stellar keys address alice

# 2. Fund account (visit URL with your address)
# https://friendbot.stellar.org/?addr=<YOUR_ADDRESS>

# 3. Deploy contract
stellar contract deploy \
  --wasm target/wasm32v1-none/release/snake.wasm \
  --source alice \
  --network <NETWORK>

# Save the returned Contract ID!

Playing the Game

CONTRACT_ID="<your-contract-id>"

# Initialize
stellar contract invoke --id $CONTRACT_ID --source alice --network <NETWORK> -- init_game

# Change direction (0=Up, 1=Down, 2=Left, 3=Right)
stellar contract invoke --id $CONTRACT_ID --source alice --network <NETWORK> -- change_direction --direction 0

# Advance game
stellar contract invoke --id $CONTRACT_ID --source alice --network <NETWORK> -- update_tick

# Check score
stellar contract invoke --id $CONTRACT_ID --source alice --network <NETWORK> -- get_score

Deployed Contract

NetworkContract IDExplorer

Project Structure

examples/snake/
โ”œโ”€โ”€ Cargo.toml              # Dependencies (cougr-core, soroban-sdk)
โ”œโ”€โ”€ README.md               # This documentation
โ”œโ”€โ”€ .gitignore              # Ignore rules (test_snapshots/, target/)
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ lib.rs              # Contract entry points (11 functions)
    โ”œโ”€โ”€ components.rs       # Components using cougr-core::ComponentTrait
    โ”œโ”€โ”€ systems.rs          # Game logic systems
    โ””โ”€โ”€ simple_world.rs     # Entity-component storage
FilePurpose
lib.rsSoroban contract with public functions and tests
components.rsComponent definitions implementing ComponentTrait
systems.rsGame mechanics (movement, collision, spawning)
simple_world.rsEntity and component storage layer

Creating Components

Using cougr-core's ComponentTrait

#![allow(unused)]
fn main() {
use cougr_core::component::{Component, ComponentStorage, ComponentTrait};
use soroban_sdk::{symbol_short, Bytes, Env, Symbol};

pub struct MyComponent {
    pub value: u32,
}

impl ComponentTrait for MyComponent {
    // Unique identifier for this component type
    fn component_type() -> Symbol {
        symbol_short!("mycomp")
    }

    // Serialize to bytes for on-chain storage
    fn serialize(&self, env: &Env) -> Bytes {
        let mut bytes = Bytes::new(env);
        bytes.append(&Bytes::from_array(env, &self.value.to_be_bytes()));
        bytes
    }

    // Deserialize from bytes
    fn deserialize(_env: &Env, data: &Bytes) -> Option<Self> {
        if data.len() != 4 { return None; }
        let value = u32::from_be_bytes([
            data.get(0)?, data.get(1)?, data.get(2)?, data.get(3)?
        ]);
        Some(Self { value })
    }

    // Choose storage strategy
    fn default_storage() -> ComponentStorage {
        ComponentStorage::Table  // For dense data
        // ComponentStorage::Sparse  // For marker components
    }
}
}

Converting to Component

#![allow(unused)]
fn main() {
impl MyComponent {
    pub fn to_component(&self, env: &Env) -> Component {
        Component::new(Self::component_type(), self.serialize(env))
    }
}
}

Troubleshooting

IssueSolution
Rust version errorsrustup update && rustup default stable
WASM target missingrustup target add wasm32v1-none
Stellar CLI not foundbrew install stellar-cli (macOS)
Dependency conflictscargo update && cargo clean && cargo build
Test snapshots issuesDelete test_snapshots/ directory

Full Verification

cargo fmt --check && cargo clippy -- -D warnings && cargo test && stellar contract build

References

ResourceLink
Soroban Docsdevelopers.stellar.org
Stellar CLICLI Documentation
Cougr Repositorygithub.com/salazarsebas/Cougr
Rust TestingRust Book Ch. 11

License

  • Food spawning is deterministic and suitable for examples, not adversarial randomness.
  • There is one game state per contract instance.
  • No authentication or ownership model is included.
  • Rendering and real-time scheduling are out of scope; callers drive ticks through contract invocations.

Sokoban

Puzzle Canonical
ecsarchetype-world

A push-crate puzzle contract demonstrating ArchetypeWorld, Cougr's archetype-grouped ECS backend, and ArchetypeQueryBuilder's without_component query for win detection.

Purpose and pattern

The player moves one tile at a time; pushing into a crate shoves it ahead if the tile beyond is free. The level is solved once every crate shares a position with a goal.

Every other canonical example builds on SimpleWorld. Sokoban is the canonical reference for ArchetypeWorld instead: its five component types (Player, Wall, Crate, Goal, OnGoal, plus the shared Position) each define a distinct archetype by construction, so the win check and push resolution query a pre-grouped set of entities rather than scanning the whole world.

Public contract API

FunctionParametersReturnsDescription
init_levelplayer: Address, width: u32, height: u32, tiles: Vec<u32>()Validates and loads a level from a row-major tile grid. Requires player's authorization. The same player may call this again to replace their level; a different address is rejected if a level is already stored.
pushplayer: Address, direction: u32()Moves the player one tile, pushing a crate ahead of it if present. Requires player's authorization and that player matches the address that called init_level.
level_meta-LevelMetaMove count, solved flag, and level dimensions.
player_position-PositionThe player's current position.

Level encoding

tiles is width * height entries, row-major (index y * width + x), using:

ConstantValueMeaning
TILE_EMPTY0Open floor
TILE_WALL1Impassable
TILE_CRATE2Pushable crate
TILE_GOAL3Crate destination
TILE_PLAYER4Player start (exactly one required)

A level needs exactly one player tile and an equal, positive number of crate and goal tiles, and neither dimension may exceed MAX_DIM (20). push takes a direction from NORTH / EAST / SOUTH / WEST (+y / +x / -y / -x), matching the convention spawn_and_move uses for the same four constants.

Pushing the player, or a crate it is pushing, outside 0..width by 0..height panics with "out of bounds".

Architecture overview

lib.rs        #[contract] entrypoints: validate input, load/save the world, delegate to systems
components.rs Position (re-exported) + Player/Wall/Crate/Goal/OnGoal markers + LevelMeta
systems.rs    try_push (move/push resolution) and is_solved (win check), independent of Soroban glue
test.rs       #[cfg(test)] client-driven tests

Entrypoints stay thin: init_level and push validate input, load state, call into systems.rs, persist state, and return. All gameplay logic, including the sole without_component query, lives in systems.rs. There is no GameApp/ScheduleStage wiring; a single push is small enough to resolve inline without a scheduled tick.

Storage model

The entire ArchetypeWorld is kept as one value in instance storage, the same tier impl_soroban_game! uses for SimpleWorld (no equivalent helper macro exists yet for ArchetypeWorld, so lib.rs wires load_world/save_world manually around env.storage().instance()). The player's Address lives under its own instance storage key rather than as a component, because ArchetypeWorld::get_typed/set_typed are bounded to ComponentTrait, and Address-bearing data needs RichComponentTrait (see impl_rich_component!), for which ArchetypeWorld has no typed accessors. No persistent or temporary storage is used.

Every call reads and rewrites the whole world blob. That is a deliberate simplification for a single, bounded-size puzzle (MAX_DIM keeps grids at 20x20 or smaller), not a pattern to copy for a world with many independent or large levels; see Known limitations.

Main gameplay flow

  1. A caller calls init_level with a grid and their own Address. The contract validates the grid, spawns one entity per non-empty tile (the player is always entity 1), stores the caller's address, and persists the world.
  2. The caller calls push with a direction. systems::try_push panics if the level is already solved or the move is blocked, otherwise moves the player (and a pushed crate, if any), updates the crate's OnGoal marker, increments the move count, and re-runs the win check.
  3. Step 2 repeats until every crate carries OnGoal, at which point level_meta().solved becomes true and further push calls panic.
  4. level_meta and player_position can be read at any time without mutating state.

Cougr APIs used

APIMaturityWhy
ArchetypeWorldStableEntities are grouped by component signature, so Wall, Crate, Goal, OnGoal, and Player each form their own archetype without any manual indexing.
ArchetypeQueryBuilderStablewith_component finds the wall/crate/goal at a tile; without_component(OnGoal) on Crate is the win check, returning no entities once every crate sits on a goal.
impl_marker_component!-Player, Wall, Crate, Goal, OnGoal carry no data; a position plus archetype membership is all each one needs.
impl_component!-LevelMeta carries real fields (move_count, solved, width, height) and uses the same fixed-size serialization every other canonical example's non-marker components use.

Build and test commands

cargo test
stellar contract build

Known limitations

  • One level per contract instance, with no multi-level or save-slot support.
  • The whole world is read and rewritten on every call. That scales fine for a puzzle-sized grid but is not the storage pattern to copy for a world meant to hold many independent or large levels.
  • No state-change events are emitted; impl_component_observed! is not used, so clients must poll level_meta / player_position rather than subscribe to updates.

Space Invaders On-Chain Game

Arcade Transitional
game-appecs

Space Invaders - On-Chain Game Example

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

Purpose and pattern

This example demonstrates a shooter entity-update loop on Soroban with Cougr ECS concepts. It remains transitional while the arcade examples converge on the canonical snake GameApp architecture.

Public contract API

FunctionParametersReturn typeDescription
init_gamenone()Initializes ship, invaders, score, lives, and game flags.
move_shipdirection: i32i32Moves the ship left/right within bounds and returns x position.
shootnoneboolSpawns a player bullet when possible.
update_ticknoneboolAdvances bullets, invaders, collisions, score, and game-over checks.
get_scorenoneu32Returns score.
get_livesnoneu32Returns remaining lives.
get_ship_positionnonei32Returns ship x position.
check_game_overnoneboolReturns terminal state.
get_active_invadersnoneu32Returns number of active invaders.
get_entity_countnoneu32Returns total tracked entity count.

Live Deployment

NetworkContract IDStatus
Testnet<CONTRACT_ID>๐ŸŸข Active

Architecture overview

contract entrypoint
  โ”œโ”€ reads game state from Soroban storage
  โ”œโ”€ applies input or tick systems
  โ””โ”€ writes updated state back to storage

## Overview

This example demonstrates how to build on-chain game logic on the Stellar blockchain using **cougr-core's ECS architecture**. The game focuses exclusively on smart contract logic (no graphical interface) and includes:

| Feature | Description |
|---------|-------------|
| ๐Ÿš€ **Ship Control** | Left/right movement with bounds checking |
| ๐Ÿ‘พ **Invader Grid** | 4ร—8 formation with wave-based movement |
| ๐Ÿ’ฅ **Bullet System** | Player and enemy projectiles with velocity |
| ๐ŸŽฏ **Collision Detection** | Position-based hit detection |
| โค๏ธ **Health System** | Lives tracking using Health components |
| ๐Ÿ† **Scoring** | Point-based scoring by invader type |

---

## Why Cougr-Core?

**Cougr-Core** provides an ECS (Entity-Component-System) architecture specifically designed for Soroban smart contracts. Here's how it benefits this project:

### Benefits of Using Cougr-Core

| Benefit | Description | Example in This Project |
|---------|-------------|------------------------|
| **Modular Components** | Reusable data structures attached to entities | `EntityPosition`, `Velocity`, `Health` used by Ship, Invaders, and Bullets |
| **Separation of Concerns** | Logic (Systems) separated from data (Components) | Movement System updates all entities with Velocity |
| **Type Safety** | Rust's type system prevents component misuse | `CougrPosition` ensures consistent coordinate handling |
| **WASM Optimization** | ECS optimizes memory access patterns for WASM | Efficient iteration over entity components |
| **Scalability** | Easy to add new features without refactoring | Adding new entity types only requires new components |
| **On-Chain Ready** | Designed for blockchain state persistence | Components serialize to Soroban storage |

### ECS Architecture in Practice

```rust
// Using cougr-core's Position component
use cougr_core::Position as CougrPosition;

// Entity with Position, Velocity, and Health components
pub struct Bullet {
    pub position: EntityPosition,   // Where the bullet is
    pub velocity: Velocity,         // How it moves
    pub active: bool,               // Entity state
}

// Movement System: Apply velocity to position
impl Bullet {
    pub fn update(&mut self) {
        self.velocity.apply_to(&mut self.position);
    }
}

Ship, invader, bullet, score, and lives state is currently compacted in contract storage. components.rs and systems.rs expose the transitional ECS boundary for future migration.

Storage model

Storage classDataWhy
Instance storagePer-contract game state where used by this example.Keeps small arcade state close to the contract instance.
Persistent storagePlayer- or world-scoped state where the example needs durable keyed state.Keeps game progress available across invocations.
Temporary storageNot used.The examples favor deterministic recalculation over ephemeral caches.

Main gameplay flow

๏ธ Quick Start

  1. Call the initialization function to create the starting state.
  2. Submit an input action such as movement, jump, flap, rotation, or shoot.
  3. Call the tick/update function to run deterministic simulation logic.
  4. Query public getters for score, position, active state, or terminal status.
  5. Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.

Cougr APIs used

  • ComponentTrait and custom component modules document the ECS data boundary.
  • SimpleWorld, SimpleQueryBuilder, GameApp, ScheduleStage, or SystemConfig are used where this transitional example has already adopted the maintained runtime shape.
  • Auth, privacy, ZK, and standards APIs are intentionally not used; these arcade examples focus on deterministic game logic.

Build and test commands

cargo test

stellar contract build

Known limitations


**Test Results**: 13 tests passing โœ…

| Test | Description |
|------|-------------|
| `test_init_game` | Game initializes with correct defaults |
| `test_move_ship_left/right` | Ship movement works correctly |
| `test_move_ship_left/right_bounds` | Ship respects boundaries |
| `test_shoot` | Shooting creates bullets |
| `test_shoot_cooldown` | Cooldown prevents rapid fire |
| `test_shoot_after_cooldown` | Shooting works after cooldown |
| `test_update_tick` | Game loop advances correctly |
| `test_score_increase` | Score increases on hits |
| `test_invader_destruction` | Invaders can be destroyed |
| `test_game_over_no_lives` | Game ends when lives = 0 |
| `test_no_move_when_game_over` | No actions after game over |

---

## Contract API

### Core Functions

| Function | Parameters | Returns | Description |
|----------|------------|---------|-------------|
| `init_game` | - | - | Initialize new game with ECS World |
| `move_ship` | `direction: i32` | `i32` | Move ship (-1=left, 1=right) |
| `shoot` | - | `bool` | Fire bullet (true if successful) |
| `update_tick` | - | `bool` | Advance game (true if running) |

### Query Functions

| Function | Returns | Description |
|----------|---------|-------------|
| `get_score` | `u32` | Current player score |
| `get_lives` | `u32` | Remaining lives |
| `get_ship_position` | `i32` | Ship X coordinate |
| `check_game_over` | `bool` | Game over status |
| `get_active_invaders` | `u32` | Remaining invader count |
| `get_entity_count` | `u32` | Cougr-core entity count |

---

## Game Mechanics

### Invaders

| Type | Position | Points | Health |
|------|----------|--------|--------|
| ๐Ÿฆ‘ Squid | Top row | 30 pts | 1 HP |
| ๐Ÿฆ€ Crab | Middle rows | 20 pts | 1 HP |
| ๐Ÿ™ Octopus | Bottom row | 10 pts | 1 HP |

**Behavior**:
- Move horizontally in formation
- Descend when reaching screen edge
- Game over if they reach player's row

### Player Ship

| Property | Value |
|----------|-------|
| Starting Lives | 3 |
| Position | Center of game board |
| Movement | Left/Right within bounds |
| Shoot Cooldown | 3 ticks |

### Game Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `GAME_WIDTH` | 40 | Board width |
| `GAME_HEIGHT` | 30 | Board height |
| `INVADER_COLS` | 8 | Invaders per row |
| `INVADER_ROWS` | 4 | Invader rows |
| `BULLET_SPEED` | 2 | Positions per tick |

---

## Deploy to Testnet

### 1. Setup Identity

```bash
# Generate a new identity
stellar keys generate --global deployer --network <NETWORK>

# Fund the account
stellar keys address deployer | xargs -I {} curl "https://friendbot.stellar.org?addr={}"

2. Build & Deploy

# Build WASM
stellar contract build

# Deploy to testnet
stellar contract deploy \
  --wasm target/wasm32v1-none/release/space_invaders.wasm \
  --source deployer \
  --network <NETWORK>

3. Initialize & Play

# Set your contract ID
CONTRACT_ID="your_contract_id_here"

# Initialize game
stellar contract invoke --id $CONTRACT_ID --source deployer --network <NETWORK> -- init_game

# Play!
stellar contract invoke --id $CONTRACT_ID --network <NETWORK> -- move_ship --direction 1
stellar contract invoke --id $CONTRACT_ID --network <NETWORK> -- shoot
stellar contract invoke --id $CONTRACT_ID --network <NETWORK> -- update_tick
stellar contract invoke --id $CONTRACT_ID --network <NETWORK> -- get_score

Project Structure

examples/space_invaders/
โ”œโ”€โ”€ Cargo.toml          # Dependencies including cougr-core
โ”œโ”€โ”€ README.md           # This documentation
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ lib.rs          # Contract entry points & ECS systems
    โ”œโ”€โ”€ game_state.rs   # ECS Components (Position, Velocity, Health)
    โ””โ”€โ”€ test.rs         # Unit tests (13 tests)

License

  • Transitional code may preserve older storage or scheduling patterns for compatibility reference.
  • No authentication, matchmaking, real-time rendering, or production randomness is included.
  • One contract instance generally represents one game or one keyed set of player games.
  • For new work, prefer the canonical snake module split and GameApp tick wiring.

spawn_and_move

Other Canonical
soroban-gameecs

Canonical example demonstrating SorobanGame, impl_component_observed!, and typed ECS.

Purpose and pattern

This example showcases a starter 2D grid world. A player can spawn an entity and move it in four directions. It demonstrates how to declare components, use observed components that automatically emit indexing events when modified, and load/save world state via SorobanGame.

Public contract API

FunctionParametersReturnsDescription
spawn-u32Spawns a new entity at origin (0,0) and returns its generated entity ID.
move_entityentity_id: u32, direction: u32-Moves the entity in the specified direction if moves remain.
positionentity_id: u32Option<Position>Retrieves the current Position of the given entity.
movesentity_id: u32Option<Moves>Retrieves the current Moves component of the given entity.
entity_count-u32Retrieves the total number of entities spawned in the world.

Architecture overview

                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  spawn_and_move Client โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Calls
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ SpawnAndMove Game   โ”‚
                     โ”‚ (Soroban Contract)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Loads / Saves
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚     SimpleWorld     โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚ Stores
               โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”                   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚  Position   โ”‚                   โ”‚    Moves    โ”‚
        โ”‚ (Observed)  โ”‚                   โ”‚  (Standard) โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

When a client calls move_entity, the contract loads the SimpleWorld using SpawnAndMove::load_world, queries/modifies the Position and Moves components of the entity using typed ECS accessors, and saves the world state back using SpawnAndMove::save_world.

Storage model

All game components and entity metadata are stored in Soroban Instance Storage via the underlying SimpleWorld. This keeps the hot-loop gameplay state localized and loaded efficiently in a single storage read/write lifecycle per transaction.

Main gameplay flow

  1. Initialization / Spawn: The user calls spawn. An entity is spawned at (0,0) with 10 moves remaining. A ("COUGR", "set", "position") event is emitted.
  2. Action / Movement: The user calls move_entity with direction (0=North, 1=East, 2=South, 3=West). The remaining moves decrement, the position updates, and a position set event is emitted.
  3. Query: The user reads the entity's position or moves remaining.

Cougr APIs used

  • SorobanGame and impl_soroban_game!: Wires up standard boilerplate for loading/saving world state from instance storage.
  • impl_component_observed!: Implements component layout with automated indexer events on set.
  • impl_component!: Defines standard components without indexing event overhead.
  • SimpleWorld: Provides structured, entity-component key-value management.

Tests in this project should utilize the cougr-core testutils feature, specifically GameHarness and Scenario. The GameHarness registers the contract, while the Scenario allows executing multi-step and multi-turn movement verification with intermediate assertions.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Simple grid coordinates without map size constraints or collision checks.
  • Unauthenticated entity movement (any caller can move any entity ID).

Staked Tournament

Other Canonical
token-escrow
staked_tournament preview

Purpose and pattern

staked_tournament demonstrates per-match token escrow for a four- or eight-player, single-elimination bracket. Each match stores its own stake flags and settlement state, and a settled winner is assigned to the next round automatically. This is a pattern for multi-match staked play, not a production tournament product.

Public contract API

FunctionParametersReturnsDescription
initializecreator: Address, entrants: Vec<Address>, token_address: Address, stake: i128, timeout_seconds: u64()Creator-authorized setup for four or eight unique entrants.
stake_matchplayer: Address, match_id: u32()Transfers the player's stake into the contract before the match deadline.
resolve_matchwinner: Address, match_id: u32()Winner-authorized settlement after both players stake; pays the pot and advances the winner.
forfeit_no_showmatch_id: u32()After the deadline, refunds and advances the sole player who staked.
get_matchmatch_id: u32MatchRecordReads the players, stake flags, deadline, and settlement result for one match.
champion-Option<Address>Reads the final winner once the bracket is complete.

Architecture overview

The contract stores the bracket as a flat list of match records. Every match has its own two stake flags, deadline, winner, and precomputed next-round destination. Normal settlement pays that match's two-stake pot; no-show settlement returns the present player's own stake. Both paths use the same winner-advancement function, which fills the appropriate parent match slot and starts its deadline once both players are assigned.

There is no GameApp tick or ECS integration. resolve_match trusts an authenticated winner to report the result, so a real game should call an escrow settlement path only after verifying its own match outcome.

Storage model

Tournament configuration, the fixed bracket slots, and champion are stored together in instance storage. The example keeps all bracket data in one instance value for clarity; a longer-running tournament would need an explicit storage and TTL strategy.

Main gameplay flow

  1. The creator initializes the contract with four or eight unique players, a Soroban token address, a positive stake, and a timeout.
  2. Each first-round player calls stake_match; their stake moves to the contract and is tracked only in that match's record.
  3. After both stakes arrive, the authenticated winner calls resolve_match. The winner receives both stakes and advances automatically.
  4. If exactly one player stakes before the deadline, anyone can call forfeit_no_show after it expires. The present player's stake is returned and they advance, so the next round can proceed.
  5. The two advancing players stake in their new match. The final settlement records the champion.

Cougr APIs used

None. This example isolates the Soroban token interface and bracket escrow pattern; it does not use Cougr ECS, scheduler, auth, or standards APIs.

Build and test commands

cd examples/staked_tournament
cargo test
stellar contract build

Known limitations

  • Only four- and eight-player single-elimination brackets are supported.
  • No seeding, ranking, matchmaking, house fee, or dispute resolution is included.
  • The winner self-reports normal match results; the contract does not verify gameplay outcomes.
  • A no-show refund requires exactly one player to have staked. A match where neither player stakes, or where both stake and then disappear, needs an external decision process and cannot advance automatically.
  • This example is not a production tournament product and makes no claim about live assets or mainnet deployment.

Sudoku

Puzzle Transitional
ecs

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

An on-chain Sudoku puzzle built with the Cougr ECS framework on Stellar Soroban.

Purpose and pattern

This example demonstrates a single-player constraint-validation puzzle where moves are checked against row/column/3ร—3-block rules rather than against an opponent. It showcases Cougr's ComponentTrait pattern for typed, byte-serializable game state: each piece of state (board, fixed-cell mask, status, move counter) is its own component with manual serialize/deserialize, demonstrating explicit byte-layout control for a fixed-size grid.

Public contract API

FunctionParametersReturnsDescription
init_gamepuzzle: Vec<u32>-Seeds an empty 9ร—9 board from an 81-cell array (0=empty, 1โ€“9=given). Cells with a non-zero starting value become fixed. Panics if already initialized, the length isn't 81, or a value exceeds 9.
submit_valuerow: u32, col: u32, value: u32-Validates and places value at (row, col). Panics if the puzzle is solved, the cell is fixed/out-of-bounds, the value is outside 1โ€“9, or the placement violates a row/column/block constraint.
get_state-GameStateCurrent status (0=playing, 1=solved) and move count.
get_cellrow: u32, col: u32CellStateCurrent value at the cell and whether it is a fixed given.
is_solved-boolWhether the puzzle is fully and correctly filled.

Architecture overview

submit_value runs a fixed validation/update pipeline synchronously on each call - there is no GameApp tick loop:

submit_value
  โ””โ”€ InputValidationSystem        โ†’ bounds check, rejects fixed cells, rejects out-of-range values
  โ””โ”€ PlacementValidationSystem    โ†’ checks row, column, and 3ร—3 block constraints
  โ””โ”€ BoardUpdateSystem            โ†’ writes the value into the board component
  โ””โ”€ CompletionSystem             โ†’ re-validates every row/column/block for full-board completion
  โ””โ”€ EndConditionSystem           โ†’ sets status=solved when CompletionSystem passes

components.rs defines the data (BoardComponent, FixedCellsComponent, GameStatusComponent, MoveCountComponent); systems.rs holds the pure validation and update functions; lib.rs owns the single ECSWorldState aggregate and is the only module that touches contract storage.

Storage model

The full puzzle state lives under one instance-storage key (WORLD_KEY, symbol_short!("WORLD")) as a single ECSWorldState bundling the board, the fixed-cell mask, status, and move count. Instance storage is used because there is exactly one puzzle per contract instance with no per-entry expiry needed; persistent or temporary storage would add TTL bookkeeping this single-instance puzzle doesn't need.

Main gameplay flow

  1. Deployer calls init_game(puzzle) with an 81-cell array representing the starting grid; non-zero cells are marked fixed and cannot be overwritten.
  2. Player calls submit_value(row, col, value) for an empty cell. The contract checks the cell isn't fixed, the value is 1โ€“9, and the placement doesn't repeat a value in the same row, column, or 3ร—3 block.
  3. On a legal placement, the board updates and the move counter increments.
  4. After every placement, CompletionSystem re-checks all 9 rows, 9 columns, and 9 blocks. When the board is completely and correctly filled, status flips to solved.
  5. Caller reads is_solved or get_state to check completion; get_cell reads individual cells (e.g., to render the board off-chain).

Cougr APIs used

  • cougr_core::component::ComponentTrait - gives each component (BoardComponent, FixedCellsComponent, GameStatusComponent, MoveCountComponent) a component_type() symbol and explicit byte serialize/deserialize, chosen because the puzzle has one fixed-shape 81-cell grid rather than a dynamic entity population that would benefit from SimpleWorld/SimpleQueryBuilder scanning.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp, ScheduleStage, or SimpleWorld - validation runs directly from contract entrypoints since a single-player puzzle with one decision point per call has no need for staged scheduling.
  • init_game accepts any caller and does not verify the supplied puzzle has a unique solution; puzzle generation/validation is left to the caller.
  • No undo, hint, or partial-progress save beyond the committed board state.

Tap Battle - Mobile-First Competitive Tapping Game

Other Transitional
authsession-authecs

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

Build Status Tests Stellar

A competitive tapping game implemented as a Soroban smart contract using cougr-core's ECS framework with passkey authentication (secp256r1/WebAuthn) on the Stellar blockchain.

feat/session-manager-fresh

Related example: For the minimal session-only flow without passkeys or game mechanics, see session_arena.

๐Ÿ” Why Passkeys?

Why Passkeys?

Traditional blockchain games require seed phrases and per-transaction wallet prompts. Tap Battle eliminates both:

TraditionalWith Passkeys
Write down 24-word seed phraseTap Face ID / Touch ID
Approve every transaction in walletSession key handles gameplay
Complex key managementBrowser/OS manages credentials
Crypto-native onlyAnyone can play

Passkeys use the secp256r1 curve (the same as WebAuthn/FIDO2), enabling biometric authentication natively on mobile devices without any crypto knowledge.


Mobile-First Authentication Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ 1. REGISTRATION                                         โ”‚
โ”‚    Player registers passkey (secp256r1 public key)       โ”‚
โ”‚    โ†’ Secp256r1Storage persists the key on-chain          โ”‚
โ”‚    โ†’ No seed phrases, no mnemonics                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 2. AUTHENTICATION + SESSION                              โ”‚
โ”‚    Player authenticates via passkey (Face ID / Touch ID)  โ”‚
โ”‚    โ†’ verify_secp256r1() validates the signature           โ”‚
โ”‚    โ†’ SessionManager::approve creates a gameplay session   โ”‚
โ”‚    โ†’ Session scoped to: tap, use_power_up                 โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 3. GAMEPLAY (gasless via session key)                     โ”‚
โ”‚    Rapid tapping โ†’ tap(session_key) increments counter    โ”‚
โ”‚    Combos: consecutive taps within time window = multi    โ”‚
โ”‚    Power-ups: spend combo charges for burst effects       โ”‚
โ”‚    Round ends after N ledger sequences                    โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 4. RENEWAL / FALLBACK                                     โ”‚
โ”‚    session_status โ†’ UI prompts when needs_renewal         โ”‚
โ”‚    renew_session โ†’ owner re-approves to extend play       โ”‚
โ”‚    fallback_tap โ†’ direct owner auth when session expires  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 5. RESULT                                                โ”‚
โ”‚    Compare scores โ†’ winner determined                    โ”‚
โ”‚    Stats recorded on-chain in PlayerProfile               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Cougr-Core Integration

This example showcases two key cougr-core features:

Passkey Authentication (secp256r1_auth)

#![allow(unused)]
fn main() {
use cougr_core::auth::{Secp256r1Key, Secp256r1Storage, verify_secp256r1};

// Register: store public key on-chain
Secp256r1Storage::store(&env, &player, &key);

// Authenticate: verify biometric signature
verify_secp256r1(&env, &pubkey, &message, &signature)?;
}

SessionManager Lifecycle

#![allow(unused)]
fn main() {
use cougr_core::accounts::SessionBuilder;
use cougr_core::session::SessionManager;

// 1. Approve once (after passkey auth)
let scope = SessionBuilder::new(&env)
    .allow_action(symbol_short!("tap"))
    .allow_action(symbol_short!("use_power"))
    .max_operations(500)
    .expires_in(duration)  // seconds from ledger timestamp
    .build_scope();
SessionManager::approve(&env, &player, scope)?;

// 2. Play many times without wallet prompts
SessionManager::execute_action(&env, &player, &session, action, intent_expires)?;

// 3. Poll health for renewal UI
let status = SessionManager::status(&env, &player, &key_id)?;

// 4. Renew before expiry (owner must require_auth)
SessionManager::renew(&env, &owner, &key_id, new_expires_at)?;

// 5. Fallback when session is stale
SessionManager::fallback_execute(&env, &session_intent, &direct_intent)?;
}

Sessions are managed by cougr_core::session::SessionManager - the same pattern as session_arena.

Duration semantics: Session expires_in / expires_at use ledger timestamps (seconds), not ledger sequence numbers. Round duration (start_round) still uses ledger sequences for combo timing.

ECS Components

ComponentFieldsPurpose
PasskeyIdentitypubkey, registered_atPlayer's passkey credential
TapCountercount, combo, multiplier, last_tap_ledgerTap tracking per round
PowerUpcharges, kindCharged abilities from combos
RoundStatestarted_at, duration, scores, finishedMatch state
PlayerProfiletotal_wins, total_taps, best_comboPersistent stats

Session state lives in SessionManager / SessionStorage - not as a custom ECS component.


Contract API

Authentication

FunctionParametersReturnsDescription
register_passkeyplayer, pubkey: BytesN<65>-Register a secp256r1 passkey
authenticate_and_start_sessionplayer, signature, challenge, durationAddressAuth + create session

Session Management

FunctionParametersReturnsDescription
session_statusowner, key_idSessionStatusRemaining ops, expiry, renewal hint
renew_sessionowner, key_id, expires_inActiveSessionExtend session (owner re-approves)
fallback_tapowner, key_idTapResultTap via session or direct auth fallback
get_latest_session_keyplayerBytesN<32>Latest session key ID for a player

Gameplay

FunctionParametersReturnsDescription
tapsession_keyTapResultSubmit a tap (gasless)
use_power_upsession_key, power_up: u32-Activate a power-up
start_roundplayer_a, player_b, duration-Start a match
get_round-RoundStateGet round state (auto-finalizes)
get_profileplayerPlayerProfileGet persistent stats

Power-Up Types

IDNameEffect
0DoubleTap+10 bonus points
1ShieldDefensive (blocks opponent effects)
2Burst+25 bonus points

Game Mechanics

Combo System

Taps within 5 ledger sequences of each other maintain a combo streak:

ComboMultiplierEffect
11xNormal tap
2-42-4xBuilding combo
55xEarns a power-up charge
10+10x (max)Maximum multiplier

Game Constants

ConstantValueDescription
COMBO_WINDOW5 ledgersTime window for combo
MAX_MULTIPLIER10Maximum score multiplier
COMBO_CHARGE_THRESHOLD5Combos to earn a power-up charge
DOUBLE_TAP_BONUS10DoubleTap power-up bonus
BURST_BONUS25Burst power-up bonus
DEFAULT_MAX_OPS500Default session operations

๏ธ Quick Start

Prerequisites

ToolVersionInstallation
Rust1.88.0+rustup.rs
Stellar CLILatestStellar Docs
WASM Target-rustup target add wasm32v1-none

Build

# Standard Rust build
cargo build

# Build WASM for Soroban deployment
stellar contract build

Test

cargo test

Test Results: 17 tests passing โœ…

TestDescription
test_register_passkeyPasskey registration stores key on-chain
test_register_multiple_playersMultiple players register independently
test_session_creationSession created and validated
test_session_ops_decrementOperations decremented per action
test_session_ops_exhaustedSession stops after ops limit
test_tap_incrementsTap counter increments correctly
test_combo_streakCombo streak with multiplier
test_multiplier_capMultiplier capped at MAX_MULTIPLIER
test_power_up_charge_and_useCombo charges power-ups
test_power_up_no_chargesNo activation without charges
test_start_roundRound initializes correctly
test_start_round_no_passkeyRound requires registered passkeys
test_round_scoring_and_winnerScores tracked per player
test_profile_statsProfile tracks taps and combos
test_default_profileDefault profile for new players
test_renew_session_extends_play_windowRenew extends absolute expiry
test_fallback_tap_uses_direct_auth_after_session_expiresFallback auth after expiry

Project Structure

examples/tap_battle/
โ”œโ”€โ”€ Cargo.toml          # Dependencies: cougr-core 1.1.0 + soroban-sdk
โ”œโ”€โ”€ README.md           # This documentation
โ””โ”€โ”€ src/
    โ”œโ”€โ”€ lib.rs          # Contract entry points
    โ”œโ”€โ”€ types.rs        # ECS Components (PasskeyIdentity, TapCounter, etc.)
    โ”œโ”€โ”€ auth.rs         # AuthSystem + SessionSystem (secp256r1 + SessionBuilder)
    โ”œโ”€โ”€ game.rs         # TapSystem, ComboSystem, PowerUpSystem, RoundSystem
    โ””โ”€โ”€ test.rs         # Unit tests (17 tests)

License

MIT OR Apache-2.0

Tetris On-Chain Game

Arcade Transitional
game-appecs

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

Purpose and pattern

Overview

This example demonstrates a falling-block board simulation on Soroban with Cougr ECS concepts. It remains transitional while the arcade examples converge on the canonical snake GameApp architecture.

Public contract API

Game Features

FunctionParametersReturn typeDescription
init_gamenoneGameStateInitializes an empty board and current/next pieces.
move_leftnoneboolAttempts to move the active piece left.
move_rightnoneboolAttempts to move the active piece right.
move_downnoneboolSoft-drops the active piece or locks it if blocked.
rotatenoneboolAttempts clockwise rotation.
dropnoneu32Hard-drops and locks the active piece; returns rows dropped.
update_ticknoneGameStateRuns one gravity tick.
get_statenoneGameStateReturns stored board and piece state.

Architecture overview

Quick Start

contract entrypoint
  โ”œโ”€ reads game state from Soroban storage
  โ”œโ”€ applies input or tick systems
  โ””โ”€ writes updated state back to storage

Board and active piece state are contract types. components.rs documents the piece components used for Cougr-facing structure; systems.rs owns movement, collision, locking, scoring, and tick helpers.

Storage model

Storage classDataWhy
Instance storagePer-contract game state where used by this example.Keeps small arcade state close to the contract instance.
Persistent storagePlayer- or world-scoped state where the example needs durable keyed state.Keeps game progress available across invocations.
Temporary storageNot used.The examples favor deterministic recalculation over ephemeral caches.

Main gameplay flow

  1. Call the initialization function to create the starting state.
  2. Submit an input action such as movement, jump, flap, rotation, or shoot.
  3. Call the tick/update function to run deterministic simulation logic.
  4. Query public getters for score, position, active state, or terminal status.
  5. Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.

Cougr APIs used

Deployment

Testnet Deployment

# Deploy to testnet
stellar contract deploy \
  --wasm target/wasm32v1-none/release/tetris.wasm \
  --source <YOUR_SECRET_KEY> \
  --network <NETWORK>
  • Network: Stellar Testnet
  • Contract ID: <CONTRACT_ID>
  • Explorer: https://stellar.expert/explorer/testnet/contract/<CONTRACT_ID>

Invoke Functions

# Initialize a new game
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source <YOUR_SECRET_KEY> \
  --network <NETWORK> \
  -- init_game

# Move piece left
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source <YOUR_SECRET_KEY> \
  --network <NETWORK> \
  -- move_left

# Update game tick (gravity + line clearing)
stellar contract invoke \
  --id <CONTRACT_ID> \
  --source <YOUR_SECRET_KEY> \
  --network <NETWORK> \
  -- update_tick

Benefits of Using Cougr-Core

Traditional Soroban vs. Cougr-Core

AspectTraditional SorobanWith Cougr-Core ECS
Code OrganizationMonolithic contract logicModular components & systems
State ManagementManual storage handlingAutomatic entity-component management
Game LogicTightly coupled functionsReusable, composable systems
ScalabilityDifficult to extendEasy to add new features
Code ReuseLimitedHigh - components are portable
TestingComplex integration testsUnit testable components

Cougr-Core Advantages

  1. Entity-Component-System Pattern

    • Separates data (components) from logic (systems)
    • Makes code more maintainable and testable
    • Enables parallel processing of game logic
  2. Simplified State Management

#![allow(unused)]
fn main() {
   // Traditional Soroban
   env.storage().instance().set(&DataKey::GameState, &state);
   
   // With Cougr-Core
   world.spawn_empty()
       .insert(Position { x: 5, y: 0 })
       .insert(Tetromino { shape: Shape::I });
}
  • ComponentTrait and custom component modules document the ECS data boundary.
  • SimpleWorld, SimpleQueryBuilder, GameApp, ScheduleStage, or SystemConfig are used where this transitional example has already adopted the maintained runtime shape.
  • Auth, privacy, ZK, and standards APIs are intentionally not used; these arcade examples focus on deterministic game logic.

Build and test commands

Testing

cargo test

stellar contract build

Known limitations

Run with output

cargo test -- --nocapture

Test specific function

cargo test test_rotate


### Test Coverage

| Test | Description |
|------|-------------|
| `test_init_game` | Verifies game initialization |
| `test_move_left` | Tests left movement |
| `test_move_right` | Tests right movement |
| `test_move_down` | Tests downward movement |
| `test_rotate` | Tests piece rotation |
| `test_update_tick` | Tests game tick and line clearing |
| `test_game_over` | Tests end game detection |

## Project Structure

examples/tetris/ โ”œโ”€โ”€ Cargo.toml # Dependencies & build config โ”œโ”€โ”€ .gitignore # Git ignore patterns โ”œโ”€โ”€ README.md # This file โ””โ”€โ”€ src/ โ””โ”€โ”€ lib.rs # Smart contract implementation


## Configuration

**Cargo.toml**
```toml
[dependencies]
soroban-sdk = "25.1.0"
cougr-core = "1.1.0"

Resources

Contributing

This example is part of the Cougr framework. Contributions are welcome!

License

  • Transitional code may preserve older storage or scheduling patterns for compatibility reference.
  • No authentication, matchmaking, real-time rendering, or production randomness is included.
  • One contract instance generally represents one game or one keyed set of player games.
  • For new work, prefer the canonical snake module split and GameApp tick wiring.

Tic Tac Toe

Board Canonical
soroban-gamerich-componentsgame-app
tic_tac_toe preview

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

An on-chain Tic Tac Toe game built with the Cougr ECS framework on Stellar Soroban. Note: although this example already uses cougr-core's newest macro-based component pattern (impl_rich_component! / impl_component! / impl_soroban_game!), it is not on the canonical examples list, so it is marked transitional per the project standard - see "Cougr APIs used" below for why these specific macros were chosen over the manual ComponentTrait implementations used in other transitional examples like reversi.

Purpose and pattern

This example demonstrates a two-player, perfect-information board game with a single shared entity for all game state. It showcases Cougr's SimpleWorld entity/component storage driven through the macro-generated RichComponentTrait/ComponentTrait implementations (impl_rich_component!, impl_component!) and the SorobanGame trait (impl_soroban_game!), which together remove the need to hand-write serialize/deserialize or repeat the storage key in every contract function.

Public contract API

FunctionParametersReturnsDescription
init_gameplayer_x: Address, player_o: AddressGameStateStarts a game with an empty 9-cell board and X moving first. Refuses to overwrite a game while its status is in progress (0).
make_moveplayer: Address, position: u32MoveResultRequires authorization from player, then validates and applies a move at position (0โ€“8). Illegal moves return success: false with a status message symbol.
get_state-GameStateCurrent board, both player addresses, whose turn it is, move count, and status.
is_valid_moveposition: u32boolWhether position is a legal move right now (in range, empty cell, game still in progress).
get_winner-Option<Address>The winning player's address, or None if the game is in progress or drawn.
reset_gameplayer: AddressGameStateRequires authorization from a registered player. Resets the board while keeping both players.

Status codes

GameState.status: 0 = in progress, 1 = X wins, 2 = O wins, 3 = draw.

MoveResult.message: ok, invalid (out of range), occupied, notturn, notplay, gameover.

Authentication and limitations

  • make_move requires Soroban authorization from player. The address must be a registered player, and it must be their turn.
  • reset_game requires authorization from one of the two registered players.
  • init_game refuses to replace a game in progress. A new game can be initialized after a win or draw.
  • The contract stores one game per contract instance. Initialization does not require authorization from both players.

Architecture overview

src/
โ”œโ”€โ”€ lib.rs          # #[contract] struct, SorobanGame wiring, #[contractimpl] entrypoints
โ”œโ”€โ”€ components.rs    # Board, Players, TurnState component structs + macro invocations
โ””โ”€โ”€ systems.rs       # detect_winner: pure win/draw detection over board cells

make_move runs a fixed validation/execution sequence synchronously on each call - there is no GameApp tick loop:

make_move
  โ””โ”€ load Players, TurnState, Board from the single game entity
  โ””โ”€ validate: game not over, position in range, caller is a player, caller's turn, cell empty
  โ””โ”€ apply the mark, call systems::detect_winner over the updated board
  โ””โ”€ persist updated Board and TurnState, save the world

components.rs owns the three SimpleWorld components and the shared GAME_ENTITY constant; systems.rs holds the one pure function (detect_winner) that contains no storage access; lib.rs owns the #[contract] struct, the SorobanGame wiring, and is the only module that loads/saves the SimpleWorld.

Storage model

All state lives in instance storage, managed transparently by impl_soroban_game! through SorobanGame::load_world/save_world under a single key ("ttt_world"). Within that one SimpleWorld, all three components (Board, Players, TurnState) are attached to a single fixed entity id (GAME_ENTITY = 1), since there is exactly one game per contract instance and no dynamic entity population to query. Instance storage is appropriate here because the game state must live for the lifetime of the contract instance with no per-entry TTL management.

Main gameplay flow

  1. A caller invokes init_game(player_x, player_o) to start a game with an empty board and X moving first. Initialization is rejected while a game is in progress.
  2. X calls make_move(player_x, position). The contract checks the game isn't over, the position is in range, the caller is a registered player, and it's their turn.
  3. On a legal move, the cell is marked, systems::detect_winner re-checks all 8 winning lines plus the draw condition, and turn state is updated (status, move count, whose turn is next).
  4. Players alternate make_move calls until detect_winner reports a win (status 1 or 2) or a draw (status 3), after which further moves return gameover.
  5. Either player can call get_state or get_winner to read the outcome. Either registered player can call reset_game(player) with authorization to start over.

Cougr APIs used

  • cougr_core::{impl_rich_component!} - used for Board (holds a Vec<u32>) and Players (holds two Address values). Both fields require Soroban's XDR codec rather than fixed-size byte packing, so impl_rich_component! was chosen to get RichComponentTrait for free from the #[contracttype] derive, avoiding a hand-written serialize/deserialize pair for Vec<u32> and Address like the one reversi's components.rs still carries.
  • cougr_core::{impl_component!} - used for TurnState, which is three fixed-size plain fields (bool, u32, u32). impl_component! generates a compact, fully typed ComponentTrait implementation (byte-packed, not XDR) since there are no Address/Vec fields needing the heavier rich-component codec.
  • cougr_core::game::SorobanGame / impl_soroban_game! - generates load_world/ save_world for the #[contract] struct so every entrypoint can read and persist the SimpleWorld without repeating the instance-storage key ("ttt_world") or its get/set boilerplate.
  • cougr_core::simple_world::SimpleWorld - used as the single source of truth for the game's three components, attached to one fixed entity rather than a dynamic population, since tic-tac-toe has exactly one game per contract instance.

This example does not use GameApp, ScheduleStage, SimpleQueryBuilder, auth, or privacy - see Known limitations.

Build and test commands

cargo test
stellar contract build

Known limitations

  • Does not use GameApp or ScheduleStage - validation and win detection run synchronously inside make_move since a turn-based game with one decision point per call has no need for staged scheduling.
  • Does not use SimpleQueryBuilder - there is exactly one game entity per contract instance, so there is no entity population to scan by component type.
  • No timeout/forfeit mechanism for an unresponsive player.
  • No spectator or replay API beyond the read-only getters.

Tower Defense Example

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 minimal tower defense game implemented as a Soroban smart contract using the cougr-core ECS framework. This example demonstrates wave spawning, enemy path progression, tower attacks, health reduction, and win/loss conditions.

Features

  • Wave Spawning: Enemies spawn in waves with increasing difficulty
  • Deterministic Path Progression: Enemies follow a predefined path toward the base
  • Tower Placement: Three tower types with different stats (Basic, Sniper, Splash)
  • Attack Resolution: Towers automatically target and damage enemies in range
  • Win/Loss Conditions: Survive all waves to win, or lose when base health reaches 0

ECS Architecture

Components

ComponentFieldsPurpose
EnemyComponenthp, max_hp, speed, path_indexRepresents enemies moving along the path
TowerComponentkind, range, damage, cooldownRepresents static defenders
WaveComponentcurrent_wave, total_waves, remaining_spawnsTracks wave progression
BaseComponenthealth, max_healthTracks survival condition
GameStatusComponentstatus, tick_count, enemies_killedTracks game state

Systems

  • WaveSpawnSystem: Spawns enemies according to wave configuration
  • PathProgressionSystem: Moves enemies along the predefined path
  • AttackResolutionSystem: Towers target and damage enemies in range
  • BaseDamageSystem: Reduces base health when enemies reach the end
  • EndConditionSystem: Checks win/loss conditions

Contract API

fn init_game(env: Env)

Initializes a new game with default settings.

fn place_tower(env: Env, x: u32, y: u32, tower_kind: u32) -> bool

Places a tower at the specified coordinates.

  • tower_kind: 0=Basic, 1=Sniper, 2=Splash
  • Returns true if placement was successful

fn advance_tick(env: Env)

Advances the game by one tick, executing all game systems.

fn get_state(env: Env) -> GameState

Returns the current game state including base health, wave info, and status.

fn is_finished(env: Env) -> bool

Returns true if the game has ended (won or lost).

fn get_result(env: Env) -> u32

Returns the game result: 0=active, 1=won, 2=lost.

Setup

  1. Navigate to the example directory:

    cd examples/tower_defense
    
  2. Build the contract:

    cargo build
    
  3. Run tests:

    cargo test
    

Validation Commands

cd examples/tower_defense
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
stellar contract build

Game Configuration

  • Map Size: 10x10 grid
  • Path Length: 8 waypoints
  • Total Waves: 5
  • Enemies per Wave: 5
  • Base Health: 100
  • Enemy Base Damage: 10

Tower Stats

TowerRangeDamageCooldown
Basic2101 tick
Sniper4253 ticks
Splash1152 ticks

Trading Card Game

Card Transitional
authsession-authecs

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

A two-player on-chain trading card game built with the Cougr ECS framework on Stellar Soroban. This example demonstrates atomic multi-action turns via BatchBuilder and match-scoped sessions via SessionBuilder.

Why Atomic Turns Matter

In a trading card game a single turn consists of multiple sequential actions: play a creature, cast a spell, declare attacks. If the player's actions were submitted as separate transactions:

  • A partially-executed turn could leave the game in an invalid intermediate state (e.g., a card removed from hand but never placed on the field).
  • The opponent could observe and exploit that intermediate state.
  • A wallet approval prompt between every action would create terrible UX.

BatchBuilder solves all three problems. The entire turn - every PlayCreature, CastSpell, and DeclareAttack - is composed into a single batch. Because a Soroban contract invocation is atomic by design, any panic inside submit_turn reverts every storage write made during that call. Either the whole turn succeeds, or nothing changes.

How it works

submit_turn(player, actions)
  โ”‚
  โ”œโ”€ Guard checks (turn ownership, session validity)
  โ”‚
  โ”œโ”€ DrawSystem   โ† draw one card at turn start
  โ”œโ”€ ManaSystem   โ† increment max-mana, refill current mana
  โ”‚
  โ”œโ”€ BatchBuilder โ† assemble one GameAction per player action
  โ”‚                 (proves intent; acts as the turn manifest)
  โ”‚
  โ”œโ”€ For each action in the batch:
  โ”‚     PlayCreature  โ†’ PlayCardSystem   (validate mana, move card to field)
  โ”‚     CastSpell     โ†’ CastSpellSystem  (validate mana, deal damage)
  โ”‚     DeclareAttack โ†’ CombatSystem     (resolve damage, remove dead creatures)
  โ”‚                     โ†‘ any PANIC here reverts the entire call
  โ”‚
  โ”œโ”€ WinConditionSystem โ† check health totals
  โ””โ”€ Advance turn

If, say, the second action in the batch runs out of mana, the contract panics, and all state changes (including the first action that succeeded) are discarded - the player's hand, field, stats, and mana are exactly as they were before the call.

Session Keys

Each player calls start_session at the beginning of the match. This builds a SessionScope (via SessionBuilder) that:

  • Allows only the three game actions: play, spell, attack.
  • Caps the session at 200 operations.
  • Expires after a 2-hour TTL (7 200 ledger seconds).

submit_turn validates the session before executing any actions. If the session has expired, the entire call is rejected - no stale turns can sneak in after a match is effectively abandoned.

Game Flow

1. MATCH SETUP
   new_match(player_a, player_b, deck_a, deck_b)
     โ†’ Both players draw STARTING_HAND_SIZE cards
     โ†’ stats: health=20, mana=1/1

   start_session(player_a)   โ†’ returns expiry timestamp
   start_session(player_b)   โ†’ returns expiry timestamp

2. EACH TURN  (atomic batch)
   submit_turn(active_player, [
     PlayCreature(card_id),
     CastSpell(card_id),
     DeclareAttack(attacker_idx, target_idx),
     ...
   ])
   โ†’ DrawSystem:   draw 1 card
   โ†’ ManaSystem:   max_mana++, mana refill
   โ†’ BatchBuilder: compose action manifest
   โ†’ Execute actions atomically
   โ†’ WinCondition: check health == 0
   โ†’ Advance turn

3. GAME END
   status == StatusAWins | StatusBWins | Conceded

Card Library

IDKindCostPowerToughnessNotes
1Creature112Cheap blocker
2Creature222Vanilla
3Creature213Sturdy blocker
4Creature332Aggressive
5Creature324Resilient
6Creature444Mid-range
7Creature555Threat
8Creature666Late-game bomb
9Spell23-3 direct damage
10Spell45-5 direct damage

ECS Components

ComponentFieldsPurpose
PlayerHandcards: Vec<CardId>Cards currently held
PlayerFieldcreatures: Vec<CreatureState>Creatures on the battlefield
PlayerStatshealth, mana, max_manaHealth and resource pool
MatchStateturn, active_player, phase, statusMatch phase tracking

ECS Systems

SystemTriggerResponsibility
DrawSystemTurn startDraw one card from deck into hand
ManaSystemTurn startIncrement max mana (cap 10), refill mana
PlayCardSystemPlayCreatureValidate mana, move card to field
CastSpellSystemCastSpellValidate mana, deal damage to opponent
CombatSystemDeclareAttackResolve creature-vs-creature or face damage
WinConditionSystemEnd of turnDetect health == 0, set match status

Contract API

#![allow(unused)]
fn main() {
// Setup
fn new_match(env, player_a, player_b, deck_a, deck_b)
fn start_session(env, player) -> u64  // returns session expiry

// Gameplay
fn submit_turn(env, player, actions: Vec<Action>) -> TurnResult
fn concede(env, player)

// Queries
fn get_state(env) -> MatchState
fn get_hand(env, player) -> Vec<Card>
fn get_field(env) -> FieldState
fn get_stats(env, player) -> PlayerStats
}

Building and Testing

# Build
cargo build

# Run tests
cargo test

# Build WASM contract
stellar contract build

Dependencies

[dependencies]
soroban-sdk = "25.1.0"
cougr-core  = { branch = "main", git = "https://github.com/salazarsebas/Cougr.git" }

Resources

Treasure Hunt (Merkle Map Pattern)

Hidden Information Transitional
authprivacy

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

A single-player exploration game demonstrating hidden information using Merkle map commitments and sparse fog-of-war on Stellar Soroban. The full map is committed off-chain, and players prove cell contents via Merkle inclusion proofs as they explore.

Status

Transitional - uses cougr-core = "1.1.0" and privacy::stable Merkle primitives (MerkleTree, SparseMerkleTree, verify_inclusion, OnChainMerkleProof). See battleship for the canonical hidden-information reference.

The Hidden Information Pattern

Large maps are too expensive to store fully on-chain. This example keeps on-chain storage bounded:

  • Full map remains off-chain, committed as a Merkle root
  • Contract stores only: committed root, player state, and explored-cell sparse state
  • Players reveal cells one at a time using Merkle inclusion proofs

Commit Phase (Off-Chain)

Map Generator:
โ”œโ”€ Encode each cell as 32-byte leaf: [x(4) | y(4) | value(1) | padding(23)]
โ”œโ”€ Build SHA256 Merkle tree from all leaves
โ”œโ”€ Store root hash on-chain via init_game()
โ””โ”€ Keep full tree off-chain for proof generation

Reveal Phase (On-Chain)

Player:
โ”œโ”€ Moves to adjacent cell (x, y)
โ”œโ”€ Submits: cell_value + MerkleProof(siblings, leaf_hash, leaf_index)
โ””โ”€ Contract:
    โ”œโ”€ verify_inclusion(proof, committed_root) โ†’ true/false
    โ”œโ”€ Applies discovery (treasure โ†’ +score, trap โ†’ -health)
    โ””โ”€ Updates sparse fog-of-war via SparseMerkleTree

When to Use Commit-Reveal vs ZK Circuits

Use commit-reveal with Merkle proofs (this pattern) when you need to hide large state spaces (maps, card decks, board layouts) and reveal them incrementally. Merkle proofs provide O(log n) verification without exposing the full state.

Use ZK circuits when the game logic itself must remain private - proving validity of moves without revealing any state. See hidden_hand for a canonical ZK circuit example, and proof_of_hunt for Groth16-based hidden state.

Contract API

Public Functions

FunctionParametersDescription
init_gameplayer: Address, map_root: BytesN<32>, width: u32, height: u32, total_treasures: u32Initialize game with committed map root
exploreplayer: Address, x: u32, y: u32, cell_value: u32, proof: OnChainMerkleProofExplore a cell with Merkle proof

Migration note (explore proof parameter)

explore now accepts cougr_core::privacy::stable::OnChainMerkleProof instead of Vec<BytesN<32>>. Build proofs with Sha256MerkleProofVerifier::to_on_chain_proof (or to_on_chain_proof helper) before calling the contract. | get_state | - | Return complete GameState | | is_explored | x: u32, y: u32 | Check if a cell has been revealed |

Data Types

#![allow(unused)]
fn main() {
enum GameStatus { Active, Won, Lost }

struct MapRoot {
    root: BytesN<32>,   // Merkle root of full map
    width: u32,
    height: u32,
    total_treasures: u32,
}

struct PlayerState {
    x: u32, y: u32, health: u32, score: u32, treasures_found: u32
}

struct ExploredMap {
    explored: Map<u32, bool>  // Sparse fog-of-war tracking
}
}

Storage Model

Committed State

Stored on-chain during initialization:

  • map_root - SHA256 Merkle root of the full encoded map
  • Map dimensions (width, height) and total_treasures
  • fog_root - initial sparse Merkle tree root (all cells unexplored)

Revealed State

Updated during exploration:

  • explored_map.explored - sparse map of visited cell indices
  • player_state - position, health, score, treasures found
  • fog_root - recomputed SparseMerkleTree root after each exploration

Proven State

The explore function verifies cell contents via Merkle inclusion:

  1. Reconstructs the leaf hash from (x, y, cell_value) using the same encoding as the commit phase
  2. Builds OnChainMerkleProof from sibling hashes and path bits
  3. Calls verify_inclusion(&env, &proof, &map_root.root) - rejects if proof is invalid
  4. Only if verification passes is the discovery applied and the cell marked explored

Cell Encoding

Each map cell is encoded into a 32-byte leaf:

bytes [0..4]:   x coordinate (big-endian u32)
bytes [4..8]:   y coordinate (big-endian u32)
byte [8]:       cell_value (0=empty, 1=treasure, 2=trap)
bytes [9..31]:  zero padding

The off-chain generator hashes each cell and builds a SHA256 Merkle tree using MerkleTree::from_leaves() from privacy::stable.

Building & Testing

Prerequisites

  • Rust 1.88.0+
  • Stellar CLI 25.0.0+ (optional)

Build

cargo build
cargo build --release --target wasm32v1-none

Test

cargo test

Test Coverage:

  • โœ… Game initialization with committed map root
  • โœ… Valid exploration with correct Merkle proof
  • โœ… Invalid proof rejection
  • โœ… Treasure scoring and trap damage
  • โœ… Re-exploration rejection
  • โœ… Win condition (all treasures found)
  • โœ… Loss condition (health reaches zero)
  • โœ… Full playable sequence from init to terminal state
  • โœ… Sparse fog root updates after exploration
  • โœ… Non-adjacent move rejection
  • โœ… Commit phase validation
  • โœ… Reveal phase with valid proof
  • โœ… Invalid reveal value rejection

Security Considerations

โœ… Secure

  • Map commitment binding: Merkle root prevents changing cell contents after init
  • Selective reveal: Only explored cells are revealed; unexplored cells stay hidden
  • Proof verification: Invalid proofs are rejected by verify_inclusion()
  • Adjacency enforcement: Players can only move to adjacent cells

โš ๏ธ Important

  • Off-chain map trust: The map must be generated honestly; the contract trusts the committed root
  • Sparse fog-of-war: Recomputed on every move for full explored-cell tracking

Deployment

stellar keys generate treasure-hunt-deployer --network testnet --fund
stellar contract deploy \
  --wasm target/wasm32v1-none/release/treasure_hunt.wasm \
  --source treasure-hunt-deployer \
  --network testnet

Resources

License

MIT OR Apache-2.0

zk_dice_session

Other Canonical
soroban-gamezk-circuitssession-authprivacy

A composition example: fair_dice ZK verification authorized through a session key.

This example wires two existing Cougr subsystems together for the first time. It does not introduce a new circuit or a new account model โ€“ only the integration between them.

SubsystemSource example this builds on
ZK proof verification (fair_dice circuit)examples/dice_duel
Session key lifecycle (SessionBuilder / SessionManager)examples/session_arena

Why this matters

Every ZK-circuit example in the repo re-signs with the owner key on every proof submission. A player who wants ten rapid dice rolls pays ten wallet prompts.

session_arena (and tap_battle) show how to approve a scoped key once and act many times without re-signing, but neither example touches a ZK proof.

zk_dice_session closes that gap: a player approves once, then rolls as many times as the session budget allows โ€“ each roll verified by an on-chain Groth16 proof, each authorized by the session key, zero additional wallet prompts.


Contract API

init_game(sides, seed_commitment) โ†’ GameConfig

Initialises the game with a die face count (โ‰ฅ 2) and a Pedersen seed commitment. Internally calls fair_dice() to validate the parameters; the circuit spec is reconstructed from storage on every roll call.

open_session(owner, max_rolls, expires_in) โ†’ ActiveSession

Creates a session scoped to the "roll" action. Requires one owner wallet signature. Returns an ActiveSession containing the key_id needed for subsequent roll calls.

roll(owner, key_id, roll_result, nonce, proof) โ†’ bool

Submits a Groth16 proof of a fair dice roll. Authorization happens in two gates:

  1. Session gate โ€“ SessionManager::execute_action checks expiry, budget, and action scope. No wallet prompt.
  2. ZK gate โ€“ spec.verify_dice_roll is called identically to the corresponding call in dice_duel. The proof format is unchanged.

Returns true if both gates pass, false if the proof is invalid. Panics (rolls back the transaction) if the session is missing, expired, or budget-exhausted.

last_roll(player) โ†’ RollRecord

Returns the last accepted roll for player.


Quick start

# From the repo root
cd examples/zk_dice_session
cargo test
stellar contract build

Test coverage

TestWhat it proves
roll_succeeds_inside_active_sessionRoll accepted when session is active (DoD)
roll_fails_with_no_sessionRoll panics when no session key exists (DoD)
roll_fails_after_session_expiryRoll panics once the session window closes (DoD)
roll_returns_false_for_invalid_proofOut-of-range roll_result โ†’ false, proof gate intact
roll_fails_when_session_budget_exhaustedBudget exhaustion panics before proof verification
independent_sessions_for_multiple_playersEach player's session is isolated
init_game_rejects_single_sided_dieCircuit parameter contract enforced
circuit_spec_matches_fair_diceCircuit ID matches FairDice (same path as dice_duel)

Cougr features demonstrated

  • zk-circuits โ€“ fair_dice verifier, Groth16Proof, CircuitId::FairDice
  • session-auth โ€“ SessionBuilder, SessionManager, SessionStorage, ActiveSession
  • soroban-game โ€“ #[contract] / #[contractimpl] / #[contracttype]

Design

โณ This section is being built. All design documents are gaps identified in docs/strategy/12-documentation-architecture.md and will be produced alongside the design system rollout.


The Design section contains guidelines for anyone building a client application against Cougr, contributing visual work, or working on the docs site itself.

GuideStatus
Branding Guide๐Ÿ”œ Coming soon
UI Guidelines๐Ÿ”œ Coming soon
UX Guidelines๐Ÿ”œ Coming soon
Accessibility๐Ÿ”œ Coming soon

Branding Guide

โณ This page is being written.

Will cover: logo usage, color palette, typography, design tokens, and the "Cougr Verified" badge. Full specification in docs/strategy/09-design-strategy.md in the main repository.

UI Guidelines

โณ This page is being written.

Will cover UI patterns for building a client application against a Cougr game contract. The murdoku frontend in the main repo is the first worked reference this guide will draw from.

UX Guidelines

โณ This page is being written.

Will cover player-facing UX patterns - wallet connection, session key UX, and transaction feedback - distinct from the developer UX covered in the Learn section.

Accessibility

โณ This page is being written.

Will cover accessibility requirements for both this documentation site and for client applications built on Cougr. Currently unaddressed anywhere in the project - flagged in docs/strategy/12-documentation-architecture.md.

Community

Welcome to the Community section. This is where you'll find everything about how to contribute, how decisions are made, and how to stay up to date.

DocumentDescription
ContributingHow to open issues, write code, and get PRs merged
Code of ConductExpected behaviour in all project spaces
GovernanceHow decisions are made, who can merge, how disputes are resolved
SecurityHow to report vulnerabilities
RoadmapWhere the project is headed
RFC ProcessHow to propose significant changes before implementing them
ChangelogWhat changed in each release

Contributing

Contributions should improve the framework, the example catalog, or the supporting documentation with a clear purpose. This repository is structured to be useful both as a reusable library and as a reference codebase, so changes should optimize for correctness, clarity, and maintainability.

Scope

Good contributions typically fall into one of these categories:

AreaExpected outcome
Core frameworkImproved ECS, scheduling, storage, authorization, or zero-knowledge capabilities
ExamplesNew game patterns, better reference implementations, or tighter example documentation
DocumentationClearer architecture, setup, or usage guidance aligned with the current codebase
QualityBetter tests, tooling, validation, or CI coverage

Development Standards

  • Keep changes focused. Avoid mixing unrelated refactors with feature work.
  • Update documentation when behavior, structure, or public APIs change.
  • Prefer clear names and straightforward control flow over clever abstractions.
  • Preserve repository consistency. New files should fit the existing layout and conventions.
  • Do not add generated reports, ad hoc summaries, or temporary planning documents to the repository root.

Local Validation

Run this from the repository root before you open or update a pull request:

make check

It runs the same checks as CI for the parts of the repository your branch changes (core library, CLI, examples, docs site, catalog, SDKs, clients, studio) and ends with a pass/fail summary. Fix everything it reports and run it again until it passes. A pull request is only merged when every CI workflow passes.

  • make fix applies the automatic fixes: cargo fmt and the docs site sync.
  • make check-all runs every check, whatever you changed.
  • If a tool is missing (for example stellar, mdbook, or cargo-deny), make check tells you how to install it. Skipped checks still run in CI.
  • CI tests your branch merged into main. Keep it current with git fetch upstream && git rebase upstream/main (add the remote once with git remote add upstream https://github.com/salazarsebas/Cougr.git).

If you use an AI assistant, see AGENTS.md.

Documentation Expectations

Documentation should be professional, current, and proportionate:

  • avoid stale exact counts when the repository is expected to grow
  • explain decisions and usage patterns without turning every page into a long-form essay
  • use tables when they improve scanability, not as a default for all content
  • keep root-level documentation limited to material with clear long-term value
  • follow the terminology and voice rules in docs/VOICE_GUIDE.md for all doc, example, and marketing copy

Pull Requests

Pull requests should make it easy to review technical intent. A strong PR description usually covers:

  1. what changed
  2. why the change was needed
  3. how it was validated
  4. any follow-up work or constraints reviewers should know about

Adding Examples

When adding a new example:

  • make the example self-contained

  • include a local README.md

  • keep the example focused on one or two clear patterns

  • add CI coverage when the example is meant to remain a maintained reference

  • add a matching [example.<name>] entry to examples/catalog.toml and validate the catalog locally (requires Python 3.11 or newer):

    python3 scripts/validate_catalog.py
    

    The validator checks required fields, allowed category and maturity values, that each name maps to a directory under examples/, that cougr_features is a non-empty list, and that any screenshot path exists. examples/catalog_broken.toml is a deliberately invalid fixture; running the validator against it must exit non-zero, which is how CI confirms the rules still fire:

    python3 scripts/validate_catalog.py examples/catalog_broken.toml  # expected to fail
    

Review Criteria

Changes are more likely to be accepted when they:

  • solve a real problem in the framework or examples
  • keep the API and repository structure coherent
  • include appropriate validation
  • improve the repository without increasing maintenance noise

Public API Checklist

Changes that touch public Rust APIs should be reviewed against this checklist before merge:

  • the symbol belongs to the curated onboarding path or an intentional namespace such as accounts, zk::stable, or zk::experimental
  • stable, beta, experimental, and test-only surfaces are not mixed in the same default entrypoint
  • new public names do not duplicate an existing public concept
  • root-level re-exports are intentional and minimal
  • examples and integration tests use the sanctioned public path instead of deep internal module paths
  • documentation is updated to match the actual exported API

Code of Conduct

All contributors are expected to follow the project's Code of Conduct. Reports of unacceptable behavior can be sent to the project maintainers through GitHub.

Code of Conduct

โš ๏ธ This document is an urgent gap identified in docs/strategy/12-documentation-architecture.md: "Missing despite 25+ active external contributors. Should be added immediately, independent of any other work in this package - this is a near-zero-cost fix for a real, present governance gap."

Tracked in: salazarsebas/Cougr issues


A Code of Conduct will be added here imminently. It will be based on the Contributor Covenant and apply to all project spaces including GitHub Issues, Pull Requests, and any community channels.

Governance

โณ This page is being written.

Identified as a gap in docs/strategy/12-documentation-architecture.md. No documented decision-making process exists yet.


Will cover:

  • Who can merge pull requests
  • How disputes over public API changes are resolved
  • How maintainer status is granted and revoked
  • The relationship between the existing CONTRIBUTING.md Public API Checklist and final authority on API decisions

Tracked in: salazarsebas/Cougr issues

Security Policy

Status

Cougr now defines a 1.0.0 stable contract for a scoped subset of the crate. Not every public subsystem is part of that stable guarantee.

Security-sensitive areas include:

  • account authorization
  • session lifecycle and replay protection
  • persistent storage integrity
  • proof verification and privacy primitives
  • ECS mutation ordering where authorization depends on state transitions

Maturity and Guarantees

Current guidance:

AreaStatusGuidance
ECS runtime and storageStablePart of the 1.0 contract when used through the documented onboarding and runtime surfaces
Accounts and smart-account flowsBetaDo not assume full production guarantees without project-specific review
Standards layer (standards)StableReusable contract primitives are part of the 1.0 stable contract
Privacy primitives (zk::stable)StableCommit-reveal, hidden-state codecs, and Merkle utilities are the stable privacy contract
Advanced ZK verificationExperimentalTreat as non-stable until verification contracts and assumptions are fully hardened

The latest maturity definitions live in docs/MATURITY_MODEL.md. The current threat-model baseline lives in docs/THREAT_MODEL.md. The explicit compatibility story lives in docs/COMPATIBILITY_PROMISES.md.

Threat Model Expectations

Cougr does not currently claim:

  • external audit coverage
  • formal verification
  • full production guarantees across all auth and privacy paths
  • stable compatibility guarantees for experimental modules

Before adopting Cougr in security-critical deployments, review at minimum:

  • auth and signer flows
  • replay handling
  • session scope and revocation rules
  • storage schema assumptions
  • proof verification assumptions

Reporting a Vulnerability

If you find a security issue:

  1. Do not open a public issue with exploit details.
  2. Report the issue privately to the project maintainers.
  3. Include:
    • affected module
    • reproduction steps
    • impact assessment
    • version or commit information
    • suggested mitigation if available

The monitored disclosure channel is GitHub Private Vulnerability Reporting:

https://github.com/salazarsebas/Cougr/security/advisories/new

security@cougr.dev is reserved as a future dedicated inbox and is not yet independently staffed. Do not rely on it for acknowledgment SLAs.

Supported Versions

The latest stable release line and current mainline development state should be assumed relevant for fixes unless a maintenance policy says otherwise.

Secure Contribution Expectations

Changes affecting auth, privacy, storage, or unsafe internals should include:

  • updated invariants or trust assumptions
  • negative-path tests
  • compatibility notes when public behavior changes
  • documentation changes when guarantees or maturity shift

Roadmap

โณ This page is being written.

No ROADMAP.md exists in the main repository yet. Per docs/strategy/12-documentation-architecture.md: "Should be a public, living version of 13-roadmap.md, updated quarterly, not a one-time publish."


The public roadmap will be maintained here once established. It will track:

  • Near-term: cougr-cli (cougr new, cougr add, cougr check)
  • Medium-term: TypeScript client SDK, resource-cost reporting in test harness
  • Long-term: Visual editor, showcase gallery, hosted-service option

Tracked in: salazarsebas/Cougr issues

RFC Process

โณ This page is being written.

The ADR practice (docs/adr/) covers internal architecture decisions well. An RFC process is the public-facing counterpart for changes the community should weigh in on before they happen.

Per docs/strategy/12-documentation-architecture.md: "Recommend adopting a lightweight RFC template modeled directly on the existing ADR format, since the team already has the discipline to use it well."


When the RFC process is established it will cover:

  • What kinds of changes require an RFC (public API changes, new primitives, breaking changes)
  • The RFC template (based on the existing ADR format)
  • How to submit, how long the comment period is, and who has final say

Tracked in: salazarsebas/Cougr issues

Changelog

Unreleased

Changed

  • Upgrade the workspace and path-dependent examples to Soroban SDK 29.0.1 and Rust 1.91.0.

Added

  • cougr_core::accounts::AccountError::DuplicateDeviceKey - new error variant (code 45) returned when attempting to register a device key that is already active, distinguishing "device already registered" from "device limit reached"
  • cougr_core::accounts::derive_session_key_id - deterministic session-key identifier derivation, so the Rust core and the TypeScript session client agree on session identity instead of re-deriving it off-chain
  • cougr_core::cors - CORS configuration validation and dynamic origin allowlist for HTTP gateways in front of a game contract: CorsConfig validates origins, methods, header names, and credential/wildcard combinations; OriginAllowlist supports runtime add/remove of exact, single-label wildcard subdomain (https://*.example.com), and global * entries with case/default-port-normalized matching; preflight evaluation returns ready-to-emit response headers
  • cougr-cli - new workspace member publishing the cougr binary
  • cougr new <name> [--template <name>] - scaffolds a Soroban game contract crate following the canonical lib.rs / components.rs / systems.rs layout, with a passing test::GameHarness suite and a dependency on the published cougr-core release rather than a path dependency
  • Four embedded templates, each derived from a canonical example and compiled into the binary so cougr new works offline: starter (spawn_and_move), turn-based (tic_tac_toe), hidden-info (hidden_hand), session-auth (session_arena)
  • CLI CI workflow - validates cougr-cli once, caches the root workspace target, and compiles generated projects when template sources change

Fixed

  • zk::merkle::proof::verify_inclusion - a proof's leaf_index was never checked against the position its path_bits actually walk, so a proof for one leaf could be relabelled to claim another leaf's position and still verify. verify_inclusion now rejects a path_bits/leaf_index mismatch (ZKError::LeafIndexMismatch), an out-of-range leaf_index (ZKError::LeafOutOfBounds), and depth > MAX_DEPTH (ZKError::MaxDepthExceeded, previously an overflow panic on path_bits >> i for depth > 32) before hashing. zk::merkle::tree::verify_proof, the in-memory equivalent, gets the same path_indices/leaf_index check
  • zk::merkle::proof::verify_inclusion_at_depth - new function that additionally pins the proof to an exact expected depth, so a shorter-than-expected proof (including a degenerate depth = 0 proof with leaf == root) is rejected even though it would otherwise be internally consistent
  • examples/battleship, examples/treasure_hunt - both pin the expected proof depth and recheck path_bits against the claimed cell locally, since they still depend on the published cougr-core = "1.1.0" and won't pick up the core fix until a release

1.1.0

Added

  • game::SorobanGame trait - standard load_world / save_world contract pattern; implement once with impl_soroban_game!(Contract, "key"), use in every entrypoint
  • impl_soroban_game! macro - wires SorobanGame to any #[contract] struct
  • SimpleWorld::load_from_instance - load world from Soroban instance storage, returning a fresh empty world on first call
  • SimpleWorld::save_to_instance - persist world to Soroban instance storage
  • SimpleWorld::set_rich_observed - store a rich component and emit a RichComponentChangedEvent for off-chain indexers
  • SimpleWorld::remove_rich_observed - remove a rich component and emit a del event
  • RichComponentChangedEvent - new Soroban event type with topics ("COUGR", "rich", component_type) for rich component change notifications
  • spawn_and_move example - canonical Cougr starter game demonstrating the complete idiomatic pattern: impl_component_observed! + SorobanGame + typed ECS access
  • SorobanGame re-exported from prelude - import from cougr_core::prelude::*
  • cougr_core::circuits - four pre-built ZK game builders (hidden cards, fog of war, fair dice, sealed bid) with pipeline-embedded verification keys
  • cougr_core::session - SessionManager, SessionStatus, and ActiveSession (Beta)
  • cougr_core::test - GameHarness, Scenario, and ReplayLog sandbox behind the testutils feature
  • Circom pipeline - internal/cougr-core-circuits with CI workflow and on-chain Groth16 proof verification using real VKs
  • ZK examples - hidden_hand, fog_explorer, dice_duel, and blind_auction
  • Workspace subcrates - internal/cougr-core-{circuits,session,test} per ADR 0007

Changed

  • tic_tac_toe example modernised: replaced ~200 lines of manual serialization with impl_rich_component! for Board and Players, and impl_soroban_game! for load/save. Public API is unchanged; all existing tests pass
  • README rewritten with clean 30-line quick start and full feature documentation
  • canonical example set expanded from three (snake, battleship, guild_arena) to ten: spawn_and_move (Starter), tic_tac_toe (Rich components), session_arena (Session UX), hidden_hand, fog_explorer, dice_duel, blind_auction (ZK circuits), snake (Arcade/GameApp), battleship (Hidden information), guild_arena (Auth & recovery)
  • session_arena example added as canonical reference for session::SessionManager

Stability Notes

  • game::SorobanGame is Stable
  • SimpleWorld::load_from_instance / save_to_instance are Stable
  • set_rich_observed / remove_rich_observed are Stable
  • RichComponentChangedEvent is Stable
  • cougr_core::session is Beta
  • cougr_core::circuits and embedded test VKs are Experimental
  • cougr_core::test is Experimental (testutils only)

1.0.0

Added

  • app as the default gameplay runtime surface
  • auth, privacy, and ops as product-level domain namespaces
  • RuntimeWorld and RuntimeWorldMut as shared Soroban-first backend contracts
  • stronger stage scheduling with ordering, sets, and validation
  • SimpleQueryBuilder, query state/cache improvements, and richer ArchetypeWorld query helpers
  • expanded benchmark coverage for backend comparisons and cache invalidation behavior

Changed

  • the recommended onboarding path is now app::GameApp + SimpleWorld + SimpleQueryBuilder
  • canonical examples now emphasize the curated runtime story and explicit maturity boundaries
  • battleship now uses stable privacy primitives from zk::stable
  • documentation now treats SimpleWorld and ArchetypeWorld as the defended Soroban-first backends

Stability Notes

  • Stable: ECS onboarding/runtime contract, app, ops, standards, privacy::stable, zk::stable
  • Beta: auth, accounts, game_world
  • Experimental: privacy::experimental, zk::experimental, hazmat cryptographic helpers

Upgrade Notes

  • Prefer app over wiring scheduler/world primitives directly for new gameplay code
  • If you still have pre-1.0 code built around removed runtime abstractions, port directly to GameApp, SimpleWorld, and SimpleQuery
  • Prefer ops, privacy, and auth in application code when you want domain-oriented imports
  • Treat root-level advanced re-exports as compatibility/advanced surfaces rather than the default learning path
  • See docs/MIGRATION_GUIDE.md for concrete migration mappings