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:
| Tool | Install command |
|---|---|
| Rust | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs |
| Stellar CLI | cargo install stellar-cli --features opt |
| wasm32 target | rustup 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
- 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.
- 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.
- 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
murdokuorbattleship) in theexamples/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.
| Guide | Status |
|---|---|
| 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:
| Backend | File | Strategy | Best for |
|---|---|---|---|
| SimpleWorld | src/simple_world/ | Map<(EntityId, Symbol), Bytes> with dual Table/Sparse indexes | General use, small entity counts |
| ArchetypeWorld | src/archetype_world/ | Groups entities by component signature | Large 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:
| Macro | When 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, behindhazmat-cryptofeature - 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) -GameCircuittrait + pre-built circuits (Movement, Combat, Inventory, TurnSequence) +CustomCircuitBuilder - ECS integration (
components.rs,systems.rs) -CommitReveal,HiddenState,ProofSubmissioncomponents 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:
OwnableandOwnable2Stepfor owner-managed authorityAccessControlfor role-based authorization with delegated adminsPausablefor emergency stopsExecutionGuardfor serialized critical sectionsRecoveryGuardfor blocking sensitive paths during recovery windowsBatchExecutorfor bounded multi-operation flowsDelayedExecutionPolicyfor 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 module | Source | Maturity | Feature |
|---|---|---|---|
cougr_core::circuits | src/circuits/ | Experimental | always |
cougr_core::session | src/session/ | Beta | always |
cougr_core::test | src/test/ | Beta | testutils |
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
| Flag | Enables |
|---|---|
hazmat-crypto | Poseidon2 hash, BN254 curve ops (via soroban-sdk/hazmat-crypto) |
testutils | cougr_core::test sandbox, MockAccount, Soroban test helpers |
debug | Runtime 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... | Use | Example | Read more |
|---|---|---|---|
| Fairness (a roll, a draw, an outcome no one can predict or bias) | circuits::FairDiceBuilder - on-chain Groth16-verified randomness (Experimental) | dice_duel | Hidden Information Guidance below, PRIVACY_MODEL.md |
| Hidden information (cards, ship positions, sealed bids - state some players shouldn't see) | privacy::stable commit-reveal + Merkle primitives | battleship (canonical), rock_paper_scissors, hidden_hand, blind_auction | Hidden 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 change | spawn_and_move (start here), snake | System 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 AccountKernel | guild_arena | ACCOUNT_KERNEL.md ยง Signers |
| A session players approve once, not per-transaction | Session signer + SessionPolicy (scope, expiry, operation budget) | session_arena | ACCOUNT_KERNEL.md ยง Session Model |
| Account recovery if a device is lost | GuardianPolicy + ActiveDevicePolicy | guild_arena | ACCOUNT_KERNEL.md ยง Policies |
| An emergency stop / pause switch | Pausable | - | STANDARDS_LAYER.md ยง Pausable |
| To serialize mutations / guard against reentrancy-like issues | ExecutionGuard | - | STANDARDS_LAYER.md ยง ExecutionGuard |
| Delayed or timelocked execution | DelayedExecutionPolicy | - | STANDARDS_LAYER.md ยง DelayedExecutionPolicy |
| To batch several operations safely | BatchExecutor | - | 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 state | battleship, snake, blind_auction | ONCHAIN_OFFCHAIN_BOUNDARY.md |
| To know whether I even need ECS | Direct contract model for small/config-driven contracts | - | When Not To Use ECS below |
| To pick table vs. sparse storage | Table for hot-loop state, sparse for infrequent markers | - | Storage Guidance below |
| A thin, explicit contract entrypoint / gameplay loop | GameApp + explicit stage placement | spawn_and_move, snake | Default 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:
- build the app
- register plugins and startup systems
- register tick systems into explicit stages, preferably with
named_system(...)/named_context_system(...) - 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 setupPreUpdate: input decoding, action validation, turn preparationUpdate: core gameplay state transitionsPostUpdate: scoring, derived-state maintenance, indexing side effectsCleanup: 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::stableMerkle 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
Updatesystems 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.
Recommended Separation
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.mdanddocs/strategy/12-documentation-architecture.md.Tracked in:
salazarsebas/Cougrissues
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.mdandSTANDARDS_LAYER.mdinto one place, distinguishing "Cougr patterns" from "general Soroban patterns."Tracked in:
salazarsebas/Cougrissues
What this guide will cover
- Access control with
AccessControlandOwnable - 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, andSnapshotAssertexist in the codebase and are used across thousands of lines of tests, but have no standalone guide yet. Named explicitly indocs/strategy/08-ux-strategy.mdStage 5.Tracked in:
salazarsebas/Cougrissues
What this guide will cover
- Setting up
GameHarnessfor unit tests - Writing
Scenario-based integration tests - Using
SnapshotAssertto lock in expected world state - How the Soroban test sandbox works (and how it differs from running
cargo testfor 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/Cougrissues
What this guide will cover
- Configuring the Stellar CLI for Testnet and Mainnet
- Building a release
.wasmbinary 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.
| Document | Description |
|---|---|
| ECS Core | The core ECS primitives: Entity, Component, Query, System, Scheduler |
| Account Kernel | Account abstraction layer - session keys, recovery, passkeys |
| Standards Layer | Reusable contract standards: AccessControl, Pausable, Ownable, etc. |
| Privacy Model | ZK proofs, Pedersen commitments, hidden state |
| Feature Flags | hazmat-crypto, testutils, debug - what each enables |
| Performance Guide | Resource cost intuition, benchmarks, optimization patterns |
| API Contract | Public API guarantees and stability promises |
| Compatibility Promises | What Cougr will and won't break between releases |
| Migration Guide | How to update your game for new cougr-core versions |
| CLI Reference | ๐ Ships with the CLI |
| Client SDK Reference | ๐ Ships with the TypeScript SDK |
| ADRs | Architecture 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 identityComponent: typed or raw data attached to entitiesQuery: a declarative selection over entities by component presenceSystem: logic that reads or mutates the worldCommandQueue: deferred structural mutationsGameApp: app-level orchestration over world + scheduler + pluginsRuntimeWorld/RuntimeWorldMut: the shared backend contract for Soroban-first worlds
For Soroban gameplay contracts, the recommended path is:
appSimpleWorldSimpleQuerySimpleSchedulerGameApp
ArchetypeWorld is the alternate backend for heavier query workloads.
The shared stable overlap between those backends lives in:
ecs::RuntimeWorldecs::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:
README.mdGameAppSimpleWorldSimpleQueryBuilder- 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
- generic
- 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
- uses
- 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:
IntentExpiryPolicySessionPolicyActiveDevicePolicyGuardianPolicy
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:
OwnableandOwnable2Steprequire an explicit caller addressAccessControlchecks the caller against the relevant admin rolePausable,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:
CommitmentSchemeMerkleProofVerifierHiddenStateCodecProofVerifieras an interface contract only
These are exposed through:
cougr_core::privacy::stablecougr_core::zk::stableas 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::experimentalcougr_core::zk::experimentalas 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
| Surface | Status | Notes |
|---|---|---|
| Commitments | Stable | Explicit interface and verification contract |
| Commit-reveal | Stable | Explicit component semantics and deadline behavior |
| Hidden-state encoding | Stable | Stable codec interface; fixed-width codecs can be defended |
| Merkle inclusion and sparse Merkle utilities | Stable | Malformed proof behavior and inclusion semantics are explicit |
| Proof submission systems | Beta | Useful orchestration, but still coupled to experimental verification flows |
| Groth16 verification and prebuilt circuits | Experimental | Assumptions 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() != depthreturnZKError::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
| Flag | Maturity | Intended use | Notes |
|---|---|---|---|
debug | Support-only | Local diagnostics and introspection | Exposes runtime snapshots and metrics that are not part of the stable product contract |
hazmat-crypto | Experimental | Advanced ZK and cryptographic integrations | Enables low-level host crypto helpers; do not treat as part of the stable privacy promise |
testutils | Non-contract support surface | Tests and explicit test-utility consumers | Enables 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:
authmirrors the Betaaccountssurfaceprivacy::stableandprivacy::experimentalmirror the split insidezkopsmirrors the stablestandardssurface
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_indexfor table-backed componentsall_indexfor table + sparse lookups
That changes the expected behavior of the common query paths:
get_table_entities_with_component()uses the direct table indexget_all_entities_with_component()uses the all-storage indexSimpleQueryselects 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
ArchetypeWorldoutperformSimpleWorld - 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
SimpleWorldvsArchetypeWorldmulti-component query comparisonSimpleWorldvsArchetypeWorldstructural mutation comparison
Reading The Current Benchmarks
Interpret the benchmark output in this order:
Query PathsIf plain indexed queries and cached queries are already cheap enough, stay onSimpleWorld.Backend Query ComparisonIfArchetypeWorldis materially better on your real multi-component query shape, it may be worth adopting.Backend Structural Mutation ComparisonIf archetype migration is significantly more expensive for your workload, do not switch just because query numbers look better in isolation.Query Cache InvalidationIf 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.
Related
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-coreis primarily an ECS framework for Soroban-compatible applicationsappis the default gameplay runtime surface for new projectsauth,privacy, andopsare 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
standardsare part of the1.0stable contract - helper APIs that exist only for compatibility or transition should remain clearly demoted
Recommended Public Contract
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:
SimpleWorldArchetypeWorldecs::{RuntimeWorld, RuntimeWorldMut, WorldBackend}- typed and raw component operations
- command queues
- scheduling primitives
- events, hooks, and observers
- incremental persistence utilities
Concrete frozen root-level contract:
SimpleWorldArchetypeWorldCommandQueueComponent,ComponentTrait,ComponentStorage,ComponentIdSimpleQuery,SimpleQueryBuilderRuntimeWorld,RuntimeWorldMut,WorldBackendResourceruntime::ChangeTracker,runtime::TrackedWorldPlugin,PluginGroup,GameAppScheduleStage,SystemConfig,SimpleScheduler,SystemGrouppreluderuntimeappopsas the clearest Stable standards namespacestandardsas a Stable namespaceprivacy::stableas the clearest stable privacy namespacezk::stableas the stable privacy namespaceauthas the clearest Beta account namespaceaccountsas a Beta namespaceprivacy::experimentalas an explicitly non-contract namespacezk::experimentalas 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:
appauthaccountsarchetype_worldcommandscomponentdebugbehind feature flagerroreventopsprivacypluginqueryresourceschedulersimple_worldzk
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:
appauthprivacyopsSimpleWorldArchetypeWorldCommandQueueGameAppapp::{named_system, named_context_system}andadd_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:
appis the clearest default runtime namespace for new gameplay codeauthis the clearest Beta auth namespace for application codeprivacyis the clearest domain namespace for privacy adoption, with stability determined by submoduleopsis the clearest stable namespace for operational standards in application code- root re-exports and
preludeare the default onboarding path runtimeis the supported namespace for advanced ECS integrations that are not part of the smallest onboarding contractqueryandarchetype_worldretain their cache/state helpers outside the smallest root onboarding surfacestandardsis a supported stable namespaceaccountsremains a public Beta namespacezk::stableis the only privacy namespace treated as Stablezk::experimentalremains 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
preluderuntimeopsstandardsprivacy::stablezk::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
authaccounts- 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::experimentalzk::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:
- MATURITY_MODEL.md if the classification changes
- API_CONTRACT.md if the recommended contract changes
- PUBLIC_GAPS.md if a known gap is closed or newly introduced
- THREAT_MODEL.md if trust assumptions or security posture change
1.0 Freeze Decisions
The 1.0 release gate decisions are:
- ECS onboarding and runtime surfaces are in the stable contract
opsis the stable domain alias for standardsstandardsis in the stable contractauthis a Beta domain alias and is not part of the stable guaranteeaccountsremains Beta and is not part of the stable guaranteeprivacy::stablemaps to the frozen privacy contractzk::stableis the frozen privacy contractprivacy::experimentalremains outside compatibility guaranteeszk::experimentalremains 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:
appfor gameplay runtimeauthfor account and session flowsprivacy::stablefor stable privacy primitivesopsfor 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:
SimpleQueryBuilderSimpleQueryStateSimpleQueryCache
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:
RuntimeWorldRuntimeWorldMut
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:
snakeforapp::GameAppand stage-based gameplay loopsbattleshipforprivacy::stableand hidden-information patternsguild_arenafor 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
appwhere 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-clicrate 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, andcougr checksubcommands.Tracked in:
salazarsebas/Cougrissues
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/Cougrissues
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.
| ADR | Title |
|---|---|
| 0001 | Public API Surface |
| 0002 | Accounts Beta |
| 0003 | Privacy Model Split |
| 0004 | Standards Layer Stable |
| 0006 | Game Circuit Suite |
| 0007 | Workspace 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
standardsas a Stable namespacezk::stableas the stable privacy namespaceaccountsas a Beta namespacezk::experimentalas 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::stablefor commitments, commit-reveal, hidden-state codecs, and Merkle verificationzk::experimentalfor 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
testutilsfeature 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
standardsas 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
-
Ship four pre-built circuit builders under
cougr_core::circuits(always available, Experimental maturity):Builder Public 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 -
Each builder returns
GameCircuitSpecwith a frozenPublicInputLayout, placeholder VK (correct IC length), and typed verify methods that delegate tozk::experimental::verify_groth16. Production deploys replace the VK viawith_verification_key. -
fog_of_warreusesFogOfWarCircuitinzk::advanced- no duplicate verification logic. -
Circom scaffolds and off-chain scripts live in
internal/cougr-core-circuits/(publish = false). Rust implementation lives insrc/circuits/. -
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
CircuitIdand 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::stableremains 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
-
Add a Cargo workspace with three internal members under
internal/:cougr-core-circuitscougr-core-sessioncougr-core-test
-
Each internal member sets
publish = false. -
Each layer's implementation lives in
src/{circuits,session,test}/inner.rs. The public modulesinclude!that file; internal workspace members point their[lib] pathat the sameinner.rsfor isolatedcargo check -pruns. This avoids:- circular dependencies (session needs
authfrom the same crate) cargo publishfailures on unpublished path deps- missing files in the published tarball
- circular dependencies (session needs
-
Public API:
cougr_core::circuits- always availablecougr_core::session- always availablecougr_core::test-testutilsfeature only
-
The test sandbox uses
no_std+alloc, notstd. It runs in Sorobantestutilsenvironments the same way contract tests do today.
Consequences
- One
cargo add cougr-corefor all capabilities - Internal folders can still be checked with
cargo check -p cougr-core-session cargo publishshipsinternal/**sources inside thecougr-coretarball- Feature
testutilskeeps 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
standardsas 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.
Browsing the gallery
- 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:
examples/catalog.tomlโ structured metadata (category, maturity, Cougr features, optional screenshot/testnet contract).- Each example's
README.mdโ the description and full documentation. 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
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:
- Register โ owner calls
register_sessiononce to approve a scoped session key viaaccount_kernel::build_scopeandSessionStorage::store. - Authorize โ each
incrementcall delegates toaccount_kernel::authorize_action, which runs the fullAccountKernel โ SessionPolicypipeline (expiry check, op-budget decrement, nonce advancement). - 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
| Function | Parameters | Returns | Description |
|---|---|---|---|
register_session | owner, key_id, max_ops, expires_at | () | Approve a scoped session for this owner (requires owner auth) |
increment | owner, key_id, nonce, intent_expires_at | u32 | Increment counter if the session authorizes the tap action |
count | owner | u32 | Current counter value for the owner |
Cougr APIs used
account_kernel::build_scopeโ piece helper that callsSessionBuilderaccount_kernel::counter_actionโ piece helper that constructs thetapGameActionaccount_kernel::authorize_actionโ piece helper that callsAccountKernel::authorize_sessionAccountKernelโ separates signer verification, policy evaluation, and replay protectionSessionStorageโ load / store session keys on-chainSessionBuilderโ declare allowed actions, max operations, and expiry
Test coverage
| Test | Path exercised |
|---|---|
in_policy_action_increments_counter | Allow โ valid session, correct nonces, counter advances |
expired_session_blocks_increment | Expiry โ ledger timestamp past expires_at, increment panics |
replayed_nonce_blocks_increment | Replay โ same nonce submitted twice, second increment panics |
budget_exhaustion_blocks_further_increments | Op-budget runs to zero, next increment panics |
count_returns_zero_before_any_increment | Baseline โ 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-end | Learning SessionManager lifecycle |
Understanding the raw AccountKernel / authorize_action path | Need approve / renew / fallback patterns |
| Writing tests that exercise the piece's own helpers | Need MockSession testutils |
License
MIT
AI Dungeon Master Arena Example
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:
- x402-style Paid Actions: Premium encounters or rerolls purchased in-game via a request-driven model.
- 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_prooffunction takes aGroth16Proofand 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
- Player completes a floor.
- At every 3rd floor, a
PremiumActionComponentis attached to their run. - This signals an HTTP 402 Payment Required state to the frontend/off-chain DM.
- Player pays the fee (XLM/tokens) to purchase the
purchase_premium_action. - Contract verifies payment and grants a high-tier reward or easier encounter path.
Development
Prerequisites
- Soroban CLI
- Rust toolchain with
wasm32-unknown-unknowntarget.
Build
cargo build --target wasm32-unknown-unknown --release
Test
cargo test
On-Chain Deployment
- Deploy the contract:
soroban contract deploy ... - Initialize VK:
soroban contract invoke --id <ID> --fn set_vk --vk <VK_JSON> - Start run:
soroban contract invoke --id <ID> --fn init_run --player <ADDR>
Acceptance Criteria
-
Includes x402 representation in
purchase_premium_action. -
Real
stellar-zkverification path inverify_run_proof. - Deterministic run loop and full test coverage.
- Soroban-compatible build (wasm32v1-none).
Angry Birds on Stellar Soroban
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
| Component | Fields | Purpose |
|---|---|---|
PositionComponent | x: i32, y: i32 | Grid position using fixed-point math (scaled by 1000) |
HealthComponent | hp: u32, max_hp: u32 | Hit points for structures and pigs |
MaterialComponent | kind: enum {Wood, Glass, Stone} | Damage resistance multiplier |
ProjectileComponent | bird_type: enum, angle: i32, power: i32, active: bool | Shot parameters for trajectory resolution |
ScoreComponent | points: u32, birds_remaining: u32 | Running score tracker |
LevelConfigComponent | level_id: u32, status: enum, player: Address | Level metadata and game state |
Systems
- ShotResolutionSystem - Resolves projectile trajectory using discrete steps (max 50 iterations)
- DamageSystem - Applies damage based on material resistance (wood < glass < stone)
- ScoreSystem - Calculates points from destroyed entities and remaining birds
- 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:
- Component Modularity: Each game aspect (position, health, material) is a separate, reusable component
- System Separation: Game logic is organized into discrete systems with clear responsibilities
- Scalability: Adding new features (power-ups, different bird types) requires minimal code changes
- Testability: Components and systems can be tested independently
- 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:
- Follow the existing ECS patterns and component structure
- Maintain deterministic behavior for all game logic
- Add comprehensive tests for new features
- Update documentation for API changes
- 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
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
-
Rust (1.88.0 or later)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup update -
Cargo (comes with Rust)
-
Stellar CLI
cargo install stellar-cli --locked -
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
- Initialize: Call
init_game()to set up the game state - 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
- Call
- 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:
- Component Pattern: Modular game data (Paddle, Ball, Bricks, Score)
- System Pattern: Organized logic (Physics, Collision, Scoring systems)
- Scalability: Easy to add features (power-ups, multiple balls)
- Clarity: Clear separation of data and operations
- 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)
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:
- Modularity - Components are reusable across different game types
- Clarity - Systems clearly separate concerns (movement, collision, scoring)
- Extensibility - Adding features (power-ups, enemies) requires only new components/systems
- 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
- Install Rust + Cargo:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- Add the WASM target:
rustup update
rustup target add wasm32v1-none
- 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
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:
- Proof is valid against stored
merkle_root - Records hit (value=1) or miss (value=0)
- Updates ship count
- 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
| Function | Parameters | Description |
|---|---|---|
new_game | player_a: Addressplayer_b: Address | Initialize game |
commit_board | player: Addresscommitment: BytesN<32>merkle_root: BytesN<32> | Commit board layout |
attack | attacker: Addressx: u32, y: u32 | Attack coordinates (0-9) |
reveal_cell | defender: Addressx: u32, y: u32value: u32proof: 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
| Pattern | Best For | Cost | Complexity |
|---|---|---|---|
| Commit-Reveal + Merkle | Hidden boards, card hands, fog-of-war | O(log n) proof verification | Low - uses standard SHA256 |
| ZK Circuits (Groth16/Poseidon) | Private game logic evaluation, hidden card deals | Single on-chain verification | High - 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 saltmerkle_root_a/b- root of the Merkle tree built from cell hasheshas_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 toCellResult(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:
- Constructs the expected leaf hash from
(index, value)using the same SHA256 scheme as the commit phase - Validates the
OnChainMerkleProofagainst the storedmerkle_rootusingSha256MerkleProofVerifier - 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
| Component | Fields | Purpose |
|---|---|---|
BoardCommitment | commitment: BytesN<32>merkle_root: BytesN<32> | Cryptographic board commitment |
AttackGrid | cells: Map<u32, CellResult> | Public record of attacks |
ShipStatus | remaining_a: u32remaining_b: u32 | Ship cell counts |
TurnState | current_player: Addressphase: Phasehas_pending: bool | Game state management |
Systems
| System | Responsibility |
|---|---|
| CommitSystem | Validates and stores commitments |
| AttackSystem | Records attack coordinates |
| RevealSystem | Verifies stable OnChainMerkleProof, updates grid |
| WinConditionSystem | Detects when all ships sunk |
Why Merkle Proofs?
Merkle trees enable selective disclosure:
| Approach | Reveal Cost | Privacy |
|---|---|---|
| Full board on-chain | O(1) | โ None |
| Reveal entire board per attack | O(n) | โ None |
| Merkle proof | O(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
- Cougr Repository
- Merkle Trees
- Commitment Schemes
- hidden_hand - ZK circuit example
- Soroban Documentation
License
MIT OR Apache-2.0
blind_auction
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_auction | max_bid: u32, auction_id: BytesN<32> | AuctionConfig | Starts a new auction config with maximum bid limits and an auction identifier. |
reveal_bid | bidder: Address, bid_commitment: BytesN<32>, revealed_bid: u32, proof: Groth16Proof | bool | Verifies a bidder's reveal proof, storing the bid record if valid. |
bid_reveal | bidder: Address | BidReveal | Retrieves 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
- Setup: Call
init_auctionto setup maximum bid constraints and the auction ID. - Commit Phase: Bidders record hash commitments of their bids (handled off-chain or via standard storage).
- Reveal Phase: Bidders call
reveal_bidwith 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.
Recommended testing approach
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
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
- Install Rust from https://rustup.rs/
- 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
| Component | Fields | Notes |
|---|---|---|
PlayerComponent | id, x, y, lives, bomb_capacity, score, bomb_power, speed | All player state |
BombComponent | x, y, timer, power, owner_id | Power set from bomb_power at placement time |
ExplosionComponent | x, y, timer | Despawned after EXPLOSION_DURATION ticks |
GridComponent | cells: Vec<CellType> | Walls, destructible blocks |
GameStateComponent | current_tick, game_over, winner_id | Global match state |
PowerUpComponent | x, y, power_up_type | Capacity | Power | Speed |
Power-ups
Three power-up types are supported and modelled as first-class ECS entities:
| Type | Effect |
|---|---|
Capacity | bomb_capacity += 1 - player may place one more active bomb |
Power | bomb_power += 1 - new bombs have a larger blast radius |
Speed | speed += 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:
- Bombs whose timer hits
0are placed in adetonation_queue. - Each bomb is detonated in order - explosions are spawned immediately.
- 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.
- The process repeats until the queue is empty.
Systems at a Glance
| System | Where | What it does |
|---|---|---|
BombTimerSystem | update_tick | Decrements bomb timers |
ChainReactionSystem | update_tick | Cascades detonations within one tick |
ExplosionPropagationSystem | detonate_bomb | Spawns explosion entities in 4 directions |
PowerUpSpawnSystem | init_game + detonate_bomb | Creates PowerUpComponent entities |
PickupSystem | move_player | Applies buff when player steps on a power-up |
Contract Functions
init_game(): Initializes the game statemove_player(player_id, direction): Moves a player in the specified directionplace_bomb(player_id): Places a bomb at the player's current positionupdate_tick(): Advances timers, triggers explosions, handles collisionsget_score(player_id): Returns the current score for a playercheck_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
- Test Account: Fund a test account using the Friendbot: https://faucet-stellar.acachete.xyz
- Stellar CLI: Install with
cargo install --locked stellar-cli - WASM Build: Ensure the contract builds successfully
To Testnet
-
Build the WASM file:
stellar contract build -
Deploy the contract:
stellar contract deploy \ --wasm target/wasm32v1-none/release/bomberman.wasm \ --source <your-secret-key> \ --network <NETWORK> -
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
- Initialize: Call
init_gameto set up the world - Spawn Players: In a full implementation, add player spawning functions
- Gameplay Loop:
- Move players with
move_player - Place bombs with
place_bomb - Advance game state with
update_tick - Check scores and game status periodically
- Move players with
- Game Over: Monitor
check_game_overfor 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
- Rust version conflicts: Update Rust with
rustup update - Stellar CLI not found: Ensure it's in your PATH after installation
- 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 byworld.despawn_entity() - Components: Plain structs implementing
ComponentTrait(typedserialize/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
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | player_one: Address, player_two: Address | Result<(), CheckersError> | Seeds the standard 8ร8 opening position; Player One moves first. Returns AlreadyInitialised if called more than once. |
submit_move | player: Address, from_row: u32, from_col: u32, to_row: u32, to_col: u32 | Result<(), 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
| Error | Code | Meaning |
|---|---|---|
AlreadyInitialised | 1 | init_game called more than once |
NotInitialised | 2 | Any call before init_game |
NotAPlayer | 3 | Caller is not player_one or player_two |
WrongTurn | 4 | Caller is a registered player but it is not their turn |
NotYourPiece | 5 | Source square is empty or owned by the opponent |
DestinationOccupied | 6 | Target square is already occupied |
IllegalMove | 7 | Move is not a legal diagonal step or jump |
MustCapture | 8 | A capture is available but a non-capture move was attempted |
GameOver | 9 | The game has already ended |
OutOfBounds | 10 | Row or column index โฅ 8 |
ChainCapturePieceMismatch | 11 | During a chain capture the origin square was not the chain square |
NotDarkSquare | 12 | Destination 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:
| Key | Contents |
|---|---|
BOARD | BoardComponent - 64-cell flat grid |
TURN | TurnComponent - current player and move number |
STATUS | GameStatusComponent - active/finished and winner |
P1 | Address of Player One |
P2 | Address of Player Two |
CHAIN | ChainCapture - 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
- 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. - Player One calls
submit_movewith 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). - 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. - 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
CHAINkey) until no further capture exists. - 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.
- After each move,
EndConditionSystemchecks whether either side has no pieces left or no legal move available; if so,STATUSflips toFinishedwith a recorded winner. - Players alternate
submit_movecalls until a winner is recorded;get_state,get_board, andget_current_playerprovide read-only views at any point.
Cougr APIs used
cougr_core::component::ComponentTrait- gives each component (BoardComponent,TurnComponent,GameStatusComponent) acomponent_type()symbol and byte-levelserialize/deserialize, chosen here because the game has a single fixed set of components per instance rather than a dynamic entity population that would benefit fromSimpleWorld/SimpleQueryBuilderscanning.
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, orSimpleWorld- 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
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 usescougr_core::circuits' pre-built circuit builders instead of constructing aCustomCircuitby 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
| Function | Parameters | Returns | Description |
|---|---|---|---|
new_game | white: Address, black: Address | - | Initializes the standard 8x8 opening position, computes the initial state_hash, and sets White to move first. |
submit_move | player: Address, from: u32, to: u32, proof: Bytes | MoveResult | Requires 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. |
resign | player: Address | - | Marks the game Resigned if called by either registered player. |
get_board | - | BoardState | Current piece map and state_hash. |
get_state | - | GameState | Full game state: both players, board, turn state, and last proof record. |
set_vk | vk: 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
- Deployer calls
set_vk(vk)once to register the Groth16 verification key matching the off-chain move-validation circuit. - Deployer (or either player) calls
new_game(white, black); the board is seeded with the standard opening position, White to move, and the initialstate_hashis computed. - Off-chain, White picks a move (
from,to), runs the move-validation circuit against the currentstate_hash, and produces a Groth16 proof that the move is legal. - White calls
submit_move(white, from, to, proof). The contract checks turn order, verifies the proof viaCustomCircuit, and on success applies the move, recomputesstate_hash, records the proof, and switches the turn to Black. - Players alternate steps 3-4. If a king is ever captured,
EndGameSystemsets the status toCheckmate; either player may also callresignat any time to end the game early. - Any caller reads
get_boardorget_stateto render the current position or check status.
Cougr APIs used
cougr_core::component::ComponentTrait- givesBoardStateandTurnStateacomponent_type()symbol and byte-levelserialize/deserialize, chosen because chess has a single fixed-shape game state per contract instance rather than a dynamic entity population that would benefit fromSimpleWorld/SimpleQueryBuilderscanning.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 theprivacy::experimentalguidance in the Cougr API table calls out (Groth16 proof submission / pairing-based verification), as opposed toprivacy::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-levelcougr_core::circuitspre-built builders instead of hand-rolling aCustomCircuit; 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 theCustomCircuitbuilder and verifier operate on; stored on-chain (VerificationKey) or passed per-call (Groth16Proof, decoded from theproof: Bytesargument) 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, orSimpleWorld-submit_moveruns its pipeline directly from the contract entrypoint since each call has exactly one decision point and does not need staged scheduling.
Connect Four
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | player_one: Address, player_two: Address | GameState | Initializes an empty 7ร6 board; player_one moves first. |
drop_piece | player: Address, column: u32 | DropResult | Validates 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 | - | GameState | Current 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_column | column: u32 | bool | Whether column is in bounds, not full, and the game is still in progress. |
is_finished | - | bool | Whether 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 | - | GameState | Re-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
- Deployer calls
init_game(player_one, player_two); the board is seeded empty,player_oneto move, status in-progress. player_onecallsdrop_piece(player, column). The contract checks turn order, column bounds, and that the column isn't full.- On a legal move:
gravity_systemfinds the lowest empty row in that column,execution_systemplaces the piece there and increments the move count,win_detection_systemscans the whole board for a 4-in-a-row, anddraw_systemmarks the game a draw if all 42 cells are filled with no winner. - If the game is still in progress,
turn_systemswitches to the other player; the updatedECSWorldStateis written back to instance storage. - Players alternate
drop_piececalls untilis_finishedreturnstrue. Either player readsget_winnerto see who won (orNonefor a draw). reset_gamecan 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) acomponent_type()symbol and explicit byte-levelserialize/deserialize. This was chosen because the game has one fixed-shapeECSWorldStateper contract instance (a single board, two fixed player slots, one game- state record) rather than a dynamic population of entities that would benefit fromSimpleWorld/SimpleQueryBuilderscanning - per the guidance inEXAMPLE_STANDARD.mdยง8,SimpleWorldandSimpleQueryBuilderare for examples that store and query entities by component type, which doesn't apply here.PlayerComponentis also#[contracttype]but does not implementComponentTrait, since it is embedded directly inECSWorldStaterather than serialized standalone.- No
GameApp,ScheduleStage,auth,privacy::stable/experimental, oropsstandards are used -drop_piecehas 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, orSimpleWorld- 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_systemre-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)
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:
- Soroban contract state for deterministic race progression and league management.
- stellar-zk style Groth16 verification on-chain using Soroban BN254 pairing checks for anti-cheat and race integrity.
- 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:
- Off-chain settlement process (e.g., via x402 gateway) receives cross-asset payment
- Owner calls
credit_payment(player, amount, receipt_hash)with proof of settlement - Replay protection via receipt hash prevents double-crediting
- 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
| Component | Fields | Purpose |
|---|---|---|
| LeagueComponent | season_id, current_race_id, league_active | Tracks league state |
| RaceComponent | race_id, season_id, entrants_count, phase, duration | Tracks race instance |
| VehicleStateComponent | speed, boost_state_type, boost_active, penalty_count | Tracks racer state |
| PaymentActionComponent | player_credits | Tracks payment-gated access |
| ProofStateComponent | commitment, verification_status | Tracks proof validation |
Systems
- RaceSystem: Handles
create_race,enter_race,start_race,complete_race, andsubmit_race_proof. - PaymentSystem: Handles
credit_paymentlogic.
stellar-zk Integration
Proof-Backed Race Integrity
This contract uses stellar-zk for a real validation path:
- Race Commitment: Off-chain race state committed via Merkle root
- Proof Generation: Player proves valid race state against commitment
- On-Chain Verification: BN254 pairing checks verify the Groth16 proof
- 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)
- Player initiates cross-asset trade (e.g., USDC โ stablecoin or asset)
- Payment gateway settles transaction
- 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 orchestrationsrc/types.rs: Data structures, enums, and storage keyssrc/components.rs: ECS componentssrc/systems.rs: Core game logic and transitionssrc/helpers.rs: Validation helperssrc/test.rs: Unit testsREADME.md: Architectural overview and documentationCargo.toml: SDK dependencies
Compatibility
- Soroban SDK: 25.1.0
- Edition: 2021
- Target:
wasm32v1-none
License
MIT OR Apache-2.0
dice_duel
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_duel | sides: u32, seed_commitment: BytesN<32> | DuelConfig | Registers the dice parameters and the starting cryptographic seed commitment. |
submit_roll | player: Address, roll_result: u32, nonce: u32, proof: Groth16Proof | bool | Verifies a dice roll ZK proof and updates the roll record if valid. |
roll_record | player: Address | RollRecord | Retrieves 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
- Setup: Call
init_duelto bind the dice properties and seed commitment. - Roll: Players roll dice off-chain and calculate proof.
- Submit: Players call
submit_rollto 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.
Recommended testing approach
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
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
| Function | Parameters | Return type | Description |
|---|---|---|---|
init_game | none | () | Initializes bird, velocity, pipes, score, and tick state. |
flap | none | () | Applies upward input velocity if the game is still active. |
update_tick | none | () | Runs one scheduled tick for gravity, movement, pipes, collisions, and scoring. |
get_score | none | u32 | Returns the current score. |
check_game_over | none | bool | Returns whether the bird has crashed. |
get_bird_pos | none | (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 class | Data | Why |
|---|---|---|
| Instance storage | Per-contract game state where used by this example. | Keeps small arcade state close to the contract instance. |
| Persistent storage | Player- or world-scoped state where the example needs durable keyed state. | Keeps game progress available across invocations. |
| Temporary storage | Not used. | The examples favor deterministic recalculation over ephemeral caches. |
Main gameplay flow
- Call the initialization function to create the starting state.
- Submit an input action such as movement, jump, flap, rotation, or shoot.
- Call the tick/update function to run deterministic simulation logic.
- Query public getters for score, position, active state, or terminal status.
- Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.
Cougr APIs used
ComponentTraitand custom component modules document the ECS data boundary.SimpleWorld,SimpleQueryBuilder,GameApp,ScheduleStage, orSystemConfigare 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 tickFLAP_VELOCITY: -15 pixels per tick (upward)PIPE_SPEED: 3 pixels per tick (leftward)GROUND_Y: 400 pixelsBIRD_SIZE: 20 pixels (hitbox radius)PIPE_GAP: 100 pixels
How to Play
- Initialize a new game with
init_game() - Make the bird flap with
flap()to gain upward velocity - Advance the game with
update_tick()- this applies:- Gravity to the bird
- Movement to all entities
- Collision detection
- Score updates
- Repeat steps 2-3 to keep the bird alive and score points
- 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
- Solution: Install Stellar CLI following the official guide
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
- Use
cargo testto verify logic before deploying - Use
--simulateflag to test transactions without broadcasting:stellar contract invoke --id <CONTRACT_ID> --simulate -- init_game - Check contract logs in Stellar Explorer
- 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
- Opt-level "z": Optimize for size in release builds
- LTO: Link-time optimization reduces binary size
- Minimal Storage: Only store essential game state
- Efficient Queries: Cache entity lookups when possible
Next Steps
Extending the Game
- Add Power-ups: Create new components for temporary abilities
- Difficulty Scaling: Increase pipe speed over time
- Multiplayer: Store multiple game states with player IDs
- Leaderboard: Track high scores across players
- 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
snakemodule split andGameApptick wiring.
fog_explorer
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_map | width: u32, height: u32, visibility_radius: u32 | MapConfig | Initializes the map configuration and builds the ZK circuit specs. |
register_explorer | player: Address, explored_root: BytesN<32> | ExplorerState | Registers a player with their starting empty explored map commitment. |
explore | player: 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: Groth16Proof | bool | Verifies a transition proof, updating the player's explored map root if valid. |
explorer_state | player: Address | ExplorerState | Retrieves 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
- Map Config: Call
init_mapto configure map dimensions and visibility boundaries. - Registration: Explorer calls
register_explorerto record their initial map state. - Exploration: Explorer calls
explorewith 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.
Recommended testing approach
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
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
| Function | Parameters | Return type | Description |
|---|---|---|---|
init_game | player: Address, level_id: u32 | () | Initializes a player level. |
jump | player: Address | () | Applies the current mode input. |
update_tick | player: Address | () | Advances the level simulation by one tick. |
get_state | player: Address | GameStatus | Returns Playing, Crashed, or Completed. |
get_pos | player: Address | (i32, i32) | Returns player position. |
get_score | player: Address | u32 | Returns distance-based score. |
get_mode | player: Address | u32 | Returns 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 class | Data | Why |
|---|---|---|
| Instance storage | Per-contract game state where used by this example. | Keeps small arcade state close to the contract instance. |
| Persistent storage | Player- or world-scoped state where the example needs durable keyed state. | Keeps game progress available across invocations. |
| Temporary storage | Not used. | The examples favor deterministic recalculation over ephemeral caches. |
Main gameplay flow
- Call the initialization function to create the starting state.
- Submit an input action such as movement, jump, flap, rotation, or shoot.
- Call the tick/update function to run deterministic simulation logic.
- Query public getters for score, position, active state, or terminal status.
- Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.
Cougr APIs used
ComponentTraitand custom component modules document the ECS data boundary.SimpleWorld,SimpleQueryBuilder,GameApp,ScheduleStage, orSystemConfigare 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
snakemodule split andGameApptick wiring.
Guild Arena
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:
- Social Recovery - guild members act as guardians who can collectively restore account access after a timelock period
- 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
| Function | Description |
|---|---|
register_player | Register with guardians and recovery config |
add_device | Add a device key with policy (Full or PlayOnly) |
remove_device | Revoke a device key |
start_match | Queue for or start a PvP match |
submit_action | Submit combat action (Attack/Defend/Special) |
initiate_recovery | Guardian starts recovery process |
approve_recovery | Guardian approves recovery |
finalize_recovery | Complete recovery after timelock |
get_player | Query player profile |
get_match | Query current arena state |
Device Policies
| Level | Play | Trade/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
Transitional example: This example uses an older Cougr pattern and is preserved for compatibility reference. For the current recommended approach, see
snake.
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-zk | With stellar-zk |
|---|---|
| All actions are public on-chain | Strategies are hidden until reveal |
| Opponents can counter every move | Sealed commitments prevent scouting |
| No strategic depth | Real hidden-information gameplay |
| Generic DAO demo | Stellar-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
| Phase | stellar-zk Role | Contract Function |
|---|---|---|
| Commit | SHA256 hash seals the war plan | submit_strategy_commitment() |
| Reveal | Preimage verification proves the plan | reveal_strategy() |
| Nullifier | Anti-replay prevents double-reveal | DataKey::Nullifier(hash) |
| Resolve | Deterministic outcome after verification | resolve_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:
- The commitment is marked as
revealed = true - The hash is stored in
DataKey::Nullifier(hash) โ true - Any attempt to reuse the same commitment is rejected
References
- stellar-zk - ZK DevKit for Stellar/Soroban
- stellar-zk on crates.io - Crate reference
- Stellar Governance - DAO voting patterns inspiration
Cougr-Core Integration
ECS Components
| Component | Fields | Purpose |
|---|---|---|
Guild | admin, name, treasury, member_count, defense/attack_strength | On-chain faction |
Proposal | proposer, action, votes_for/against, status | DAO governance |
StrategyCommitment | commitment_hash, committer, revealed | Sealed war plan |
StrategyReveal | action_type, target_guild_id, resource_amount, salt | Revealed preimage |
GameState | total_guilds, total_proposals, active_campaigns, round | Global 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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_guild | guild_admin | u32 | Create guild with treasury |
join_guild | guild_id, member | - | Join an existing guild |
Governance
| Function | Parameters | Returns | Description |
|---|---|---|---|
submit_proposal | proposer, proposal: ProposalInput | u32 | Create a DAO proposal |
vote | voter, proposal_id, support: bool | - | Cast a vote |
execute_proposal | proposal_id | - | Execute approved proposal |
Strategy (stellar-zk)
| Function | Parameters | Returns | Description |
|---|---|---|---|
submit_strategy_commitment | guild_member, proof_input: ProofInput | - | Seal a war plan |
reveal_strategy | guild_member, guild_id, reveal: StrategyReveal | - | Reveal and verify |
resolve_battle | attacker_member, defender_member, attacker_guild_id, defender_guild_id | - | Resolve outcome |
Queries
| Function | Parameters | Returns | Description |
|---|---|---|---|
get_state | - | GameState | Global game state |
get_guild | guild_id | Guild | Guild info |
get_proposal | proposal_id | Proposal | Proposal info |
๏ธ Quick Start
Prerequisites
| Tool | Version | Installation |
|---|---|---|
| Rust | 1.88.0+ | rustup.rs |
| Stellar CLI | Latest | Stellar 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 โ
| Test | Description |
|---|---|
test_init_guild | Guild creates with correct treasury and state |
test_join_guild | Member joins and count increments |
test_submit_proposal | Proposal created with correct fields |
test_unauthorized_proposal | Non-member rejected |
test_vote_counting_and_threshold | 51% threshold approves |
test_vote_rejection | Majority-against rejects |
test_unauthorized_vote | Non-member vote rejected |
test_double_vote | Double-voting prevented |
test_treasury_execution_approved | Treasury deducted, effects applied |
test_execute_unapproved | Non-approved execution rejected |
test_strategy_commitment_and_reveal | Full commit-reveal flow |
test_invalid_reveal | Wrong preimage rejected |
test_double_reveal_prevention | Nullifier blocks re-reveal |
test_battle_resolution | Attack 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
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_table | deck_size: u32, hand_size: u32 | TableConfig | Configures the game table with the specified deck and hand size, initializing the ZK verifier. |
verify_deal | player: Address, deck_root: BytesN<32>, hand_commitment: BytesN<32>, proof: Groth16Proof | bool | Verifies 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
- Setup: The contract calls
init_table(deck_size, hand_size)to prepare the ZK circuit parameters. - Dealing: The dealer generates a hand commitment and a proof off-chain.
- 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.
Recommended testing approach
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
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | player: Address | GameState | Seeds a fresh 16-card board (8 deterministic pairs) for player and returns the initial state. |
reveal_card | player: Address, position: u32 | RevealInfo | Reveals 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 | - | GameState | Current board view, revealed/matched counts, move count, and game-over flag. |
reset_game | player: Address | GameState | Hides 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
- 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 startingHidden. - Player calls
reveal_card(player, position)for a hidden card; it flips toRevealedand the move counter increments. - Once two cards are revealed, the contract compares their values:
- Match: both cards become
Matched,matched_pairsincrements, and reveals are re-enabled. If all 8 pairs are now matched, the game is marked over. - No match: both cards flip back to
Hiddenand reveals are re-enabled.
- Match: both cards become
- Player repeats step 2โ3 until
matched_pairsreaches 8 andgame_overistrue. - Player (or anyone reading state) calls
get_game_stateat any point to read the board, or callsreset_gameto rehide all cards and start over on the same layout.
Cougr APIs used
cougr_core::component::ComponentTrait- gives each component (CardComponent,BoardComponent,GameStateComponent) acomponent_type()symbol and explicit byte-levelserialize/deserialize. This is the only Cougr API the example uses: the game has one fixed-shapeECSWorldStateper contract instance (16 cards, one board, one player) rather than a dynamic entity population, soSimpleWorld/SimpleQueryBuilderscanning would add overhead with no benefit. There is a single decision point per call (reveal or reset), soGameApp/ScheduleStagestaged ticking is not needed either, and the game has no hidden information, multi-device auth, or proof-verification requirements that would call forauth,privacy::stable,privacy::experimental, oropsstandards.
Build and test commands
cargo test
stellar contract build
Known limitations
- Does not use
GameApp,ScheduleStage, orSimpleWorld- 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_gamereuses the same fixed layout rather than generating a new one.- No timeout/forfeit mechanism, scoring across multiple games, or multiplayer support.
Minesweeper
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | - | GameState | Initializes a 9ร9 board with mines placed at fixed, deterministic positions. Overwrites any existing game. |
reveal_cell | row: u32, col: u32 | RevealResult | Reveals the cell at (row, col). Returns success, is_mine, adjacent_mines, and a status message symbol (ok, invalid, revealed, boom, or over). |
get_state | - | GameState | Current status, revealed-cell count, and remaining safe cells. |
get_visible_cell | row: u32, col: u32 | VisibleCellState | Whether the cell is revealed, whether it's a mine (only meaningful once revealed), and its adjacent-mine count. |
is_finished | - | bool | Whether 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 | - | GameState | Re-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
- Deployer (or any caller) calls
init_game(). The board starts fully hidden andplace_mines_deterministicseeds 10 mines at fixed coordinates across the 9ร9 grid. - Player calls
reveal_cell(row, col)for a hidden cell. - If the cell is a mine, the board marks it revealed-as-mine, status flips to
Lost, and the call returnsmessage = boom. No further reveals are accepted untilreset_game. - 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.
- After each safe reveal,
CompletionSystemchecks whether all 71 safe cells (81 total minus 10 mines) have been revealed; if so, status flips toWon. - Caller reads
get_state,get_visible_cell, oris_finishedto check progress or terminal state;reset_gamestarts over with the same deterministic layout.
Cougr APIs used
cougr_core::component::ComponentTrait- gives each component (BoardComponent,MineLayoutComponent,GameStateComponent) acomponent_type()symbol and explicit byteserialize/deserialize, chosen because the game has one fixed-shape 81-cell grid rather than a dynamic entity population that would benefit fromSimpleWorld/SimpleQueryBuilderscanning.- No other
cougr-coreAPIs are used.GameApp/ScheduleStageare unnecessary becausereveal_cellhas a single decision point per call with no multi-stage ordering;SimpleWorld/SimpleQueryBuilderare unnecessary because the game has exactly three fixed components and never scans entities by type;auth,privacy::stable/experimental, andopsstandards are unused because this example has no multi-party authorization, hidden-information disclosure, or pausability/ownership requirements - the entire board state is openly readable viaget_boardandget_visible_cell.
Build and test commands
cargo test
stellar contract build
Known limitations
- Does not use
GameApp,ScheduleStage, orSimpleWorld- 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_gamecall) 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
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.
zkfeature 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.
| Function | Parameters | Return Type | Description |
|---|---|---|---|
init_game | env: 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> | u32 | Registers 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: PuzzleMetadata | u32 | Registers a new puzzle with a solution commitment and Groth16 verifier key. No plaintext solution is stored. |
list_puzzles | env: Env, offset: u32, limit: u32 | Vec<PuzzleSummary> | Returns a paginated list of summaries of all registered puzzles. |
get_puzzle | env: Env, puzzle_id: u32 | Puzzle | Retrieves puzzle details. In ZK mode, returns the solution commitment (not the plaintext solution). |
deactivate_puzzle | env: 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: Address | bool | Returns 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 Pattern | Type | Description |
|---|---|---|
(Symbol("VKEY"), puzzle_id) | Bytes | XDR-serialized Groth16 VerificationKey |
(Symbol("SOLVED"), puzzle_id, player) | bool | Whether a player has solved this puzzle |
(Symbol("SOLVER_CNT"), puzzle_id) | u32 | Number of unique solvers for this puzzle |
Main Gameplay Flow
Play Flow
- 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.
- Browse Catalog: The frontend calls
list_puzzlesto fetch and render the list of available murder mystery puzzles. - Open Puzzle: The player selects a puzzle. The frontend calls
get_puzzleto retrieve the clues and grid dimensions, then callsstart_gameto initialize the ECS world state for this puzzle. - 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. - 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.
- Completion: If all cells are filled correctly, the contract marks the game status as solved.
is_solvedreturnstrue, and the frontend displays a victory screen.
Create Flow
- Connect Wallet: The creator authenticates on the creator portal via Pollar.
- 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).
- Submit Puzzle (v1): The creator clicks "Publish", triggering a call to
submit_puzzlewith the plaintext solution. - Submit Puzzle (ZK): The creator generates a Poseidon2 commitment of the solution and a Groth16 verifier key, then calls
submit_puzzlewith the commitment and key instead of the plaintext. - 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.
- 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 / Module | Used For | Location |
|---|---|---|
GameApp | Orchestrates systems execution flow during suspect placement and puzzle registration. | src/lib.rs |
ScheduleStage | Ensures that validation systems execute strictly before update systems, and completion systems execute last. | src/lib.rs |
SimpleWorld | Stores 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::SessionBuilder | Builds scoped session keys containing specific permissions (e.g., only allowing place_suspect). | src/auth.rs |
auth::authorize_with_fallback | Authorizes transactions using either the active session key or the player's direct signature. | src/auth.rs |
ops::Ownable | Restricts 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
cellssuch that: (1)cellsis a valid Latin square of size N, (2) Poseidon2(cells || salt) == commitment, (3) each cell value is in range 1..=N."
Public Inputs Format
| Index | Type | Description |
|---|---|---|
| 0 | BytesN<32> | Solution commitment (Poseidon2 hash of solution + salt) |
| 1..N | BytesN<32> | Additional public inputs (e.g., grid size, clue hashes) |
Player Solve Flow (ZK mode)
- Browse Puzzles: Frontend calls
list_puzzlesandget_puzzleto fetch puzzle metadata (grid size, suspects, clues, solution commitment - not the plaintext solution). - Solve Off-Chain: The player solves the puzzle manually, arriving at a candidate grid arrangement.
- Generate Proof: The frontend (or a backend prover service) generates a Groth16 proof that the player knows a valid arrangement consistent with the commitment.
- Submit Proof: The player calls
submit_proof(player, puzzle_id, proof, public_inputs). The contract verifies the Groth16 proof against the stored verifier key. - Mark Solved: On valid proof, the contract marks the puzzle as solved for that player and increments the solver count.
Key Files
| File | Purpose |
|---|---|
src/zk.rs | Groth16 proof verification logic, verifier key storage, solver state tracking |
src/components.rs | SolutionCommitment ECS component (ZK mode only) |
src/lib.rs | Feature-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.rsfor 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
difficultyfield 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_COUNTwithout an upper bound. Reads are paginated viaoffset/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
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 orchestrationsrc/types.rs: Game enums, state structures, and storage keyssrc/components.rs: ECS-inspired data structures (Position, Ghost)src/maze.rs: Maze layout and generation logicsrc/systems.rs: Core game loop and logicsrc/test.rs: Comprehensive test suiteREADME.md: This fileCargo.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
-
Rust (1.88.0 or later)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup update -
Stellar CLI
cargo install stellar-cli --locked stellar --version -
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
-
Verbose Build
cargo build --verbose -
Check Contract Size
ls -la target/wasm32v1-none/release/pac_man.wasmShould be under 64KB.
-
Simulate Before Deploy
stellar contract invoke \ --id <CONTRACT_ID> \ --source pacman-deployer \ --network testnet \ --sim \ -- \ init_game
Game Mechanics
Scoring
| Action | Points |
|---|---|
| Regular Pellet | 10 |
| Power Pellet | 50 |
| Eat Ghost | 200 |
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
-
init_game - Successfully initialized with:
- Score: 0
- Lives: 3
- Pellets: 47
- 4 ghosts in chase mode
-
change_direction - Successfully changed Pac-Man direction to Down
-
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
-
get_score - Read-only query returned: 10
References
- Soroban Documentation
- Soroban Hello World Tutorial
- Stellar CLI Documentation
- Cougr Repository
- Rust Testing Guide
License
MIT OR Apache-2.0
Pokemon Mini On-Chain Game
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:
| Feature | Description |
|---|---|
| Tile Map | 8x8 grid with deterministic tile placement |
| Movement | Grid-based with collision detection |
| Encounters | Deterministic triggering on TallGrass tiles |
| Battle | Turn-based 1v1 combat system |
Why cougr-core?
| Aspect | Benefit |
|---|---|
| ComponentTrait | Standardized serialization for on-chain storage |
| Type Safety | Unique Symbol identifiers per component |
| Storage Optimization | Table vs Sparse storage strategies |
| Code Reusability | Proven ECS patterns |
Prerequisites
| Tool | Installation |
|---|---|
| Rust | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| WASM Target | rustup target add wasm32v1-none |
| Stellar CLI | brew 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
| Function | Description |
|---|---|
init_player() | Create player at spawn (1,1) with starter creature |
Movement
| Function | Parameters | Returns | Description |
|---|---|---|---|
move_player | direction: u32 | u32 | Move player (0=blocked, 1=moved, 2=encounter) |
get_tile | x, y: i32 | u32 | Get tile type at position |
get_map_size | - | (i32, i32) | Returns (8, 8) |
Direction Values: 0=Up, 1=Down, 2=Left, 3=Right
State Queries
| Function | Returns | Description |
|---|---|---|
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
get_battle_state | - | (in_battle, player_hp, enemy_hp, turn, result) | Current battle |
battle_action | action: u32 | u32 | Execute 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>
| Network | Status | Explorer |
|---|---|---|
| Stellar Testnet | โ Live | View 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
| Resource | Link |
|---|---|
| Soroban Docs | developers.stellar.org |
| Stellar CLI | CLI Documentation |
| Cougr Repository | github.com/salazarsebas/Cougr |
License
Part of the Cougr project. See main repository for license information.
Pong On-Chain Game
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
| Function | Parameters | Return type | Description |
|---|---|---|---|
init_game | none | GameState | Initializes paddles, ball, score, and active flag. |
move_paddle | player: u32, direction: i32 | GameState | Moves a paddle and returns state. |
update_tick | none | GameState | Advances ball physics, collisions, scoring, and win checks. |
get_game_state | none | GameState | Returns current state. |
reset_game | none | GameState | Resets 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 class | Data | Why |
|---|---|---|
| Instance storage | Per-contract game state where used by this example. | Keeps small arcade state close to the contract instance. |
| Persistent storage | Player- or world-scoped state where the example needs durable keyed state. | Keeps game progress available across invocations. |
| Temporary storage | Not used. | The examples favor deterministic recalculation over ephemeral caches. |
Main gameplay flow
- Call the initialization function to create the starting state.
- Submit an input action such as movement, jump, flap, rotation, or shoot.
- Call the tick/update function to run deterministic simulation logic.
- Query public getters for score, position, active state, or terminal status.
- Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.
Cougr APIs used
- Deploy the contract:
stellar contract deploy \ --wasm target/wasm32v1-none/release/pong.wasm \ --source <your-secret-key> \ --network <NETWORK>
ComponentTraitand custom component modules document the ECS data boundary.SimpleWorld,SimpleQueryBuilder,GameApp,ScheduleStage, orSystemConfigare 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:
-
Initialize Game:
stellar contract invoke --id <CONTRACT_ID> --source pong-test --network <NETWORK> -- init_gameResult: โ 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 } -
Move Paddle (Player 1 up):
stellar contract invoke --id <CONTRACT_ID> --source pong-test --network <NETWORK> -- move_paddle --player 1 --direction -1Result: โ Success - Paddle moved from y=30 to y=28
{ "player1_paddle_y": 28, "player2_paddle_y": 30, ... } -
Update Tick (Physics simulation):
stellar contract invoke --id <CONTRACT_ID> --source pong-test --network <NETWORK> -- update_tickResult: โ Success - Ball moved from (50,30) to (51,31)
{ "ball_x": 51, "ball_y": 31, ... }
Deployment Date: January 23, 2026
Transaction Hashes:
- Deploy:
<TRANSACTION_HASH> - View on Stellar Expert
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:
-
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; } } -
CollisionSystem: Handles wall and paddle collision detection
- Detects wall bounces (top/bottom)
- Detects paddle hits (left/right)
- Updates ball velocity on collision
-
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:
- Component Organization: Clear separation of data (components) from logic (systems)
- Scalability: Easy to add new components or systems for game features
- Testability: Systems can be tested independently
- 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
snakemodule split andGameApptick wiring.
Proof of Hunt (Stellar + Soroban)
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:
- Soroban contract state for deterministic gameplay and progression
- Stellar-zk style Groth16 verifier flow on-chain using Soroban BN254 pairing checks
- 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
| Function | Parameters | Description |
|---|---|---|
init_game | player: Address, map_commitment: BytesN<32>, width: u32, height: u32 | Initialize game with committed map |
explore | player: Address, x: u32, y: u32, proof: ProofInput | Explore a cell with ZK proof |
purchase_hint | player: Address, hint_type: u32 | Buy hint (0) or scan (1) using credits |
get_state | - | Return GameState |
is_finished | - | Check if game is won or lost |
set_verification_key | owner: Address, vk_bytes: Bytes | Set Groth16 verification key |
credit_x402_payment | owner: 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, discoveriesdiscovered_cells- count of explored cellsexplored_cell(cell_idx)- persistent storage for replay prevention- Game status (
Active/Won/Lost)
Proven State
The explore function performs multi-layer verification:
- Public input validation: Checks that
(x, y)in proof inputs match claimed coordinates - Merkle path verification: Reconstructs root from leaf + sibling path
- Groth16 verification: Verifies BN254 pairing check via
env.crypto().bn254().pairing_check() - 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
- Cougr Repository
- stellar-zk
- battleship - canonical commit-reveal example
- hidden_hand - canonical ZK circuit example
- Soroban BN254 Documentation
License
MIT OR Apache-2.0
Reversi
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | player_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_move | player: 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 | - | GameState | Current player, pass-count state, and active/finished status. |
get_board | - | BoardState | Raw 64-cell board array (row-major, 0=empty/1=black/2=white), plus width/height. |
get_score | - | ScoreState | Black/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
- Deployer calls
init_game(player_one, player_two); board is seeded with the four-piece opening position, Black (player_one) to move, status active. - 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). - 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_countis set to 1. - Players alternate
submit_movecalls. If neither player has a legal move (pass_countreaches 2) or the board fills,EndConditionSystemsets status to finished. - Either player calls
get_scoreto read the final piece counts and winner.
Cougr APIs used
cougr_core::component::ComponentTrait- gives each component (BoardComponent,TurnComponent,GameStatusComponent,ScoreComponent) acomponent_type()symbol and byte-levelserialize/deserialize, chosen here because the game has a single fixed set of components per instance rather than a dynamic entity population that would benefit fromSimpleWorld/SimpleQueryBuilderscanning.
Build and test commands
cargo test
stellar contract build
Known limitations
- Does not use
GameApp,ScheduleStage, orSimpleWorld- 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
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:
- Player A submits "Rock" โ visible on-chain
- 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
| Function | Parameters | Description |
|---|---|---|
new_match | player_a: Address, player_b: Address, best_of: u32 | Initialize a new match |
commit | player: Address, hash: BytesN<32> | Submit commitment hash |
reveal | player: Address, choice: u32, salt: BytesN<32> | Reveal choice (0=Rock, 1=Paper, 2=Scissors) |
claim_timeout | player: Address | Claim 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 salthas_commit_a/b- flags tracking which players have committedcommit_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 revealedchoice_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
| Component | Fields | Purpose |
|---|---|---|
PlayerCommitment | hash: BytesN<32>revealed: bool | Stores commitment hash |
MatchState | phase: Phasewinner: Option<Address>round: u32 | Game phase tracking |
ScoreBoard | wins_a: u32wins_b: u32draws: u32best_of: u32 | Match scoring |
All components implement cougr_core::component::ComponentTrait for type-safe serialization.
Systems
| System | Responsibility |
|---|---|
| CommitSystem | Accepts hashes, transitions to reveal when both committed |
| RevealSystem | Verifies `sha256(choice |
| ResolveSystem | Compares choices, updates scoreboard |
| MatchSystem | Checks 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:
- Start here: Commit-reveal with SHA256 (this example)
- Next: Upgrade to Poseidon2 hashing (ZK-friendly)
- Advanced: Full ZK proofs with circuits (see
examples/chess/)
Resources
- Cougr Repository
- Commit-Reveal Schemes
- battleship - canonical commit-reveal example
- hidden_hand - ZK circuit example
- Soroban Documentation
License
MIT OR Apache-2.0
Season Ladder
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
initialize | admin: Address | Address | One-time setup; binds the season admin through Ownable. |
register_player | player: Address | PlayerRecord | Self-registration at the starting rating. Frozen while paused. |
report_result | reporter, home, away: Address, match_no: u32, home_goals, away_goals: u32 | MatchResult | Owner-only; records fixture match_no for the ordered pair and updates both ratings. Frozen while paused. |
pause | admin: Address | โ | Owner-only emergency stop. |
unpause | admin: Address | โ | Owner-only resume. |
is_paused | โ | bool | Current pause state. |
owner | โ | Option<Address> | Current season admin. |
player | id: Address | PlayerRecord | Full record (rating, matches, W/D/L). |
rating | id: Address | i64 | Convenience view of a player's rating. |
matches_played | home, away: Address | u32 | Fixtures already recorded for the ordered pair. |
fixture | home, away: Address, match_no: u32 | MatchResult | A 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 โ
OwnableandPausablefromcougr_core::standardsown the authorization ofreport_result/pause/unpauseand the freeze ofregister_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
| Key | Storage | Why |
|---|---|---|
Player(Address) | instance | Small example; ratings and W/D/L are read on every result. |
PairMatches(Address, Address) | instance | Next fixture number per ordered pair; enforces the duplicate guard. |
Fixture(Address, Address, u32) | instance | Each reported result, round-trippable through the fixture view. |
Roster | instance | Registration order, feeds standings. |
Ownable / Pausable state | persistent (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, draw500_000, loss0. - 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
100so 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_playeris rejected,report_resultis rejected,- views (
player,standings,fixture, โฆ) keep working read-only, - only the
Ownableowner canpause/unpause.
After unpause, reporting continues with the existing fixture counters โ
the pause never renumbers or discards fixtures.
Main gameplay flow
- Deploy, then
initialize(admin)โ binds the owner. - Players call
register_playerthemselves (starting rating 1000). - The admin records fixtures in order:
report_result(admin, home, away, 1, 2, 1)โ ratings update immediately. - Repeat with
match_no2, 3, โฆ for the same pair; duplicates or skipped numbers are rejected. - If something is wrong, the admin calls
pause; the season freezes.unpauseresumes exactly where it left off. standingsshows 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'sResults.
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_nomust 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
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:
- Approve - owner signs once to create a scoped session key
- Play - many
tapcalls without wallet prompts - Renew - extend session before expiry (owner re-approves)
- 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_inandexpires_atuse ledger timestamps (seconds).SessionBuilder::expires_in(n)sets expiry toledger.timestamp() + n.
Public contract API
| Function | Parameters | Returns | Description |
|---|---|---|---|
approve_session | owner, max_taps, expires_in | ActiveSession | One-time owner approval creating a scoped session |
tap | owner, key_id | u32 | Gameplay action via active session (no wallet prompt) |
renew_session | owner, key_id, expires_in | ActiveSession | Extend session lifetime (owner must re-approve) |
fallback_tap | owner, key_id | u32 | Tap via session first, fall back to direct owner auth |
score | owner | u32 | Current tap count for the owner |
Cougr APIs used
SessionBuilder- declare allowed actions, max operations, and expirySessionManager::approve- create scoped session after owner authSessionManager::execute_action- gasless gameplay via session keySessionManager::status- poll remaining ops and renewal hintsSessionManager::renew- extend absoluteexpires_attimestampSessionManager::fallback_execute- session-first with direct-auth fallbackSessionStorage- load session keys by owner and key IDMockSession(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
| Function | Parameters | Returns | Description |
|---|---|---|---|
approve_session | owner: Address, max_taps: u32, expires_in: u64 | ActiveSession | Approves a new session key with specific action scopes and expiration constraints. |
tap | owner: Address, key_id: BytesN<32> | u32 | Increments the player's score, verified using the active session key (no wallet prompt). |
renew_session | owner: Address, key_id: BytesN<32>, expires_in: u64 | ActiveSession | Extends the active session key expiration window (requires owner wallet authorization). |
fallback_tap | owner: Address, key_id: BytesN<32> | u32 | Performs a tap action, falling back to direct owner wallet authorization if the session key is expired. |
score | owner: Address | u32 | Retrieves 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
- Authorization: Owner calls
approve_sessionfrom their wallet to authorize a temporary key for thetapaction. - Gameplay: Client signs and executes calls to
tapusing the session key, bypassing any ledger signature popups. - Renewal / Fallback: If the session expires, the client calls
renew_session(wallet-prompted) or callsfallback_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'sScorecomponent.
Recommended testing approach
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
| Test | Description |
|---|---|
approve_and_tap_without_reauth | Multiple taps after single approval |
renew_session_extends_play_window | Renew increases absolute expires_at |
fallback_tap_uses_direct_auth_after_session_expires | Fallback after timestamp expiry |
mock_session_helper_matches_manager_flow | MockSession matches manager flow |
When to use which example
Use session_arena whenโฆ | Use tap_battle whenโฆ |
|---|---|
| Learning SessionManager basics | Building a real game with passkeys |
| Prototyping session UX in a new game | Need combo mechanics and rounds |
| Writing integration tests for sessions | Demonstrating 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
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-zkproof-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
| Component | Fields | Purpose |
|---|---|---|
DeckComponent | player, active_format | Deck context and active season format |
HandCommitmentComponent | commitment, committed, revealed, revealed_card | Hidden-hand state per player |
BoardStateComponent | round, score_one/two, last_played_*, round_state | Visible board after each resolution |
FormatProposalComponent | proposal_id, ban_card, unban_card, proposer, vote_count, status | DAO governance proposals |
GameStatusComponent | status, phase | Top-level match lifecycle |
Systems
| System | Trigger | Responsibility |
|---|---|---|
DraftSystem | Both players committed | Advance PHASE_DRAFT โ PHASE_PLAY |
ProofValidationSystem | play_card | Verify SHA-256 commitment + optional Groth16 proof |
CardPlaySystem | play_card | Enforce active format ban list |
RoundResolutionSystem | Both players revealed | Compare card powers, update scores, reset or end match |
FormatGovernanceSystem | submit_format_proposal | Apply 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
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
| Function | Parameters | Return type | Description |
|---|---|---|---|
init_game | none | () | Initializes a new game on the default 10ร10 grid. |
init_game_with_size | grid_size: i32 | () | Initializes a new game on a custom square grid. |
change_direction | direction: u32 | bool | Changes the snake direction (0 up, 1 down, 2 left, 3 right); returns false for invalid values, reversals, or game-over state. |
update_tick | none | () | Advances the game by one GameApp tick. |
get_score | none | u32 | Returns the current score. |
check_game_over | none | bool | Returns whether the game has reached a terminal state. |
get_head_pos | none | (i32, i32) | Returns the current snake-head position. |
get_snake_length | none | u32 | Returns the number of snake entities. |
get_food_pos | none | (i32, i32) | Returns the current food position. |
get_snake_positions | none | Vec<(i32, i32)> | Returns all snake segment positions. |
get_grid_size | none | i32 | Returns 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.rscontains the Soroban contract entrypoints, storage access, andGameAppwiring.components.rscontains serializable ECS components such asPosition,DirectionComponent,SnakeHead,SnakeSegment, andFood.systems.rscontains reusable game systems for movement, direction validation, collision checks, growth, and food spawning.
Storage model
| Storage class | Data | Why |
|---|---|---|
| Instance storage | none | The example does not need contract-wide configuration shared across games. |
| Persistent storage | state: GameState, world: SimpleWorld | Game progress must survive across transactions. GameState stores compact scalar data; SimpleWorld stores entities and component bytes. |
| Temporary storage | none | No 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
- A player calls
init_gameorinit_game_with_size. - Startup systems spawn the snake head at the grid center and create one food entity.
- The player calls
change_directionto submit a valid non-reversing input. - The player or a relayer calls
update_tick. GameAppruns movement first, then collision and food checks.- A wall/self collision sets
game_over; eating food grows the snake, increments score, and spawns new food. - Query functions expose score, positions, grid size, and terminal state.
Cougr APIs used
| API | Why it is used |
|---|---|
GameApp | Provides the maintained arcade-loop pattern and owns scheduled system execution per tick. |
ScheduleStage / SystemConfig | Ensures movement runs before post-update collision and food systems. |
SimpleWorld | Stores snake, food, and component data in a Soroban-serializable ECS container. |
SimpleQueryBuilder | Scans entities by component type for food, head, and segment queries. |
ComponentTrait | Gives 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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | - | - | Start with 10ร10 grid |
init_game_with_size | grid_size: i32 | - | Start with custom grid |
Control Functions
| Function | Parameters | Returns | Description |
|---|---|---|---|
change_direction | direction: u32 | bool | Change movement direction |
update_tick | - | - | Advance game one step |
Direction Values:
| Value | Direction | Delta (x, y) |
|---|---|---|
| 0 | Up | (0, -1) |
| 1 | Down | (0, +1) |
| 2 | Left | (-1, 0) |
| 3 | Right | (+1, 0) |
Query Functions
| Function | Returns | Description |
|---|---|---|
get_score | u32 | Current score |
check_game_over | bool | Game ended status |
get_head_pos | (i32, i32) | Head coordinates |
get_snake_length | u32 | Total length |
get_food_pos | (i32, i32) | Food coordinates |
get_snake_positions | Vec<(i32, i32)> | All positions |
get_grid_size | i32 | Grid 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
| Network | Contract ID | Explorer |
|---|
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
| File | Purpose |
|---|---|
lib.rs | Soroban contract with public functions and tests |
components.rs | Component definitions implementing ComponentTrait |
systems.rs | Game mechanics (movement, collision, spawning) |
simple_world.rs | Entity 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
| Issue | Solution |
|---|---|
| Rust version errors | rustup update && rustup default stable |
| WASM target missing | rustup target add wasm32v1-none |
| Stellar CLI not found | brew install stellar-cli (macOS) |
| Dependency conflicts | cargo update && cargo clean && cargo build |
| Test snapshots issues | Delete test_snapshots/ directory |
Full Verification
cargo fmt --check && cargo clippy -- -D warnings && cargo test && stellar contract build
References
| Resource | Link |
|---|---|
| Soroban Docs | developers.stellar.org |
| Stellar CLI | CLI Documentation |
| Cougr Repository | github.com/salazarsebas/Cougr |
| Rust Testing | Rust 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
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_level | player: 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. |
push | player: 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 | - | LevelMeta | Move count, solved flag, and level dimensions. |
player_position | - | Position | The player's current position. |
Level encoding
tiles is width * height entries, row-major (index y * width + x), using:
| Constant | Value | Meaning |
|---|---|---|
TILE_EMPTY | 0 | Open floor |
TILE_WALL | 1 | Impassable |
TILE_CRATE | 2 | Pushable crate |
TILE_GOAL | 3 | Crate destination |
TILE_PLAYER | 4 | Player 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
- A caller calls
init_levelwith a grid and their ownAddress. 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. - The caller calls
pushwith a direction.systems::try_pushpanics if the level is already solved or the move is blocked, otherwise moves the player (and a pushed crate, if any), updates the crate'sOnGoalmarker, increments the move count, and re-runs the win check. - Step 2 repeats until every crate carries
OnGoal, at which pointlevel_meta().solvedbecomestrueand furtherpushcalls panic. level_metaandplayer_positioncan be read at any time without mutating state.
Cougr APIs used
| API | Maturity | Why |
|---|---|---|
ArchetypeWorld | Stable | Entities are grouped by component signature, so Wall, Crate, Goal, OnGoal, and Player each form their own archetype without any manual indexing. |
ArchetypeQueryBuilder | Stable | with_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 polllevel_meta/player_positionrather than subscribe to updates.
Space Invaders On-Chain Game
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
| Function | Parameters | Return type | Description |
|---|---|---|---|
init_game | none | () | Initializes ship, invaders, score, lives, and game flags. |
move_ship | direction: i32 | i32 | Moves the ship left/right within bounds and returns x position. |
shoot | none | bool | Spawns a player bullet when possible. |
update_tick | none | bool | Advances bullets, invaders, collisions, score, and game-over checks. |
get_score | none | u32 | Returns score. |
get_lives | none | u32 | Returns remaining lives. |
get_ship_position | none | i32 | Returns ship x position. |
check_game_over | none | bool | Returns terminal state. |
get_active_invaders | none | u32 | Returns number of active invaders. |
get_entity_count | none | u32 | Returns total tracked entity count. |
Live Deployment
| Network | Contract ID | Status |
|---|---|---|
| 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 class | Data | Why |
|---|---|---|
| Instance storage | Per-contract game state where used by this example. | Keeps small arcade state close to the contract instance. |
| Persistent storage | Player- or world-scoped state where the example needs durable keyed state. | Keeps game progress available across invocations. |
| Temporary storage | Not used. | The examples favor deterministic recalculation over ephemeral caches. |
Main gameplay flow
๏ธ Quick Start
- Call the initialization function to create the starting state.
- Submit an input action such as movement, jump, flap, rotation, or shoot.
- Call the tick/update function to run deterministic simulation logic.
- Query public getters for score, position, active state, or terminal status.
- Stop when the game-over/completed condition is reached, or reset/reinitialize where supported.
Cougr APIs used
ComponentTraitand custom component modules document the ECS data boundary.SimpleWorld,SimpleQueryBuilder,GameApp,ScheduleStage, orSystemConfigare 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
snakemodule split andGameApptick wiring.
spawn_and_move
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
spawn | - | u32 | Spawns a new entity at origin (0,0) and returns its generated entity ID. |
move_entity | entity_id: u32, direction: u32 | - | Moves the entity in the specified direction if moves remain. |
position | entity_id: u32 | Option<Position> | Retrieves the current Position of the given entity. |
moves | entity_id: u32 | Option<Moves> | Retrieves the current Moves component of the given entity. |
entity_count | - | u32 | Retrieves 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
- Initialization / Spawn: The user calls
spawn. An entity is spawned at(0,0)with 10 moves remaining. A("COUGR", "set", "position")event is emitted. - Action / Movement: The user calls
move_entitywithdirection(0=North, 1=East, 2=South, 3=West). The remaining moves decrement, the position updates, and a position set event is emitted. - Query: The user reads the entity's position or moves remaining.
Cougr APIs used
SorobanGameandimpl_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.
Recommended testing approach
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
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
initialize | creator: Address, entrants: Vec<Address>, token_address: Address, stake: i128, timeout_seconds: u64 | () | Creator-authorized setup for four or eight unique entrants. |
stake_match | player: Address, match_id: u32 | () | Transfers the player's stake into the contract before the match deadline. |
resolve_match | winner: Address, match_id: u32 | () | Winner-authorized settlement after both players stake; pays the pot and advances the winner. |
forfeit_no_show | match_id: u32 | () | After the deadline, refunds and advances the sole player who staked. |
get_match | match_id: u32 | MatchRecord | Reads 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
- The creator initializes the contract with four or eight unique players, a Soroban token address, a positive stake, and a timeout.
- Each first-round player calls
stake_match; their stake moves to the contract and is tracked only in that match's record. - After both stakes arrive, the authenticated winner calls
resolve_match. The winner receives both stakes and advances automatically. - If exactly one player stakes before the deadline, anyone can call
forfeit_no_showafter it expires. The present player's stake is returned and they advance, so the next round can proceed. - 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
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | puzzle: 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_value | row: 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 | - | GameState | Current status (0=playing, 1=solved) and move count. |
get_cell | row: u32, col: u32 | CellState | Current value at the cell and whether it is a fixed given. |
is_solved | - | bool | Whether 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
- Deployer calls
init_game(puzzle)with an 81-cell array representing the starting grid; non-zero cells are marked fixed and cannot be overwritten. - 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. - On a legal placement, the board updates and the move counter increments.
- After every placement,
CompletionSystemre-checks all 9 rows, 9 columns, and 9 blocks. When the board is completely and correctly filled, status flips to solved. - Caller reads
is_solvedorget_stateto check completion;get_cellreads individual cells (e.g., to render the board off-chain).
Cougr APIs used
cougr_core::component::ComponentTrait- gives each component (BoardComponent,FixedCellsComponent,GameStatusComponent,MoveCountComponent) acomponent_type()symbol and explicit byteserialize/deserialize, chosen because the puzzle has one fixed-shape 81-cell grid rather than a dynamic entity population that would benefit fromSimpleWorld/SimpleQueryBuilderscanning.
Build and test commands
cargo test
stellar contract build
Known limitations
- Does not use
GameApp,ScheduleStage, orSimpleWorld- validation runs directly from contract entrypoints since a single-player puzzle with one decision point per call has no need for staged scheduling. init_gameaccepts 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
Transitional example: This example uses an older Cougr pattern and is preserved for compatibility reference. For the current recommended approach, see
snake.
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:
| Traditional | With Passkeys |
|---|---|
| Write down 24-word seed phrase | Tap Face ID / Touch ID |
| Approve every transaction in wallet | Session key handles gameplay |
| Complex key management | Browser/OS manages credentials |
| Crypto-native only | Anyone 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_atuse ledger timestamps (seconds), not ledger sequence numbers. Round duration (start_round) still uses ledger sequences for combo timing.
ECS Components
| Component | Fields | Purpose |
|---|---|---|
PasskeyIdentity | pubkey, registered_at | Player's passkey credential |
TapCounter | count, combo, multiplier, last_tap_ledger | Tap tracking per round |
PowerUp | charges, kind | Charged abilities from combos |
RoundState | started_at, duration, scores, finished | Match state |
PlayerProfile | total_wins, total_taps, best_combo | Persistent stats |
Session state lives in SessionManager / SessionStorage - not as a custom ECS component.
Contract API
Authentication
| Function | Parameters | Returns | Description |
|---|---|---|---|
register_passkey | player, pubkey: BytesN<65> | - | Register a secp256r1 passkey |
authenticate_and_start_session | player, signature, challenge, duration | Address | Auth + create session |
Session Management
| Function | Parameters | Returns | Description |
|---|---|---|---|
session_status | owner, key_id | SessionStatus | Remaining ops, expiry, renewal hint |
renew_session | owner, key_id, expires_in | ActiveSession | Extend session (owner re-approves) |
fallback_tap | owner, key_id | TapResult | Tap via session or direct auth fallback |
get_latest_session_key | player | BytesN<32> | Latest session key ID for a player |
Gameplay
| Function | Parameters | Returns | Description |
|---|---|---|---|
tap | session_key | TapResult | Submit a tap (gasless) |
use_power_up | session_key, power_up: u32 | - | Activate a power-up |
start_round | player_a, player_b, duration | - | Start a match |
get_round | - | RoundState | Get round state (auto-finalizes) |
get_profile | player | PlayerProfile | Get persistent stats |
Power-Up Types
| ID | Name | Effect |
|---|---|---|
| 0 | DoubleTap | +10 bonus points |
| 1 | Shield | Defensive (blocks opponent effects) |
| 2 | Burst | +25 bonus points |
Game Mechanics
Combo System
Taps within 5 ledger sequences of each other maintain a combo streak:
| Combo | Multiplier | Effect |
|---|---|---|
| 1 | 1x | Normal tap |
| 2-4 | 2-4x | Building combo |
| 5 | 5x | Earns a power-up charge |
| 10+ | 10x (max) | Maximum multiplier |
Game Constants
| Constant | Value | Description |
|---|---|---|
COMBO_WINDOW | 5 ledgers | Time window for combo |
MAX_MULTIPLIER | 10 | Maximum score multiplier |
COMBO_CHARGE_THRESHOLD | 5 | Combos to earn a power-up charge |
DOUBLE_TAP_BONUS | 10 | DoubleTap power-up bonus |
BURST_BONUS | 25 | Burst power-up bonus |
DEFAULT_MAX_OPS | 500 | Default session operations |
๏ธ Quick Start
Prerequisites
| Tool | Version | Installation |
|---|---|---|
| Rust | 1.88.0+ | rustup.rs |
| Stellar CLI | Latest | Stellar 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 โ
| Test | Description |
|---|---|
test_register_passkey | Passkey registration stores key on-chain |
test_register_multiple_players | Multiple players register independently |
test_session_creation | Session created and validated |
test_session_ops_decrement | Operations decremented per action |
test_session_ops_exhausted | Session stops after ops limit |
test_tap_increments | Tap counter increments correctly |
test_combo_streak | Combo streak with multiplier |
test_multiplier_cap | Multiplier capped at MAX_MULTIPLIER |
test_power_up_charge_and_use | Combo charges power-ups |
test_power_up_no_charges | No activation without charges |
test_start_round | Round initializes correctly |
test_start_round_no_passkey | Round requires registered passkeys |
test_round_scoring_and_winner | Scores tracked per player |
test_profile_stats | Profile tracks taps and combos |
test_default_profile | Default profile for new players |
test_renew_session_extends_play_window | Renew extends absolute expiry |
test_fallback_tap_uses_direct_auth_after_session_expires | Fallback 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
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
| Function | Parameters | Return type | Description |
|---|---|---|---|
init_game | none | GameState | Initializes an empty board and current/next pieces. |
move_left | none | bool | Attempts to move the active piece left. |
move_right | none | bool | Attempts to move the active piece right. |
move_down | none | bool | Soft-drops the active piece or locks it if blocked. |
rotate | none | bool | Attempts clockwise rotation. |
drop | none | u32 | Hard-drops and locks the active piece; returns rows dropped. |
update_tick | none | GameState | Runs one gravity tick. |
get_state | none | GameState | Returns 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 class | Data | Why |
|---|---|---|
| Instance storage | Per-contract game state where used by this example. | Keeps small arcade state close to the contract instance. |
| Persistent storage | Player- or world-scoped state where the example needs durable keyed state. | Keeps game progress available across invocations. |
| Temporary storage | Not used. | The examples favor deterministic recalculation over ephemeral caches. |
Main gameplay flow
- Call the initialization function to create the starting state.
- Submit an input action such as movement, jump, flap, rotation, or shoot.
- Call the tick/update function to run deterministic simulation logic.
- Query public getters for score, position, active state, or terminal status.
- 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
| Aspect | Traditional Soroban | With Cougr-Core ECS |
|---|---|---|
| Code Organization | Monolithic contract logic | Modular components & systems |
| State Management | Manual storage handling | Automatic entity-component management |
| Game Logic | Tightly coupled functions | Reusable, composable systems |
| Scalability | Difficult to extend | Easy to add new features |
| Code Reuse | Limited | High - components are portable |
| Testing | Complex integration tests | Unit testable components |
Cougr-Core Advantages
-
Entity-Component-System Pattern
- Separates data (components) from logic (systems)
- Makes code more maintainable and testable
- Enables parallel processing of game logic
-
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 }); }
ComponentTraitand custom component modules document the ECS data boundary.SimpleWorld,SimpleQueryBuilder,GameApp,ScheduleStage, orSystemConfigare 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
snakemodule split andGameApptick wiring.
Tic Tac Toe
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
| Function | Parameters | Returns | Description |
|---|---|---|---|
init_game | player_x: Address, player_o: Address | GameState | Starts 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_move | player: Address, position: u32 | MoveResult | Requires 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 | - | GameState | Current board, both player addresses, whose turn it is, move count, and status. |
is_valid_move | position: u32 | bool | Whether 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_game | player: Address | GameState | Requires 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_moverequires Soroban authorization fromplayer. The address must be a registered player, and it must be their turn.reset_gamerequires authorization from one of the two registered players.init_gamerefuses 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
- 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. - 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. - On a legal move, the cell is marked,
systems::detect_winnerre-checks all 8 winning lines plus the draw condition, and turn state is updated (status, move count, whose turn is next). - Players alternate
make_movecalls untildetect_winnerreports a win (status1or2) or a draw (status3), after which further moves returngameover. - Either player can call
get_stateorget_winnerto read the outcome. Either registered player can callreset_game(player)with authorization to start over.
Cougr APIs used
cougr_core::{impl_rich_component!}- used forBoard(holds aVec<u32>) andPlayers(holds twoAddressvalues). Both fields require Soroban's XDR codec rather than fixed-size byte packing, soimpl_rich_component!was chosen to getRichComponentTraitfor free from the#[contracttype]derive, avoiding a hand-writtenserialize/deserializepair forVec<u32>andAddresslike the onereversi'scomponents.rsstill carries.cougr_core::{impl_component!}- used forTurnState, which is three fixed-size plain fields (bool,u32,u32).impl_component!generates a compact, fully typedComponentTraitimplementation (byte-packed, not XDR) since there are noAddress/Vecfields needing the heavier rich-component codec.cougr_core::game::SorobanGame/impl_soroban_game!- generatesload_world/save_worldfor the#[contract]struct so every entrypoint can read and persist theSimpleWorldwithout 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
GameApporScheduleStage- validation and win detection run synchronously insidemake_movesince 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
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
| Component | Fields | Purpose |
|---|---|---|
EnemyComponent | hp, max_hp, speed, path_index | Represents enemies moving along the path |
TowerComponent | kind, range, damage, cooldown | Represents static defenders |
WaveComponent | current_wave, total_waves, remaining_spawns | Tracks wave progression |
BaseComponent | health, max_health | Tracks survival condition |
GameStatusComponent | status, tick_count, enemies_killed | Tracks 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
trueif 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
-
Navigate to the example directory:
cd examples/tower_defense -
Build the contract:
cargo build -
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
| Tower | Range | Damage | Cooldown |
|---|---|---|---|
| Basic | 2 | 10 | 1 tick |
| Sniper | 4 | 25 | 3 ticks |
| Splash | 1 | 15 | 2 ticks |
Trading Card Game
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
| ID | Kind | Cost | Power | Toughness | Notes |
|---|---|---|---|---|---|
| 1 | Creature | 1 | 1 | 2 | Cheap blocker |
| 2 | Creature | 2 | 2 | 2 | Vanilla |
| 3 | Creature | 2 | 1 | 3 | Sturdy blocker |
| 4 | Creature | 3 | 3 | 2 | Aggressive |
| 5 | Creature | 3 | 2 | 4 | Resilient |
| 6 | Creature | 4 | 4 | 4 | Mid-range |
| 7 | Creature | 5 | 5 | 5 | Threat |
| 8 | Creature | 6 | 6 | 6 | Late-game bomb |
| 9 | Spell | 2 | 3 | - | 3 direct damage |
| 10 | Spell | 4 | 5 | - | 5 direct damage |
ECS Components
| Component | Fields | Purpose |
|---|---|---|
PlayerHand | cards: Vec<CardId> | Cards currently held |
PlayerField | creatures: Vec<CreatureState> | Creatures on the battlefield |
PlayerStats | health, mana, max_mana | Health and resource pool |
MatchState | turn, active_player, phase, status | Match phase tracking |
ECS Systems
| System | Trigger | Responsibility |
|---|---|---|
DrawSystem | Turn start | Draw one card from deck into hand |
ManaSystem | Turn start | Increment max mana (cap 10), refill mana |
PlayCardSystem | PlayCreature | Validate mana, move card to field |
CastSpellSystem | CastSpell | Validate mana, deal damage to opponent |
CombatSystem | DeclareAttack | Resolve creature-vs-creature or face damage |
WinConditionSystem | End of turn | Detect 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)
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
| Function | Parameters | Description |
|---|---|---|
init_game | player: Address, map_root: BytesN<32>, width: u32, height: u32, total_treasures: u32 | Initialize game with committed map root |
explore | player: Address, x: u32, y: u32, cell_value: u32, proof: OnChainMerkleProof | Explore 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) andtotal_treasures fog_root- initial sparse Merkle tree root (all cells unexplored)
Revealed State
Updated during exploration:
explored_map.explored- sparse map of visited cell indicesplayer_state- position, health, score, treasures foundfog_root- recomputedSparseMerkleTreeroot after each exploration
Proven State
The explore function verifies cell contents via Merkle inclusion:
- Reconstructs the leaf hash from
(x, y, cell_value)using the same encoding as the commit phase - Builds
OnChainMerkleProoffrom sibling hashes and path bits - Calls
verify_inclusion(&env, &proof, &map_root.root)- rejects if proof is invalid - 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
- Cougr Repository
- Merkle Trees
- battleship - canonical commit-reveal example
- hidden_hand - ZK circuit example
- Soroban Documentation
License
MIT OR Apache-2.0
zk_dice_session
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.
| Subsystem | Source 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:
- Session gate โ
SessionManager::execute_actionchecks expiry, budget, and action scope. No wallet prompt. - ZK gate โ
spec.verify_dice_rollis called identically to the corresponding call indice_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
| Test | What it proves |
|---|---|
roll_succeeds_inside_active_session | Roll accepted when session is active (DoD) |
roll_fails_with_no_session | Roll panics when no session key exists (DoD) |
roll_fails_after_session_expiry | Roll panics once the session window closes (DoD) |
roll_returns_false_for_invalid_proof | Out-of-range roll_result โ false, proof gate intact |
roll_fails_when_session_budget_exhausted | Budget exhaustion panics before proof verification |
independent_sessions_for_multiple_players | Each player's session is isolated |
init_game_rejects_single_sided_die | Circuit parameter contract enforced |
circuit_spec_matches_fair_dice | Circuit ID matches FairDice (same path as dice_duel) |
Cougr features demonstrated
zk-circuitsโfair_diceverifier,Groth16Proof,CircuitId::FairDicesession-authโSessionBuilder,SessionManager,SessionStorage,ActiveSessionsoroban-gameโ#[contract]/#[contractimpl]/#[contracttype]
Design
โณ This section is being built. All design documents are gaps identified in
docs/strategy/12-documentation-architecture.mdand 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.
| Guide | Status |
|---|---|
| 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.
| Document | Description |
|---|---|
| Contributing | How to open issues, write code, and get PRs merged |
| Code of Conduct | Expected behaviour in all project spaces |
| Governance | How decisions are made, who can merge, how disputes are resolved |
| Security | How to report vulnerabilities |
| Roadmap | Where the project is headed |
| RFC Process | How to propose significant changes before implementing them |
| Changelog | What 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:
| Area | Expected outcome |
|---|---|
| Core framework | Improved ECS, scheduling, storage, authorization, or zero-knowledge capabilities |
| Examples | New game patterns, better reference implementations, or tighter example documentation |
| Documentation | Clearer architecture, setup, or usage guidance aligned with the current codebase |
| Quality | Better 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 fixapplies the automatic fixes:cargo fmtand the docs site sync.make check-allruns every check, whatever you changed.- If a tool is missing (for example
stellar,mdbook, orcargo-deny),make checktells you how to install it. Skipped checks still run in CI. - CI tests your branch merged into
main. Keep it current withgit fetch upstream && git rebase upstream/main(add the remote once withgit 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:
- what changed
- why the change was needed
- how it was validated
- 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 toexamples/catalog.tomland validate the catalog locally (requires Python 3.11 or newer):python3 scripts/validate_catalog.pyThe validator checks required fields, allowed
categoryandmaturityvalues, that eachnamemaps to a directory underexamples/, thatcougr_featuresis a non-empty list, and that anyscreenshotpath exists.examples/catalog_broken.tomlis 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, orzk::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/Cougrissues
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.mdPublic 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:
| Area | Status | Guidance |
|---|---|---|
| ECS runtime and storage | Stable | Part of the 1.0 contract when used through the documented onboarding and runtime surfaces |
| Accounts and smart-account flows | Beta | Do not assume full production guarantees without project-specific review |
Standards layer (standards) | Stable | Reusable contract primitives are part of the 1.0 stable contract |
Privacy primitives (zk::stable) | Stable | Commit-reveal, hidden-state codecs, and Merkle utilities are the stable privacy contract |
| Advanced ZK verification | Experimental | Treat 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:
- Do not open a public issue with exploit details.
- Report the issue privately to the project maintainers.
- 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.mdexists in the main repository yet. Perdocs/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-chaincougr_core::cors- CORS configuration validation and dynamic origin allowlist for HTTP gateways in front of a game contract:CorsConfigvalidates origins, methods, header names, and credential/wildcard combinations;OriginAllowlistsupports 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 headerscougr-cli- new workspace member publishing thecougrbinarycougr new <name> [--template <name>]- scaffolds a Soroban game contract crate following the canonicallib.rs/components.rs/systems.rslayout, with a passingtest::GameHarnesssuite and a dependency on the publishedcougr-corerelease rather than a path dependency- Four embedded templates, each derived from a canonical example and compiled into
the binary so
cougr newworks offline:starter(spawn_and_move),turn-based(tic_tac_toe),hidden-info(hidden_hand),session-auth(session_arena) - CLI CI workflow - validates
cougr-clionce, caches the root workspace target, and compiles generated projects when template sources change
Fixed
zk::merkle::proof::verify_inclusion- a proof'sleaf_indexwas never checked against the position itspath_bitsactually walk, so a proof for one leaf could be relabelled to claim another leaf's position and still verify.verify_inclusionnow rejects apath_bits/leaf_indexmismatch (ZKError::LeafIndexMismatch), an out-of-rangeleaf_index(ZKError::LeafOutOfBounds), anddepth > MAX_DEPTH(ZKError::MaxDepthExceeded, previously an overflow panic onpath_bits >> ifordepth > 32) before hashing.zk::merkle::tree::verify_proof, the in-memory equivalent, gets the samepath_indices/leaf_indexcheckzk::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 degeneratedepth = 0proof withleaf == root) is rejected even though it would otherwise be internally consistentexamples/battleship,examples/treasure_hunt- both pin the expected proof depth and recheckpath_bitsagainst the claimed cell locally, since they still depend on the publishedcougr-core = "1.1.0"and won't pick up the core fix until a release
1.1.0
Added
game::SorobanGametrait - standardload_world/save_worldcontract pattern; implement once withimpl_soroban_game!(Contract, "key"), use in every entrypointimpl_soroban_game!macro - wiresSorobanGameto any#[contract]structSimpleWorld::load_from_instance- load world from Soroban instance storage, returning a fresh empty world on first callSimpleWorld::save_to_instance- persist world to Soroban instance storageSimpleWorld::set_rich_observed- store a rich component and emit aRichComponentChangedEventfor off-chain indexersSimpleWorld::remove_rich_observed- remove a rich component and emit adeleventRichComponentChangedEvent- new Soroban event type with topics("COUGR", "rich", component_type)for rich component change notificationsspawn_and_moveexample - canonical Cougr starter game demonstrating the complete idiomatic pattern:impl_component_observed!+SorobanGame+ typed ECS accessSorobanGamere-exported fromprelude- import fromcougr_core::prelude::*cougr_core::circuits- four pre-built ZK game builders (hidden cards, fog of war, fair dice, sealed bid) with pipeline-embedded verification keyscougr_core::session-SessionManager,SessionStatus, andActiveSession(Beta)cougr_core::test-GameHarness,Scenario, andReplayLogsandbox behind thetestutilsfeature- Circom pipeline -
internal/cougr-core-circuitswith CI workflow and on-chain Groth16 proof verification using real VKs - ZK examples -
hidden_hand,fog_explorer,dice_duel, andblind_auction - Workspace subcrates -
internal/cougr-core-{circuits,session,test}per ADR 0007
Changed
tic_tac_toeexample modernised: replaced ~200 lines of manual serialization withimpl_rich_component!forBoardandPlayers, andimpl_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_arenaexample added as canonical reference forsession::SessionManager
Stability Notes
game::SorobanGameis StableSimpleWorld::load_from_instance/save_to_instanceare Stableset_rich_observed/remove_rich_observedare StableRichComponentChangedEventis Stablecougr_core::sessionis Betacougr_core::circuitsand embedded test VKs are Experimentalcougr_core::testis Experimental (testutilsonly)
1.0.0
Added
appas the default gameplay runtime surfaceauth,privacy, andopsas product-level domain namespacesRuntimeWorldandRuntimeWorldMutas shared Soroban-first backend contracts- stronger stage scheduling with ordering, sets, and validation
SimpleQueryBuilder, query state/cache improvements, and richerArchetypeWorldquery 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
battleshipnow uses stable privacy primitives fromzk::stable- documentation now treats
SimpleWorldandArchetypeWorldas 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
appover 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, andSimpleQuery - Prefer
ops,privacy, andauthin 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