diff --git a/.github/workflows/circuit_examples.yml b/.github/workflows/circuit_examples.yml index 3c8b8c71..2233bf17 100644 --- a/.github/workflows/circuit_examples.yml +++ b/.github/workflows/circuit_examples.yml @@ -46,14 +46,25 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32v1-none - name: Cache dependencies uses: Swatinem/rust-cache@v2 + with: + workspaces: examples/${{ matrix.example }} - name: Test ${{ matrix.example }} working-directory: examples/${{ matrix.example }} run: cargo test + - name: Install Stellar CLI + uses: stellar/stellar-cli@v25.1.0 + + - name: Stellar contract build + working-directory: examples/${{ matrix.example }} + run: stellar contract build + circuits-lib: name: Circuits Module runs-on: ubuntu-latest diff --git a/.github/workflows/session_arena.yml b/.github/workflows/session_arena.yml new file mode 100644 index 00000000..2993feb3 --- /dev/null +++ b/.github/workflows/session_arena.yml @@ -0,0 +1,28 @@ +name: Session Arena Example CI + +on: + push: + branches: [main, develop] + paths: + - 'examples/session_arena/**' + - '!examples/session_arena/**/*.md' + - '.github/workflows/example_reusable.yml' + - '.github/workflows/session_arena.yml' + pull_request: + branches: [main, develop] + paths: + - 'examples/session_arena/**' + - '!examples/session_arena/**/*.md' + - '.github/workflows/example_reusable.yml' + - '.github/workflows/session_arena.yml' + +concurrency: + group: session-arena-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + uses: ./.github/workflows/example_reusable.yml + with: + job_name: Session Arena Example CI + working_directory: examples/session_arena diff --git a/.github/workflows/spawn_and_move.yml b/.github/workflows/spawn_and_move.yml new file mode 100644 index 00000000..5e0b7b4a --- /dev/null +++ b/.github/workflows/spawn_and_move.yml @@ -0,0 +1,28 @@ +name: Spawn And Move Example CI + +on: + push: + branches: [main, develop] + paths: + - 'examples/spawn_and_move/**' + - '!examples/spawn_and_move/**/*.md' + - '.github/workflows/example_reusable.yml' + - '.github/workflows/spawn_and_move.yml' + pull_request: + branches: [main, develop] + paths: + - 'examples/spawn_and_move/**' + - '!examples/spawn_and_move/**/*.md' + - '.github/workflows/example_reusable.yml' + - '.github/workflows/spawn_and_move.yml' + +concurrency: + group: spawn-and-move-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ci: + uses: ./.github/workflows/example_reusable.yml + with: + job_name: Spawn And Move Example CI + working_directory: examples/spawn_and_move diff --git a/examples/README.md b/examples/README.md index 209698ce..a01b07ea 100644 --- a/examples/README.md +++ b/examples/README.md @@ -83,11 +83,17 @@ them as the default onboarding path. The maintained reference architectures for Cougr should be read in this order: -- `spawn_and_move`: **start here** — canonical onboarding example showing the full modern Cougr pattern -- `tic_tac_toe`: turn-based game with rich components (`impl_rich_component!`) for `Address` and `Vec` fields +- `spawn_and_move`: **start here** — canonical onboarding example showing the full modern Cougr pattern (`SorobanGame`, `impl_component_observed!`, typed ECS) +- `tic_tac_toe`: turn-based game showing rich components (`impl_rich_component!`, `impl_soroban_game!`) for `Address` and `Vec` fields +- `session_arena`: session lifecycle, authorization scopes, and fallback direct-auth flows (`SessionManager`) - `snake`: canonical arcade loop and `GameApp` tick model - `battleship`: canonical hidden-information / commit-reveal flow using `privacy::stable` Merkle primitives - `guild_arena`: canonical account recovery and multi-device authorization flow +- **ZK Circuit Reference Examples**: + - `hidden_hand`: private card deals via `circuits::hidden_cards` + - `fog_explorer`: private line-of-sight map verification via `circuits::fog_of_war` + - `dice_duel`: verifiable on-chain dice rolling via `circuits::fair_dice` + - `blind_auction`: sealed-bid auction reveals via `circuits::sealed_bid` ## Conventions diff --git a/examples/ai_dungeon_master_arena/.gitignore b/examples/ai_dungeon_master_arena/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/ai_dungeon_master_arena/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/angry_birds/.gitignore b/examples/angry_birds/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/angry_birds/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/angry_birds/Cargo.toml b/examples/angry_birds/Cargo.toml index 68986411..631781c1 100644 --- a/examples/angry_birds/Cargo.toml +++ b/examples/angry_birds/Cargo.toml @@ -3,7 +3,7 @@ name = "angry_birds" version = "0.1.0" edition = "2021" publish = false -description = "Angry Birds turn-based physics puzzle using Cougr-Core ECS framework on Stellar Soroban" +description = "Angry Birds turn-based physics puzzle using Cougr-Core ECS on Stellar Soroban" license = "MIT OR Apache-2.0" [lib] diff --git a/examples/angry_birds/README.md b/examples/angry_birds/README.md index 4d316cfc..62bdb9d7 100644 --- a/examples/angry_birds/README.md +++ b/examples/angry_birds/README.md @@ -146,7 +146,7 @@ stellar network testnet stellar contract deploy \ --wasm target/wasm32v1-none/release/angry_birds.wasm \ --source \ - --network testnet + --network # Note the contract ID for subsequent interactions ``` @@ -158,7 +158,7 @@ stellar contract deploy \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- init_level \ --player \ --level_id 1 @@ -167,7 +167,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- shoot \ --player \ --angle 45000 \ @@ -177,7 +177,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- get_state ``` diff --git a/examples/arkanoid/.gitignore b/examples/arkanoid/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/arkanoid/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/arkanoid/README.md b/examples/arkanoid/README.md index 5b1fc978..3e7a1d6a 100644 --- a/examples/arkanoid/README.md +++ b/examples/arkanoid/README.md @@ -113,10 +113,10 @@ Get test XLM from Friendbot: ```bash # Generate a new keypair -stellar keys generate alice --network testnet +stellar keys generate alice --network # Fund the account -stellar keys fund alice --network testnet +stellar keys fund alice --network ``` Or use the web faucet: https://faucet-stellar.acachete.xyz @@ -131,10 +131,10 @@ stellar contract build stellar contract deploy \ --wasm target/wasm32v1-none/release/arkanoid.wasm \ --source alice \ - --network testnet + --network ``` -Save the contract ID returned (e.g., `CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`). +Save the contract ID returned (e.g., `XXX`). ### 3. Invoke Contract Functions @@ -144,7 +144,7 @@ Save the contract ID returned (e.g., `CXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX stellar contract invoke \ --id \ --source alice \ - --network testnet \ + --network \ -- init_game ``` @@ -170,14 +170,14 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source alice \ - --network testnet \ + --network \ -- move_paddle --direction 1 # Move left (direction = -1) stellar contract invoke \ --id \ --source alice \ - --network testnet \ + --network \ -- move_paddle --direction -1 ``` @@ -187,7 +187,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source alice \ - --network testnet \ + --network \ -- update_tick ``` @@ -203,7 +203,7 @@ This advances the game by one frame: stellar contract invoke \ --id \ --source alice \ - --network testnet \ + --network \ -- get_game_state ``` @@ -213,7 +213,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source alice \ - --network testnet \ + --network \ -- check_game_over ``` diff --git a/examples/asteroids/.gitignore b/examples/asteroids/.gitignore index b6821b26..0dccac60 100644 --- a/examples/asteroids/.gitignore +++ b/examples/asteroids/.gitignore @@ -5,4 +5,6 @@ target .soroban .stellar -/test_snapshots/ \ No newline at end of file +/test_snapshots/ +target/ +*.wasm diff --git a/examples/asteroids/Cargo.toml b/examples/asteroids/Cargo.toml index 231da9b1..5823be86 100644 --- a/examples/asteroids/Cargo.toml +++ b/examples/asteroids/Cargo.toml @@ -1,4 +1,5 @@ [package] +description = "Arcade asteroid shooter with entity movement and collision systems" name = "asteroids" version = "0.0.0" edition = "2021" diff --git a/examples/asteroids/README.md b/examples/asteroids/README.md index c619538d..e9dca516 100644 --- a/examples/asteroids/README.md +++ b/examples/asteroids/README.md @@ -94,13 +94,13 @@ stellar network add testnet \ --rpc-url https://soroban-testnet.stellar.org \ --network-passphrase "Test SDF Network ; September 2015" -stellar keys generate testnet --network testnet -stellar keys fund testnet --network testnet +stellar keys generate testnet --network +stellar keys fund testnet --network stellar contract deploy \ --wasm target/wasm32v1-none/release/asteroids.wasm \ --source testnet \ - --network testnet + --network ``` Invoke contract methods: @@ -110,7 +110,7 @@ Invoke contract methods: stellar contract invoke \ --id \ --source testnet \ - --network testnet \ + --network \ -- \ init_game @@ -118,21 +118,21 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source testnet \ - --network testnet \ + --network \ -- \ rotate_ship --delta_steps 1 stellar contract invoke \ --id \ --source testnet \ - --network testnet \ + --network \ -- \ thrust_ship stellar contract invoke \ --id \ --source testnet \ - --network testnet \ + --network \ -- \ shoot @@ -140,7 +140,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source testnet \ - --network testnet \ + --network \ -- \ update_tick @@ -148,7 +148,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source testnet \ - --network testnet \ + --network \ -- \ get_game_state ``` diff --git a/examples/battleship/.gitignore b/examples/battleship/.gitignore index 88de57d9..2093a7ff 100644 --- a/examples/battleship/.gitignore +++ b/examples/battleship/.gitignore @@ -1,6 +1,5 @@ # Rust target/ -Cargo.lock **/*.rs.bk *.pdb diff --git a/examples/battleship/Cargo.toml b/examples/battleship/Cargo.toml index 5f4688d1..e4a9c0c8 100644 --- a/examples/battleship/Cargo.toml +++ b/examples/battleship/Cargo.toml @@ -11,10 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = "1.0.0" +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/battleship/README.md b/examples/battleship/README.md index e09501de..a60e42e9 100644 --- a/examples/battleship/README.md +++ b/examples/battleship/README.md @@ -151,6 +151,9 @@ cargo build --release --target wasm32v1-none cargo test ``` +**Recommended Testing Approach:** +For verification of complex setup commitment sequences and attack/reveal turn-based interactions, utilize `GameHarness` and `Scenario` (see [sandbox_tests.rs](src/sandbox_tests.rs)). This ensures cryptographic commitments and Merkle proofs verify correctly across turns. + **Test Coverage (10 tests):** - ✅ Game initialization - ✅ Board commitment @@ -191,18 +194,18 @@ assert!(verifier.verify(&env, &proof, &stored_merkle_root)?); ## Security Considerations -### ✅ Secure +### 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 +### ️ 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 +### Best Practices ```rust // ✅ Good: Random salt let salt = generate_random_bytes(32); @@ -254,11 +257,11 @@ For a 10x10 board: ```bash # Deploy to testnet -stellar keys generate battleship-deployer --network testnet --fund +stellar keys generate battleship-deployer --network --fund stellar contract deploy \ --wasm target/wasm32v1-none/release/battleship.wasm \ --source battleship-deployer \ - --network testnet + --network ``` ## Resources diff --git a/examples/battleship/src/lib.rs b/examples/battleship/src/lib.rs index 9458123d..1c37530d 100644 --- a/examples/battleship/src/lib.rs +++ b/examples/battleship/src/lib.rs @@ -98,6 +98,7 @@ const GRID_SIZE: u32 = 10; const TOTAL_SHIP_CELLS: u32 = 17; // 5+4+3+3+2 #[contract] +#[derive(Clone)] pub struct BattleshipContract; #[contractimpl] @@ -344,5 +345,7 @@ impl BattleshipContract { } } +#[cfg(test)] +mod sandbox_tests; #[cfg(test)] mod test; diff --git a/examples/battleship/src/sandbox_tests.rs b/examples/battleship/src/sandbox_tests.rs new file mode 100644 index 00000000..256c3d98 --- /dev/null +++ b/examples/battleship/src/sandbox_tests.rs @@ -0,0 +1,97 @@ +#![cfg(test)] + +use crate::{BattleshipContract, BattleshipContractClient, Phase}; +use cougr_core::privacy::stable::{to_on_chain_proof, MerkleTree}; +use cougr_core::test::{GameHarness, PlayerSlot, Scenario}; +use soroban_sdk::{Bytes, BytesN, Env}; + +fn build_merkle_tree(env: &Env, board: &[u32; 100]) -> (BytesN<32>, MerkleTree) { + let mut leaves = [[0u8; 32]; 100]; + for (idx, &value) in board.iter().enumerate() { + let mut data = Bytes::new(env); + data.append(&Bytes::from_array(env, &(idx as u32).to_be_bytes())); + data.append(&Bytes::from_array(env, &value.to_be_bytes())); + let leaf: BytesN<32> = env.crypto().sha256(&data).into(); + leaves[idx] = leaf.to_array(); + } + let tree = MerkleTree::from_leaves(env, &leaves).unwrap(); + (tree.root_bytes(env), tree) +} + +fn make_commitment(env: &Env, board: &[u32; 100], salt: &BytesN<32>) -> BytesN<32> { + let mut data = Bytes::new(env); + for &cell in board.iter() { + data.append(&Bytes::from_array(env, &cell.to_be_bytes())); + } + for i in 0..32 { + data.push_back(salt.get(i).unwrap()); + } + env.crypto().sha256(&data).into() +} + +#[test] +fn sandbox_battleship_gameplay_scenario() { + let env = Env::default(); + let mut harness = GameHarness::new(env, BattleshipContract); + harness.mock_players(2); + harness.mock_all_auths(); + + let client = BattleshipContractClient::new(harness.env(), harness.contract_id()); + let p_a = harness.player(PlayerSlot(0)).clone(); + let p_b = harness.player(PlayerSlot(1)).clone(); + + client.new_game(&p_a, &p_b); + + // Setup boards and commitments + let board_a = [0u32; 100]; + let mut board_b = [0u32; 100]; + board_b[10] = 1; // Ship at (1, 0) (index 10) + + let salt_a = BytesN::from_array(harness.env(), &[1u8; 32]); + let salt_b = BytesN::from_array(harness.env(), &[2u8; 32]); + + let commitment_a = make_commitment(harness.env(), &board_a, &salt_a); + let (root_a, _) = build_merkle_tree(harness.env(), &board_a); + + let commitment_b = make_commitment(harness.env(), &board_b, &salt_b); + let (root_b, tree_b) = build_merkle_tree(harness.env(), &board_b); + + Scenario::new("battleship deployment and attack") + .players(2) + .turns(4) + .run(&harness, |player_slot, turn, h| { + let c = BattleshipContractClient::new(h.env(), h.contract_id()); + let p = h.player(player_slot).clone(); + match turn.0 { + 0 => { + // Player A commits + c.commit_board(&p, &commitment_a, &root_a); + assert_eq!(c.get_state().turn_state.phase, Phase::Setup); + } + 1 => { + // Player B commits -> transitions to Attack phase + c.commit_board(&p, &commitment_b, &root_b); + assert_eq!(c.get_state().turn_state.phase, Phase::Attack); + } + 2 => { + // Player A attacks cell (0, 1) (index 10) + c.attack(&p, &0, &1); + let state = c.get_state(); + assert!(state.turn_state.has_pending); + assert_eq!(state.turn_state.pending_reveal_x, 0); + assert_eq!(state.turn_state.pending_reveal_y, 1); + } + 3 => { + // Player B reveals cell (0, 1) + let proof = to_on_chain_proof(&tree_b.proof(10).unwrap(), h.env()); + c.reveal_cell(&p, &0, &1, &1, &proof); + let state = c.get_state(); + assert!(!state.turn_state.has_pending); + // Check hit recorded + let cell = state.attack_grid_b.cells.get(10).unwrap(); + assert_eq!(cell, crate::CellResult::Hit); + } + _ => {} + } + }); +} diff --git a/examples/blind_auction/.gitignore b/examples/blind_auction/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/blind_auction/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/blind_auction/Cargo.toml b/examples/blind_auction/Cargo.toml index 4f8f0318..75d2ec8d 100644 --- a/examples/blind_auction/Cargo.toml +++ b/examples/blind_auction/Cargo.toml @@ -11,11 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } -cougr-core = { features = ["testutils"], path = "../../" } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/blind_auction/README.md b/examples/blind_auction/README.md new file mode 100644 index 00000000..83c77104 --- /dev/null +++ b/examples/blind_auction/README.md @@ -0,0 +1,66 @@ +# 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 + +1. **Setup**: Call `init_auction` to setup maximum bid constraints and the auction ID. +2. **Commit Phase**: Bidders record hash commitments of their bids (handled off-chain or via standard storage). +3. **Reveal Phase**: Bidders call `reveal_bid` with their bids and ZK proofs to unlock and register their bid weights. + +## Cougr APIs used + +- `circuits::sealed_bid`: Handles verify calculations for sealed on-chain bidding logic. +- `zk::Groth16Proof`: Contains the proof struct representation. + +## Recommended testing approach + +Use `GameHarness` and standard `test_fixtures` to execute happy-path reveals and invalid proof rejection flows. + +## Build and test commands + +```bash +cargo test +stellar contract build +``` + +## Known limitations + +- Simple single-item auction model. +- Winner computation logic is not included (focuses on verification of bid validity). diff --git a/examples/blind_auction/src/lib.rs b/examples/blind_auction/src/lib.rs index 82058b24..458e749c 100644 --- a/examples/blind_auction/src/lib.rs +++ b/examples/blind_auction/src/lib.rs @@ -4,7 +4,9 @@ use cougr_core::circuits::sealed_bid; use cougr_core::zk::Groth16Proof; -use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol}; +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol, +}; #[contracttype] #[derive(Clone, Debug)] @@ -87,4 +89,4 @@ fn bid_key(_env: &Env, bidder: &Address) -> (Symbol, Address) { } #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; diff --git a/examples/blind_auction/src/tests.rs b/examples/blind_auction/src/tests.rs index 2a655c31..3244849a 100644 --- a/examples/blind_auction/src/tests.rs +++ b/examples/blind_auction/src/tests.rs @@ -36,4 +36,21 @@ fn reveal_bid_accepts_pipeline_proof() { assert!(client.reveal_bid(&bidder, &commit, &50, &proof)); let reveal = client.bid_reveal(&bidder); assert_eq!(reveal.revealed_bid, 50); -} \ No newline at end of file +} + +#[test] +fn reveal_bid_rejects_over_max_bid() { + let env = Env::default(); + env.mock_all_auths(); + let harness = GameHarness::new(env, BlindAuction); + let client = BlindAuctionClient::new(harness.env(), harness.contract_id()); + let bidder = Address::generate(harness.env()); + let public = test_fixtures::pipeline_public_inputs(harness.env(), CircuitId::SealedBid); + let proof = test_fixtures::pipeline_proof(harness.env(), CircuitId::SealedBid); + + let auction_id = BytesN::from_array(harness.env(), &public[0].bytes.to_array()); + let commit = BytesN::from_array(harness.env(), &public[1].bytes.to_array()); + client.init_auction(&1000, &auction_id); + + assert!(!client.reveal_bid(&bidder, &commit, &1001, &proof)); +} diff --git a/examples/bomberman/.gitignore b/examples/bomberman/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/bomberman/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/bomberman/Cargo.toml b/examples/bomberman/Cargo.toml index 2504ecda..a9c2fba2 100644 --- a/examples/bomberman/Cargo.toml +++ b/examples/bomberman/Cargo.toml @@ -1,4 +1,5 @@ [package] +description = "Grid action game with tile updates and timed hazards on Stellar Soroban" name = "bomberman" version = "0.0.1" edition = "2021" diff --git a/examples/bomberman/README.md b/examples/bomberman/README.md index f8d8a3ad..df943842 100644 --- a/examples/bomberman/README.md +++ b/examples/bomberman/README.md @@ -141,12 +141,11 @@ This example demonstrates how cougr-core simplifies on-chain game development by stellar contract deploy \ --wasm target/wasm32v1-none/release/bomberman.wasm \ --source \ - --network testnet + --network ``` -3. the contract ID for future invocations (e.g., `CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE`) +3. the contract ID for future invocations (e.g., ``) -### this the deployed testnet link https://stellar.expert/explorer/testnet/account/GAQAXKUQYNBHZYZ2OYISPXDZDP2HV57534VMGARGGIICH2BGNKDTKXOX ### Testing the Contract @@ -158,7 +157,7 @@ Invoke functions to test gameplay: stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- \ init_game @@ -166,7 +165,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- \ move_player \ --player_id 1 \ @@ -176,7 +175,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- \ place_bomb \ --player_id 1 @@ -185,7 +184,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- \ update_tick @@ -193,7 +192,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- \ get_score \ --player_id 1 @@ -202,7 +201,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source \ - --network testnet \ + --network \ -- \ check_game_over ``` diff --git a/examples/checkers/.gitignore b/examples/checkers/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/checkers/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/checkers/README.md b/examples/checkers/README.md index 0aa88c04..0e94a8f1 100644 --- a/examples/checkers/README.md +++ b/examples/checkers/README.md @@ -181,13 +181,13 @@ target/wasm32v1-none/release/checkers.wasm ```bash # Generate or reuse an identity -stellar keys generate --global alice --network testnet +stellar keys generate --global alice --network # Deploy stellar contract deploy \ --wasm target/wasm32v1-none/release/checkers.wasm \ --source alice \ - --network testnet \ + --network \ --alias checkers_contract ``` @@ -198,7 +198,7 @@ stellar contract deploy \ stellar contract invoke \ --id checkers_contract \ --source alice \ - --network testnet \ + --network \ -- init_game \ --player_one \ --player_two @@ -207,7 +207,7 @@ stellar contract invoke \ stellar contract invoke \ --id checkers_contract \ --source alice \ - --network testnet \ + --network \ -- submit_move \ --player \ --from_row 2 --from_col 1 \ @@ -216,7 +216,7 @@ stellar contract invoke \ # Read the current board stellar contract invoke \ --id checkers_contract \ - --network testnet \ + --network \ -- get_board ``` diff --git a/examples/chess/.gitignore b/examples/chess/.gitignore index 1b6c0db6..83e8137e 100644 --- a/examples/chess/.gitignore +++ b/examples/chess/.gitignore @@ -1,6 +1,5 @@ # Rust target/ -Cargo.lock **/*.rs.bk *.pdb diff --git a/examples/chess/README.md b/examples/chess/README.md index bbf0a887..dfe06cd6 100644 --- a/examples/chess/README.md +++ b/examples/chess/README.md @@ -268,7 +268,7 @@ These can be added by extending the circuit without changing the contract. ```bash # Generate funded account -stellar keys generate chess-deployer --network testnet --fund +stellar keys generate chess-deployer --network --fund # Build contract stellar contract build @@ -277,7 +277,7 @@ stellar contract build stellar contract deploy \ --wasm target/wasm32v1-none/release/chess.wasm \ --source chess-deployer \ - --network testnet + --network ``` ### Set Verification Key @@ -288,7 +288,7 @@ stellar contract deploy \ stellar contract invoke \ --id \ - --network testnet \ + --network \ -- set_vk \ --vk ``` @@ -299,7 +299,7 @@ stellar contract invoke \ # Initialize game stellar contract invoke \ --id \ - --network testnet \ + --network \ -- new_game \ --white \ --black @@ -307,7 +307,7 @@ stellar contract invoke \ # Submit move (white's turn) stellar contract invoke \ --id \ - --network testnet \ + --network \ --source white-player \ -- submit_move \ --player \ @@ -318,7 +318,7 @@ stellar contract invoke \ # Get game state stellar contract invoke \ --id \ - --network testnet \ + --network \ -- get_state ``` diff --git a/examples/connect_four/.gitignore b/examples/connect_four/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/connect_four/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/connect_four/README.md b/examples/connect_four/README.md index b8de35c3..b8f1093d 100644 --- a/examples/connect_four/README.md +++ b/examples/connect_four/README.md @@ -4,9 +4,6 @@ A fully functional Connect Four game implemented as a Soroban smart contract on | | | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| **Contract ID** | `TBD` (Deploy to testnet) | -| **Network** | Stellar Testnet | -| **Explorer** | [View on Stellar Expert](https://stellar.expert/explorer/testnet) (Deploy contract to view) | ## Why Cougr-Core? @@ -248,7 +245,7 @@ Player 1 wins with vertical line in column 3: ```bash # Generate funded account -stellar keys generate deployer --network testnet --fund +stellar keys generate deployer --network --fund # Build contract stellar contract build @@ -257,7 +254,7 @@ stellar contract build stellar contract deploy \ --wasm target/wasm32v1-none/release/connect_four.wasm \ --source deployer \ - --network testnet + --network ``` ### Interact with Deployed Contract @@ -266,7 +263,7 @@ stellar contract deploy \ # Initialize a game stellar contract invoke \ --id \ - --network testnet \ + --network \ -- init_game \ --player_one \ --player_two @@ -274,7 +271,7 @@ stellar contract invoke \ # Drop a piece in column 3 stellar contract invoke \ --id \ - --network testnet \ + --network \ -- drop_piece \ --player \ --column 3 @@ -282,13 +279,13 @@ stellar contract invoke \ # Get game state stellar contract invoke \ --id \ - --network testnet \ + --network \ -- get_state # Check if game is finished stellar contract invoke \ --id \ - --network testnet \ + --network \ -- is_finished ``` diff --git a/examples/cross_asset_racing_league/.gitignore b/examples/cross_asset_racing_league/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/cross_asset_racing_league/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/cross_asset_racing_league/Cargo.toml b/examples/cross_asset_racing_league/Cargo.toml index d357f27c..a0c51757 100644 --- a/examples/cross_asset_racing_league/Cargo.toml +++ b/examples/cross_asset_racing_league/Cargo.toml @@ -2,7 +2,7 @@ name = "cross_asset_racing_league" version = "0.1.0" edition = "2021" -description = "Cross-asset racing league example with payment-driven gameplay and stellar-zk anti-cheat" +description = "Cross-asset racing league with payment-driven gameplay and stellar-zk anti-cheat" license = "MIT OR Apache-2.0" [lib] diff --git a/examples/dice_duel/.gitignore b/examples/dice_duel/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/dice_duel/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/dice_duel/Cargo.toml b/examples/dice_duel/Cargo.toml index 36dcc29f..54976f52 100644 --- a/examples/dice_duel/Cargo.toml +++ b/examples/dice_duel/Cargo.toml @@ -11,11 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } -cougr-core = { path = "../../", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/dice_duel/README.md b/examples/dice_duel/README.md new file mode 100644 index 00000000..e85fb350 --- /dev/null +++ b/examples/dice_duel/README.md @@ -0,0 +1,66 @@ +# 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 + +1. **Setup**: Call `init_duel` to bind the dice properties and seed commitment. +2. **Roll**: Players roll dice off-chain and calculate proof. +3. **Submit**: Players call `submit_roll` to verify and record the roll on-chain. + +## Cougr APIs used + +- `circuits::fair_dice`: Implements the dice roll verification boundary specifications. +- `zk::Groth16Proof`: Holds proof payload. + +## 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 + +```bash +cargo test +stellar contract build +``` + +## Known limitations + +- Simple single-roll mechanics. +- The seed is assumed to be cryptographically secure and generated off-chain. diff --git a/examples/dice_duel/src/lib.rs b/examples/dice_duel/src/lib.rs index 5fdb1875..a052a16b 100644 --- a/examples/dice_duel/src/lib.rs +++ b/examples/dice_duel/src/lib.rs @@ -4,7 +4,9 @@ use cougr_core::circuits::fair_dice; use cougr_core::zk::Groth16Proof; -use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol}; +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol, +}; #[contracttype] #[derive(Clone, Debug)] @@ -83,4 +85,4 @@ fn roll_key(_env: &Env, player: &Address) -> (Symbol, Address) { } #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; diff --git a/examples/dice_duel/src/tests.rs b/examples/dice_duel/src/tests.rs index 169e3417..56d49b1e 100644 --- a/examples/dice_duel/src/tests.rs +++ b/examples/dice_duel/src/tests.rs @@ -59,4 +59,18 @@ fn scenario_two_player_rolls() { assert!(c.submit_roll(roller, &6, &5, &proof)); } }); -} \ No newline at end of file +} + +#[test] +fn submit_roll_rejects_out_of_range_result() { + let env = Env::default(); + env.mock_all_auths(); + let harness = GameHarness::new(env, DiceDuel); + let client = DiceDuelClient::new(harness.env(), harness.contract_id()); + let player = Address::generate(harness.env()); + let seed = test_fixtures::pipeline_public_bytes32(harness.env(), CircuitId::FairDice); + let proof = test_fixtures::pipeline_proof(harness.env(), CircuitId::FairDice); + + client.init_duel(&6, &seed); + assert!(!client.submit_roll(&player, &7, &5, &proof)); +} diff --git a/examples/flappy_bird/.gitignore b/examples/flappy_bird/.gitignore index 1b6c0db6..83e8137e 100644 --- a/examples/flappy_bird/.gitignore +++ b/examples/flappy_bird/.gitignore @@ -1,6 +1,5 @@ # Rust target/ -Cargo.lock **/*.rs.bk *.pdb diff --git a/examples/flappy_bird/README.md b/examples/flappy_bird/README.md index 66b8c72d..74319764 100644 --- a/examples/flappy_bird/README.md +++ b/examples/flappy_bird/README.md @@ -135,7 +135,7 @@ The tests cover: Generate a new keypair: ```bash -stellar keys generate test-account --network testnet +stellar keys generate test-account --network ``` Get the account address: @@ -156,7 +156,7 @@ curl "https://friendbot.stellar.org?addr=$(stellar keys address test-account)" stellar contract deploy \ --wasm target/wasm32v1-none/release/flappy_bird.wasm \ --source test-account \ - --network testnet + --network ``` Save the returned CONTRACT_ID for later use. @@ -169,7 +169,7 @@ Save the returned CONTRACT_ID for later use. stellar contract invoke \ --id \ --source test-account \ - --network testnet \ + --network \ -- init_game ``` @@ -179,7 +179,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source test-account \ - --network testnet \ + --network \ -- flap ``` @@ -189,7 +189,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source test-account \ - --network testnet \ + --network \ -- update_tick ``` @@ -199,7 +199,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source test-account \ - --network testnet \ + --network \ -- get_score ``` @@ -209,7 +209,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source test-account \ - --network testnet \ + --network \ -- check_game_over ``` @@ -219,7 +219,7 @@ stellar contract invoke \ stellar contract invoke \ --id \ --source test-account \ - --network testnet \ + --network \ -- get_bird_pos ``` diff --git a/examples/fog_explorer/.gitignore b/examples/fog_explorer/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/fog_explorer/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/fog_explorer/Cargo.toml b/examples/fog_explorer/Cargo.toml index e7819d01..0c57b5ae 100644 --- a/examples/fog_explorer/Cargo.toml +++ b/examples/fog_explorer/Cargo.toml @@ -11,11 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } -cougr-core = { path = "../../", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/fog_explorer/README.md b/examples/fog_explorer/README.md new file mode 100644 index 00000000..f331fe3e --- /dev/null +++ b/examples/fog_explorer/README.md @@ -0,0 +1,68 @@ +# 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 + +1. **Map Config**: Call `init_map` to configure map dimensions and visibility boundaries. +2. **Registration**: Explorer calls `register_explorer` to record their initial map state. +3. **Exploration**: Explorer calls `explore` with a Groth16 proof to update their visible tiles and progress. + +## Cougr APIs used + +- `circuits::fog_of_war`: Configures and handles verification of private line-of-sight map calculations. +- `zk::experimental::{FogOfWarSnapshot, FogOfWarTransition}`: Input structs for ZK state transition logic. +- `zk::Groth16Proof`: Cryptographic proof data container. + +## 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 + +```bash +cargo test +stellar contract build +``` + +## Known limitations + +- Grid coordinates are simplified. +- Map generation and parsing are managed off-chain. diff --git a/examples/fog_explorer/src/lib.rs b/examples/fog_explorer/src/lib.rs index 048048a6..9ff21421 100644 --- a/examples/fog_explorer/src/lib.rs +++ b/examples/fog_explorer/src/lib.rs @@ -5,7 +5,9 @@ use cougr_core::circuits::fog_of_war; use cougr_core::zk::experimental::{FogOfWarSnapshot, FogOfWarTransition}; use cougr_core::zk::Groth16Proof; -use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol}; +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol, +}; #[contracttype] #[derive(Clone, Debug)] @@ -40,13 +42,19 @@ impl FogExplorer { config } - pub fn register_explorer(env: Env, player: Address, explored_root: BytesN<32>) -> ExplorerState { + pub fn register_explorer( + env: Env, + player: Address, + explored_root: BytesN<32>, + ) -> ExplorerState { let state = ExplorerState { player: player.clone(), explored_root, reveals: 0, }; - env.storage().instance().set(&explorer_key(&env, &player), &state); + env.storage() + .instance() + .set(&explorer_key(&env, &player), &state); state } @@ -67,13 +75,8 @@ impl FogExplorer { .instance() .get(&map_key(&env)) .expect("map not initialized"); - let spec = fog_of_war( - &env, - config.width, - config.height, - config.visibility_radius, - ) - .expect("circuit spec"); + let spec = fog_of_war(&env, config.width, config.height, config.visibility_radius) + .expect("circuit spec"); let snapshot = FogOfWarSnapshot { map_root, @@ -124,4 +127,4 @@ fn explorer_key(_env: &Env, player: &Address) -> (Symbol, Address) { } #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; diff --git a/examples/fog_explorer/src/tests.rs b/examples/fog_explorer/src/tests.rs index 6866f036..3f44e1c9 100644 --- a/examples/fog_explorer/src/tests.rs +++ b/examples/fog_explorer/src/tests.rs @@ -35,15 +35,26 @@ fn explore_accepts_pipeline_proof() { let map_root = BytesN::from_array(harness.env(), &public[0].bytes.to_array()); let next = BytesN::from_array(harness.env(), &public[2].bytes.to_array()); - assert!(client.explore( - &player, - &map_root, - &prior, - &next, - &0, - &0, - &1, - &2, - &proof, + assert!(client.explore(&player, &map_root, &prior, &next, &0, &0, &1, &2, &proof,)); +} + +#[test] +fn explore_rejects_mismatched_map_root() { + let env = Env::default(); + env.mock_all_auths(); + let harness = GameHarness::new(env, FogExplorer); + let client = FogExplorerClient::new(harness.env(), harness.contract_id()); + let player = Address::generate(harness.env()); + let public = test_fixtures::pipeline_public_inputs(harness.env(), CircuitId::FogOfWar); + let proof = test_fixtures::pipeline_proof(harness.env(), CircuitId::FogOfWar); + + client.init_map(&32, &32, &3); + let prior = BytesN::from_array(harness.env(), &public[1].bytes.to_array()); + client.register_explorer(&player, &prior); + + let bad_root = BytesN::from_array(harness.env(), &[0u8; 32]); + let next = BytesN::from_array(harness.env(), &public[2].bytes.to_array()); + assert!(!client.explore( + &player, &bad_root, &prior, &next, &0, &0, &1, &2, &proof, )); -} \ No newline at end of file +} diff --git a/examples/geometry_dash/.gitignore b/examples/geometry_dash/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/geometry_dash/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/guild_arena/.gitignore b/examples/guild_arena/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/guild_arena/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/guild_treasury_wars/.gitignore b/examples/guild_treasury_wars/.gitignore index 96ef6c0b..0ac7c3b2 100644 --- a/examples/guild_treasury_wars/.gitignore +++ b/examples/guild_treasury_wars/.gitignore @@ -1,2 +1,4 @@ /target -Cargo.lock + +target/ +*.wasm diff --git a/examples/guild_treasury_wars/Cargo.toml b/examples/guild_treasury_wars/Cargo.toml index 35156094..a54bd8c8 100644 --- a/examples/guild_treasury_wars/Cargo.toml +++ b/examples/guild_treasury_wars/Cargo.toml @@ -2,7 +2,7 @@ name = "guild_treasury_wars" version = "0.1.0" edition = "2021" -description = "Guild Treasury Wars — DAO-governed factions with stellar-zk commitments using cougr-core" +description = "Guild Treasury Wars with DAO-governed factions and stellar-zk commitments" license = "MIT OR Apache-2.0" [lib] diff --git a/examples/guild_treasury_wars/README.md b/examples/guild_treasury_wars/README.md index f89615c9..d451141e 100644 --- a/examples/guild_treasury_wars/README.md +++ b/examples/guild_treasury_wars/README.md @@ -1,4 +1,4 @@ -# ⚔️ Guild Treasury Wars — DAO-Governed Factions with stellar-zk Commitments +# ️ Guild Treasury Wars — DAO-Governed Factions with stellar-zk Commitments [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/salazarsebas/Cougr) [![Tests](https://img.shields.io/badge/tests-14%20passing-brightgreen)](https://github.com/salazarsebas/Cougr) @@ -6,7 +6,7 @@ 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? +## 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. @@ -19,7 +19,7 @@ In strategy games, revealing your plans to the enemy is a losing move. **stellar --- -## 🎮 Gameplay Flow +## Gameplay Flow ``` ┌─────────────────────────────────────────────────────────┐ @@ -56,7 +56,7 @@ In strategy games, revealing your plans to the enemy is a losing move. **stellar --- -## 🔐 stellar-zk Integration +## 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: @@ -92,7 +92,7 @@ Each commitment hash acts as a **nullifier** (following stellar-zk's verifier co --- -## 🔧 Cougr-Core Integration +## Cougr-Core Integration ### ECS Components @@ -114,7 +114,7 @@ let _guild_entity = world.spawn_entity(); --- -## 📖 Contract API +## Contract API ### Guild Management @@ -149,7 +149,7 @@ let _guild_entity = world.spawn_entity(); --- -## 🏗️ Quick Start +## ️ Quick Start ### Prerequisites @@ -206,7 +206,7 @@ stellar contract build --- -## 📁 Project Structure +## Project Structure ``` examples/guild_treasury_wars/ @@ -222,6 +222,6 @@ examples/guild_treasury_wars/ --- -## 📄 License +## License MIT OR Apache-2.0 diff --git a/examples/hidden_hand/.gitignore b/examples/hidden_hand/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/hidden_hand/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/hidden_hand/Cargo.toml b/examples/hidden_hand/Cargo.toml index a3b980ed..6dfe2c04 100644 --- a/examples/hidden_hand/Cargo.toml +++ b/examples/hidden_hand/Cargo.toml @@ -11,11 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } -cougr-core = { path = "../../", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/hidden_hand/README.md b/examples/hidden_hand/README.md new file mode 100644 index 00000000..6244160f --- /dev/null +++ b/examples/hidden_hand/README.md @@ -0,0 +1,65 @@ +# 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 + +1. **Setup**: The contract calls `init_table(deck_size, hand_size)` to prepare the ZK circuit parameters. +2. **Dealing**: The dealer generates a hand commitment and a proof off-chain. +3. **Verification**: The contract calls `verify_deal(player, deck_root, hand_commitment, proof)` to validate that the deal is correct before proceeding with gameplay. + +## Cougr APIs used + +- `circuits::hidden_cards`: Fetches the circuit layout, verification key, and verify wrapper for Groth16 hidden cards. +- `zk::Groth16Proof`: Data structure representing the cryptographic proof. + +## 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 + +```bash +cargo test +stellar contract build +``` + +## Known limitations + +- Simple single-deck configuration. +- Proving is done entirely off-chain. diff --git a/examples/hidden_hand/src/lib.rs b/examples/hidden_hand/src/lib.rs index 41b63a14..3174a7a7 100644 --- a/examples/hidden_hand/src/lib.rs +++ b/examples/hidden_hand/src/lib.rs @@ -6,7 +6,9 @@ use cougr_core::circuits::hidden_cards; use cougr_core::zk::Groth16Proof; -use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol}; +use soroban_sdk::{ + contract, contractimpl, contracttype, symbol_short, Address, BytesN, Env, Symbol, +}; #[contracttype] #[derive(Clone, Debug)] @@ -46,10 +48,15 @@ impl HiddenHand { .get(&table_key(&env)) .expect("table not initialized"); let spec = hidden_cards(&env, config.deck_size, config.hand_size).expect("circuit spec"); - spec.verify_hidden_hand(&env, &proof, &deck_root, &hand_commitment, player_id(&player)) - .unwrap_or(false) + spec.verify_hidden_hand( + &env, + &proof, + &deck_root, + &hand_commitment, + player_id(&player), + ) + .unwrap_or(false) } - } fn table_key(_env: &Env) -> Symbol { @@ -66,4 +73,4 @@ fn player_id(player: &Address) -> u32 { } #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; diff --git a/examples/hidden_hand/src/tests.rs b/examples/hidden_hand/src/tests.rs index 1c069bc7..549ec845 100644 --- a/examples/hidden_hand/src/tests.rs +++ b/examples/hidden_hand/src/tests.rs @@ -3,6 +3,15 @@ use cougr_core::circuits::{hidden_cards, test_fixtures, CircuitId}; use cougr_core::test::GameHarness; use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; +fn player_id_for_test(player: &Address) -> u32 { + let bytes = player.to_string().to_bytes(); + let mut id = 0u32; + for i in 0..bytes.len().min(4) { + id = id.wrapping_add(u32::from(bytes.get(i).unwrap_or(0))); + } + id +} + #[test] fn init_table_configures_standard_deck() { let env = Env::default(); @@ -34,11 +43,33 @@ fn verify_deal_accepts_pipeline_proof() { let deck = BytesN::from_array(harness.env(), &public[0].bytes.to_array()); let hand = BytesN::from_array(harness.env(), &public[1].bytes.to_array()); - // Pipeline fixture uses player_id=2; verify through the same spec the contract uses. let spec = hidden_cards(harness.env(), 52, 5).unwrap(); assert_eq!( spec.verify_hidden_hand(harness.env(), &proof, &deck, &hand, 2), Ok(true) ); - let _ = client.verify_deal(&player, &deck, &hand, &proof); -} \ No newline at end of file + let player_id = player_id_for_test(&player); + assert_eq!( + client.verify_deal(&player, &deck, &hand, &proof), + spec + .verify_hidden_hand(harness.env(), &proof, &deck, &hand, player_id) + .unwrap_or(false) + ); +} + +#[test] +fn verify_deal_rejects_mismatched_hand_commitment() { + let env = Env::default(); + env.mock_all_auths(); + let harness = GameHarness::new(env, HiddenHand); + let player = Address::generate(harness.env()); + let client = HiddenHandClient::new(harness.env(), harness.contract_id()); + let public = test_fixtures::pipeline_public_inputs(harness.env(), CircuitId::HiddenCards); + let proof = test_fixtures::pipeline_proof(harness.env(), CircuitId::HiddenCards); + + client.init_table(&52, &5); + let deck = BytesN::from_array(harness.env(), &public[0].bytes.to_array()); + let bad_hand = BytesN::from_array(harness.env(), &[0u8; 32]); + + assert!(!client.verify_deal(&player, &deck, &bad_hand, &proof)); +} diff --git a/examples/memory_match/.gitignore b/examples/memory_match/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/memory_match/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/minesweeper/.gitignore b/examples/minesweeper/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/minesweeper/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/minesweeper/README.md b/examples/minesweeper/README.md index ede88f18..b385e0db 100644 --- a/examples/minesweeper/README.md +++ b/examples/minesweeper/README.md @@ -4,9 +4,6 @@ A fully functional Minesweeper game implemented as a Soroban smart contract on t | | | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| **Contract ID** | `TBD` (Deploy to testnet) | -| **Network** | Stellar Testnet | -| **Explorer** | [View on Stellar Expert](https://stellar.expert/explorer/testnet) (Deploy contract to view) | ## Why Cougr-Core? @@ -265,7 +262,7 @@ Status: Lost ```bash # Generate funded account -stellar keys generate deployer --network testnet --fund +stellar keys generate deployer --network --fund # Build contract stellar contract build @@ -274,7 +271,7 @@ stellar contract build stellar contract deploy \ --wasm target/wasm32v1-none/release/minesweeper.wasm \ --source deployer \ - --network testnet + --network ``` ### Interact with Deployed Contract @@ -283,13 +280,13 @@ stellar contract deploy \ # Initialize a game stellar contract invoke \ --id \ - --network testnet \ + --network \ -- init_game # Reveal cell at row 0, col 0 stellar contract invoke \ --id \ - --network testnet \ + --network \ -- reveal_cell \ --row 0 \ --col 0 @@ -297,13 +294,13 @@ stellar contract invoke \ # Get game state stellar contract invoke \ --id \ - --network testnet \ + --network \ -- get_state # Check if game is finished stellar contract invoke \ --id \ - --network testnet \ + --network \ -- is_finished ``` diff --git a/examples/murdoku/.gitignore b/examples/murdoku/.gitignore index ea8c4bf7..0ac7c3b2 100644 --- a/examples/murdoku/.gitignore +++ b/examples/murdoku/.gitignore @@ -1 +1,4 @@ /target + +target/ +*.wasm diff --git a/examples/murdoku/Cargo.toml b/examples/murdoku/Cargo.toml index 6715b526..da15ce36 100644 --- a/examples/murdoku/Cargo.toml +++ b/examples/murdoku/Cargo.toml @@ -31,3 +31,5 @@ panic = "abort" [profile.release-with-logs] inherits = "release" debug-assertions = true + +[workspace] diff --git a/examples/murdoku/README.md b/examples/murdoku/README.md index 9b66fd9d..fc1e7fa3 100644 --- a/examples/murdoku/README.md +++ b/examples/murdoku/README.md @@ -184,18 +184,18 @@ To deploy and test the contract manually on Stellar Testnet, use the following c stellar keys generate murdoku_deployer # 2. Fund the identity using Friendbot -stellar keys fund murdoku_deployer --network testnet +stellar keys fund murdoku_deployer --network # 3. Deploy the compiled WASM to Testnet CONTRACT_ID=$(stellar contract deploy \ --wasm target/wasm32v1-none/release/murdoku.wasm \ - --network testnet \ + --network \ --source murdoku_deployer) # 4. Initialize the contract game registry stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ --source murdoku_deployer \ -- init_game \ --admin murdoku_deployer @@ -204,7 +204,7 @@ stellar contract invoke \ # (Solution grid contains flat representation of a 4x4 Latin square) stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ --source murdoku_deployer \ -- submit_puzzle \ --creator murdoku_deployer \ @@ -222,9 +222,9 @@ stellar contract invoke \ The Groth16 circuit proves the following statement without revealing the solution: > "I know a set of values `cells` such that: -> (1) `cells` is a valid Latin square of size N, -> (2) Poseidon2(cells || salt) == commitment, -> (3) each cell value is in range 1..=N." +> (1) `cells` is a valid Latin square of size N, +> (2) Poseidon2(cells || salt) == commitment, +> (3) each cell value is in range 1..=N." ### Public Inputs Format diff --git a/examples/pac_man/.gitignore b/examples/pac_man/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/pac_man/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/pac_man/README.md b/examples/pac_man/README.md index 88856b38..91c3c664 100644 --- a/examples/pac_man/README.md +++ b/examples/pac_man/README.md @@ -70,7 +70,7 @@ The game uses a fixed 10x10 maze: ########## Legend: - # = Wall + # = Wall . = Pellet (10 points) P = Power Pellet (50 points) ``` @@ -318,13 +318,13 @@ pub fn update_tick(env: Env) -> GameState { ### 1. Generate a Stellar Identity ```bash -stellar keys generate --global pacman-deployer --network testnet +stellar keys generate --global pacman-deployer --network ``` ### 2. Fund the Account ```bash -stellar keys fund pacman-deployer --network testnet +stellar keys fund pacman-deployer --network ``` Or use Friendbot: @@ -338,10 +338,10 @@ curl "https://friendbot.stellar.org?addr=$(stellar keys address pacman-deployer) stellar contract deploy \ --wasm target/wasm32v1-none/release/pac_man.wasm \ --source pacman-deployer \ - --network testnet + --network ``` -Save the returned contract ID (e.g., `CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC`). +Save the returned contract ID (e.g., ``). ### 4. Initialize the Game @@ -349,7 +349,7 @@ Save the returned contract ID (e.g., `CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2 stellar contract invoke \ --id \ --source pacman-deployer \ - --network testnet \ + --network \ -- \ init_game ``` @@ -361,7 +361,7 @@ Change direction: stellar contract invoke \ --id \ --source pacman-deployer \ - --network testnet \ + --network \ -- \ change_direction \ --direction 2 # 0=Up, 1=Down, 2=Left, 3=Right @@ -372,7 +372,7 @@ Update game state: stellar contract invoke \ --id \ --source pacman-deployer \ - --network testnet \ + --network \ -- \ update_tick ``` @@ -383,7 +383,7 @@ Get score: ```bash stellar contract invoke \ --id \ - --network testnet \ + --network \ -- \ get_score ``` @@ -392,7 +392,7 @@ Get full state: ```bash stellar contract invoke \ --id \ - --network testnet \ + --network \ -- \ get_game_state ``` @@ -401,7 +401,7 @@ Check if game is over: ```bash stellar contract invoke \ --id \ - --network testnet \ + --network \ -- \ check_game_over ``` @@ -460,7 +460,7 @@ cargo install stellar-cli --locked stellar contract invoke \ --id \ --source pacman-deployer \ - --network testnet \ + --network \ --sim \ -- \ init_game @@ -496,10 +496,6 @@ Collect all pellets (regular and power) to win. The contract has been successfully deployed to Stellar Testnet: -**Contract ID**: `CDWERKYRRWD5N6Q7RKCVWT7BNNS5ADRRTM2VCG45AYRE52ABP5NUBJ3C` - -**Explorer Link**: https://stellar.expert/explorer/testnet/contract/CDWERKYRRWD5N6Q7RKCVWT7BNNS5ADRRTM2VCG45AYRE52ABP5NUBJ3C - ### Verified Invocations 1. **init_game** - Successfully initialized with: diff --git a/examples/pokemon_mini/.gitignore b/examples/pokemon_mini/.gitignore index b55a529c..9644da5a 100644 --- a/examples/pokemon_mini/.gitignore +++ b/examples/pokemon_mini/.gitignore @@ -1,7 +1,6 @@ # Build artifacts /target/ **/*.rs.bk -Cargo.lock # Test snapshots (regenerated by soroban-sdk tests) test_snapshots/ @@ -15,3 +14,4 @@ test_snapshots/ # OS .DS_Store Thumbs.db +*.wasm diff --git a/examples/pokemon_mini/README.md b/examples/pokemon_mini/README.md index ab96aa81..ce2ab319 100644 --- a/examples/pokemon_mini/README.md +++ b/examples/pokemon_mini/README.md @@ -142,11 +142,9 @@ damage = max(1, attacker_atk - defender_def) ## Deployed Contract (Testnet) -> **Contract ID:** `CCFMAYEZL6762FEWVU5SMXP7SRAGOEOSXKBKORXORMBVLDNQ33666I52` - | Network | Status | Explorer | |---------|--------|----------| -| Stellar Testnet | ✅ Live | [View on Stellar Lab](https://stellar-explorer.acachete.xyz/contract/CCFMAYEZL6762FEWVU5SMXP7SRAGOEOSXKBKORXORMBVLDNQ33666I52) | +| Stellar Testnet | Live | [View on Stellar Lab](https://stellar-explorer.acachete.xyz/contract/) | --- @@ -156,7 +154,7 @@ damage = max(1, attacker_atk - defender_def) ```bash # Generate keypair -stellar keys generate --global alice --network testnet +stellar keys generate --global alice --network # Fund account # Visit: https://friendbot.stellar.org/?addr= @@ -165,26 +163,25 @@ stellar keys generate --global alice --network testnet stellar contract deploy \ --wasm target/wasm32v1-none/release/pokemon_mini.wasm \ --source alice \ - --network testnet + --network ``` ### Playing ```bash # Use the deployed contract -CONTRACT_ID="CCFMAYEZL6762FEWVU5SMXP7SRAGOEOSXKBKORXORMBVLDNQ33666I52" # Initialize player -stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- init_player +stellar contract invoke --id $CONTRACT_ID --source alice --network -- 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 +stellar contract invoke --id $CONTRACT_ID --source alice --network -- 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 +stellar contract invoke --id $CONTRACT_ID --source alice --network -- 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 +stellar contract invoke --id $CONTRACT_ID --source alice --network -- battle_action --action 0 ``` --- @@ -213,7 +210,6 @@ examples/pokemon_mini/ | Stellar CLI | [CLI Documentation](https://developers.stellar.org/docs/tools/cli) | | Cougr Repository | [github.com/salazarsebas/Cougr](https://github.com/salazarsebas/Cougr) | - --- ## License diff --git a/examples/pong/.gitignore b/examples/pong/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/pong/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/pong/README.md b/examples/pong/README.md index 6d995a4a..35a8a88b 100644 --- a/examples/pong/README.md +++ b/examples/pong/README.md @@ -160,7 +160,7 @@ pub struct GameState { stellar contract deploy \ --wasm target/wasm32v1-none/release/pong.wasm \ --source \ - --network testnet + --network ``` 3. **Save the contract ID** returned from the deployment. @@ -171,43 +171,37 @@ pub struct GameState { # Initialize a new game stellar contract invoke \ --id \ - --network testnet \ + --network \ -- init_game # Move Player 1's paddle up stellar contract invoke \ --id \ - --network testnet \ + --network \ -- move_paddle --player 1 --direction -1 # Update game tick stellar contract invoke \ --id \ - --network testnet \ + --network \ -- update_tick # Get current game state stellar contract invoke \ --id \ - --network testnet \ + --network \ -- get_game_state ``` ### Deployment Results -**✅ Successfully Deployed to Stellar Testnet** - -**Contract ID**: `CADGGDYDBRVRPPG27IZYZJTFUZ47IJFW3QQ5O67QDMZ7UV3VWCZHPYI3` - -**Explorer Link**: [View on Stellar Expert](https://stellar.expert/explorer/testnet/contract/CADGGDYDBRVRPPG27IZYZJTFUZ47IJFW3QQ5O67QDMZ7UV3VWCZHPYI3) - **Test Account**: `GA5VOXGSGDQBIY7W2UJ2GD23V3566NA7OF4YIL4QCFAVM3PGN7QQQHZA` **Test Invocations**: 1. **Initialize Game**: ```bash - stellar contract invoke --id CADGGDYDBRVRPPG27IZYZJTFUZ47IJFW3QQ5O67QDMZ7UV3VWCZHPYI3 --source pong-test --network testnet -- init_game + stellar contract invoke --id --source pong-test --network -- init_game ``` **Result**: ✅ Success ```json @@ -226,7 +220,7 @@ stellar contract invoke \ 2. **Move Paddle** (Player 1 up): ```bash - stellar contract invoke --id CADGGDYDBRVRPPG27IZYZJTFUZ47IJFW3QQ5O67QDMZ7UV3VWCZHPYI3 --source pong-test --network testnet -- move_paddle --player 1 --direction -1 + stellar contract invoke --id --source pong-test --network -- move_paddle --player 1 --direction -1 ``` **Result**: ✅ Success - Paddle moved from y=30 to y=28 ```json @@ -239,7 +233,7 @@ stellar contract invoke \ 3. **Update Tick** (Physics simulation): ```bash - stellar contract invoke --id CADGGDYDBRVRPPG27IZYZJTFUZ47IJFW3QQ5O67QDMZ7UV3VWCZHPYI3 --source pong-test --network testnet -- update_tick + stellar contract invoke --id --source pong-test --network -- update_tick ``` **Result**: ✅ Success - Ball moved from (50,30) to (51,31) ```json @@ -253,8 +247,8 @@ stellar contract invoke \ **Deployment Date**: January 23, 2026 **Transaction Hashes**: -- Deploy: `acd8c82bb0d7167fdd7b438af49dc78e47a90ed9fa682574d20e621aa01769a3` -- [View on Stellar Expert](https://stellar.expert/explorer/testnet/tx/acd8c82bb0d7167fdd7b438af49dc78e47a90ed9fa682574d20e621aa01769a3) +- Deploy: `` +- [View on Stellar Expert](https://stellar.expert/explorer/testnet/tx/) ## Architecture @@ -367,7 +361,7 @@ rustc --version # Should be 1.89.0 or newer **Network errors**: Use `--simulate` flag first to test without deploying: ```bash -stellar contract invoke --id --network testnet --simulate -- init_game +stellar contract invoke --id --network --simulate -- init_game ``` **Insufficient funds**: Get more test XLM from the [faucet](https://faucet-stellar.acachete.xyz) diff --git a/examples/proof_of_hunt/.gitignore b/examples/proof_of_hunt/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/proof_of_hunt/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/reversi/.gitignore b/examples/reversi/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/reversi/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/reversi/README.md b/examples/reversi/README.md index 0e4cdbf0..0e6593ba 100644 --- a/examples/reversi/README.md +++ b/examples/reversi/README.md @@ -188,47 +188,47 @@ After building the WASM (`stellar contract build`), deploy and play on Testnet: # Generate two player identities and fund them via Friendbot stellar keys generate reversi_black stellar keys generate reversi_white -stellar keys fund reversi_black --network testnet -stellar keys fund reversi_white --network testnet +stellar keys fund reversi_black --network +stellar keys fund reversi_white --network # Deploy the contract CONTRACT_ID=$(stellar contract deploy \ --wasm target/wasm32v1-none/release/reversi.wasm \ - --network testnet \ + --network \ --source reversi_black) # Initialise (reversi_black plays Black, reversi_white plays White) -stellar contract invoke --id $CONTRACT_ID --network testnet --source reversi_black \ +stellar contract invoke --id $CONTRACT_ID --network --source reversi_black \ -- init_game \ --player_one reversi_black \ --player_two reversi_white # Black places at row=3, col=2 (flips (3,3) horizontally) -stellar contract invoke --id $CONTRACT_ID --network testnet --source reversi_black \ +stellar contract invoke --id $CONTRACT_ID --network --source reversi_black \ -- submit_move \ --player reversi_black \ --row 3 --col 2 # Check the board after the move -stellar contract invoke --id $CONTRACT_ID --network testnet --source reversi_black \ +stellar contract invoke --id $CONTRACT_ID --network --source reversi_black \ -- get_board # → {"cells":[0,0,...,1,1,1,0,...,1,2,0,...],"width":8,"height":8} -# (3,2)=1 (3,3)=1 (3,4)=1 — three Black pieces in a row +# (3,2)=1 (3,3)=1 (3,4)=1 — three Black pieces in a row # Check whose turn it is -stellar contract invoke --id $CONTRACT_ID --network testnet --source reversi_black \ +stellar contract invoke --id $CONTRACT_ID --network --source reversi_black \ -- get_state # → {"current_player":2,"pass_count":0,"status":0} -# current_player=2 → White's turn, game active +# current_player=2 → White's turn, game active # Check the score -stellar contract invoke --id $CONTRACT_ID --network testnet --source reversi_black \ +stellar contract invoke --id $CONTRACT_ID --network --source reversi_black \ -- get_score # → {"black_count":4,"white_count":1,"winner":0} -# winner=0 → game ongoing +# winner=0 → game ongoing # White responds at row=2, col=3 -stellar contract invoke --id $CONTRACT_ID --network testnet --source reversi_white \ +stellar contract invoke --id $CONTRACT_ID --network --source reversi_white \ -- submit_move \ --player reversi_white \ --row 2 --col 3 diff --git a/examples/rock_paper_scissors/.gitignore b/examples/rock_paper_scissors/.gitignore index 1b6c0db6..83e8137e 100644 --- a/examples/rock_paper_scissors/.gitignore +++ b/examples/rock_paper_scissors/.gitignore @@ -1,6 +1,5 @@ # Rust target/ -Cargo.lock **/*.rs.bk *.pdb diff --git a/examples/rock_paper_scissors/README.md b/examples/rock_paper_scissors/README.md index 172473d7..cf5c01aa 100644 --- a/examples/rock_paper_scissors/README.md +++ b/examples/rock_paper_scissors/README.md @@ -229,18 +229,18 @@ let hash = poseidon2_hash(&env, ¶ms, &choice_u256, &salt_u256); ## Security Considerations -### ✅ Secure +### 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 +### ️ 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 +### Best Practices ```rust // ✅ Good: Random salt per round let salt = generate_random_bytes(32); @@ -278,7 +278,7 @@ All components implement `cougr_core::component::ComponentTrait` for type-safe s ### Deploy to Testnet ```bash # Generate funded account -stellar keys generate rps-deployer --network testnet --fund +stellar keys generate rps-deployer --network --fund # Build contract cargo build --release --target wasm32v1-none @@ -287,7 +287,7 @@ cargo build --release --target wasm32v1-none stellar contract deploy \ --wasm target/wasm32v1-none/release/rock_paper_scissors.wasm \ --source rps-deployer \ - --network testnet + --network ``` ### Play a Game @@ -297,7 +297,7 @@ CONTRACT_ID= # Initialize match stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ -- new_match \ --player_a \ --player_b \ @@ -306,7 +306,7 @@ stellar contract invoke \ # Player A commits (compute hash off-chain first) stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ --source player-a \ -- commit \ --player \ @@ -315,7 +315,7 @@ stellar contract invoke \ # Player B commits stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ --source player-b \ -- commit \ --player \ @@ -324,7 +324,7 @@ stellar contract invoke \ # Player A reveals stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ --source player-a \ -- reveal \ --player \ @@ -334,7 +334,7 @@ stellar contract invoke \ # Player B reveals stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ --source player-b \ -- reveal \ --player \ @@ -344,7 +344,7 @@ stellar contract invoke \ # Check results stellar contract invoke \ --id $CONTRACT_ID \ - --network testnet \ + --network \ -- get_score ``` diff --git a/examples/session_arena/.gitignore b/examples/session_arena/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/session_arena/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/session_arena/Cargo.toml b/examples/session_arena/Cargo.toml index d9d1861a..c039653c 100644 --- a/examples/session_arena/Cargo.toml +++ b/examples/session_arena/Cargo.toml @@ -11,11 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } -cougr-core = { path = "../../", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/session_arena/README.md b/examples/session_arena/README.md new file mode 100644 index 00000000..7c9db348 --- /dev/null +++ b/examples/session_arena/README.md @@ -0,0 +1,75 @@ +# session_arena + +**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 + +1. **Authorization**: Owner calls `approve_session` from their wallet to authorize a temporary key for the `tap` action. +2. **Gameplay**: Client signs and executes calls to `tap` using the session key, bypassing any ledger signature popups. +3. **Renewal / Fallback**: If the session expires, the client calls `renew_session` (wallet-prompted) or calls `fallback_tap` (which handles direct-auth fallback). + +## Cougr APIs used + +- `SessionManager`: Manages approval, status, execution verification, and direct-auth fallbacks. +- `SessionBuilder`: Scopes maximum operations and timestamps for the generated key. +- `SessionStorage`: Loads active session state from the environment. +- `impl_component!`: Declares the player's `Score` component. + +## 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 + +```bash +cargo test +stellar contract build +``` + +## Known limitations + +- Simple score counter used for demonstrating the session auth wrapper; no actual gameplay engine included. +- Minimal session storage configuration. diff --git a/examples/session_arena/src/lib.rs b/examples/session_arena/src/lib.rs index e73bbca9..ef6a77c2 100644 --- a/examples/session_arena/src/lib.rs +++ b/examples/session_arena/src/lib.rs @@ -8,8 +8,8 @@ use cougr_core::accounts::{ GameAction, ReplayProtection, SessionBuilder, SessionStorage, SignedIntent, }; -use cougr_core::session::{ActiveSession, SessionManager}; use cougr_core::impl_component; +use cougr_core::session::{ActiveSession, SessionManager}; use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, Symbol}; #[contracttype] @@ -27,7 +27,12 @@ pub struct SessionArena; #[contractimpl] impl SessionArena { /// One-time owner approval that creates a scoped session key. - pub fn approve_session(env: Env, owner: Address, max_taps: u32, expires_in: u64) -> ActiveSession { + pub fn approve_session( + env: Env, + owner: Address, + max_taps: u32, + expires_in: u64, + ) -> ActiveSession { owner.require_auth(); let scope = SessionBuilder::new(&env) .allow_action(symbol_short!("tap")) @@ -104,7 +109,8 @@ impl SessionArena { ReplayProtection::next_account_nonce(&env, &owner), env.ledger().timestamp().saturating_add(60), ); - SessionManager::fallback_execute(&env, &session_intent, &direct_intent).expect("fallback tap"); + SessionManager::fallback_execute(&env, &session_intent, &direct_intent) + .expect("fallback tap"); let key = (Symbol::new(&env, "score"), owner.clone()); let mut score: Score = env @@ -128,4 +134,4 @@ impl SessionArena { } #[cfg(test)] -mod tests; \ No newline at end of file +mod tests; diff --git a/examples/session_arena/src/tests.rs b/examples/session_arena/src/tests.rs index b95c6971..b38f56f1 100644 --- a/examples/session_arena/src/tests.rs +++ b/examples/session_arena/src/tests.rs @@ -1,6 +1,9 @@ use super::*; use cougr_core::test::{GameHarness, MockSession}; -use soroban_sdk::{testutils::{Address as _, Ledger as _}, Address, Env}; +use soroban_sdk::{ + testutils::{Address as _, Ledger as _}, + Address, Env, +}; #[test] fn approve_and_tap_without_reauth() { @@ -57,4 +60,4 @@ fn mock_session_helper_matches_manager_flow() { assert!(status.active); assert_eq!(status.remaining_operations, 3); }); -} \ No newline at end of file +} diff --git a/examples/shadow_draft_card_game/.gitignore b/examples/shadow_draft_card_game/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/shadow_draft_card_game/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/shadow_draft_card_game/Cargo.toml b/examples/shadow_draft_card_game/Cargo.toml index a797b30e..e8b594e6 100644 --- a/examples/shadow_draft_card_game/Cargo.toml +++ b/examples/shadow_draft_card_game/Cargo.toml @@ -2,7 +2,7 @@ name = "shadow-draft-card-game" version = "0.1.0" edition = "2021" -description = "Shadow Draft Card Game — hidden-hand draft gameplay with stellar-zk proof-backed card validation and DAO-governed format rules on Stellar Soroban" +description = "Shadow Draft card game with hidden-hand draft gameplay and stellar-zk card validation" license = "MIT OR Apache-2.0" [lib] diff --git a/examples/snake/Cargo.toml b/examples/snake/Cargo.toml index ad1b0b90..baeece7b 100644 --- a/examples/snake/Cargo.toml +++ b/examples/snake/Cargo.toml @@ -10,11 +10,12 @@ description = "Snake on-chain game example using cougr-core ECS framework" crate-type = ["cdylib", "rlib"] [dependencies] -cougr-core = "1.0.0" +cougr-core = "1.1.0" soroban-sdk = "25.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/snake/README.md b/examples/snake/README.md index 656a95a7..07efdf21 100644 --- a/examples/snake/README.md +++ b/examples/snake/README.md @@ -164,10 +164,13 @@ stellar contract build cargo test ``` +**Recommended Testing Approach:** +For comprehensive testing, use the `GameHarness` and `Scenario` APIs provided by `cougr-core`'s `testutils` feature (see [sandbox_tests.rs](src/sandbox_tests.rs)). This allows writing replayable multi-turn scenarios to verify movement trajectories, direction change validation, and tick updates. + **Expected Output:** ``` -running 30 tests -test result: ok. 30 passed; 0 failed; 0 ignored +running 31 tests +test result: ok. 31 passed; 0 failed; 0 ignored ``` ### 4. Lint @@ -224,7 +227,7 @@ cargo clippy -- -D warnings ```bash # 1. Generate keypair -stellar keys generate --global alice --network testnet +stellar keys generate --global alice --network stellar keys address alice # 2. Fund account (visit URL with your address) @@ -234,7 +237,7 @@ stellar keys address alice stellar contract deploy \ --wasm target/wasm32v1-none/release/snake.wasm \ --source alice \ - --network testnet + --network # Save the returned Contract ID! ``` @@ -245,23 +248,22 @@ stellar contract deploy \ CONTRACT_ID="" # Initialize -stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- init_game +stellar contract invoke --id $CONTRACT_ID --source alice --network -- init_game # Change direction (0=Up, 1=Down, 2=Left, 3=Right) -stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- change_direction --direction 0 +stellar contract invoke --id $CONTRACT_ID --source alice --network -- change_direction --direction 0 # Advance game -stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- update_tick +stellar contract invoke --id $CONTRACT_ID --source alice --network -- update_tick # Check score -stellar contract invoke --id $CONTRACT_ID --source alice --network testnet -- get_score +stellar contract invoke --id $CONTRACT_ID --source alice --network -- get_score ``` ### Deployed Contract | Network | Contract ID | Explorer | |---------|-------------|----------| -| Testnet | `CCMDAHIKL3K5YHBMFYMMP65F6NRTQXICQSJJ2AF7JG7RVRVWGZY2S5LJ` | [View on Stellar Expert](https://stellar.expert/explorer/testnet/contract/CCMDAHIKL3K5YHBMFYMMP65F6NRTQXICQSJJ2AF7JG7RVRVWGZY2S5LJ) | --- diff --git a/examples/snake/src/lib.rs b/examples/snake/src/lib.rs index b7b8b87b..3b3c173f 100644 --- a/examples/snake/src/lib.rs +++ b/examples/snake/src/lib.rs @@ -26,6 +26,8 @@ extern crate alloc; mod components; +#[cfg(test)] +mod sandbox_tests; mod systems; use alloc::rc::Rc; @@ -50,6 +52,7 @@ pub struct GameState { /// Snake game contract #[contract] +#[derive(Clone)] pub struct SnakeContract; #[contractimpl] diff --git a/examples/snake/src/sandbox_tests.rs b/examples/snake/src/sandbox_tests.rs new file mode 100644 index 00000000..934047eb --- /dev/null +++ b/examples/snake/src/sandbox_tests.rs @@ -0,0 +1,46 @@ +#![cfg(test)] + +use crate::{SnakeContract, SnakeContractClient}; +use cougr_core::test::{GameHarness, Scenario}; +use soroban_sdk::Env; + +#[test] +fn sandbox_snake_movement_scenario() { + let env = Env::default(); + let harness = GameHarness::new(env, SnakeContract); + let client = SnakeContractClient::new(harness.env(), harness.contract_id()); + + // Initialize game + client.init_game(); + + let (x1, y1) = client.get_head_pos(); + + Scenario::new("three ticks") + .turns(3) + .run(&harness, |_player, turn, h| { + let c = SnakeContractClient::new(h.env(), h.contract_id()); + match turn.0 { + 0 => { + c.update_tick(); + let (x, y) = c.get_head_pos(); + // Moves right by default + assert_eq!((x, y), (x1 + 1, y1)); + } + 1 => { + // Change direction to Up (0) + assert!(c.change_direction(&0)); + c.update_tick(); + let (x, y) = c.get_head_pos(); + assert_eq!((x, y), (x1 + 1, y1 - 1)); + } + 2 => { + // Change direction to Left (2) + assert!(c.change_direction(&2)); + c.update_tick(); + let (x, y) = c.get_head_pos(); + assert_eq!((x, y), (x1, y1 - 1)); + } + _ => {} + } + }); +} diff --git a/examples/space_invaders/README.md b/examples/space_invaders/README.md index 028a7876..e2900173 100644 --- a/examples/space_invaders/README.md +++ b/examples/space_invaders/README.md @@ -1,4 +1,4 @@ -# 🎮 Space Invaders - On-Chain Game Example +# Space Invaders - On-Chain Game Example [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/salazarsebas/Cougr) [![Tests](https://img.shields.io/badge/tests-13%20passing-brightgreen)](https://github.com/salazarsebas/Cougr) @@ -6,17 +6,15 @@ A fully functional Space Invaders game implemented as a **Soroban smart contract** using the `cougr-core` ECS (Entity-Component-System) framework on the Stellar blockchain. -## 🚀 Live Deployment +## Live Deployment | Network | Contract ID | Status | |---------|-------------|--------| -| **Testnet** | [`CD6EUPL7Z255BTDPOCMQVWQ7CNM4ORP7QEFPPHO6JC63HRGLW6PYQAG7`](https://stellar.expert/explorer/testnet/contract/CD6EUPL7Z255BTDPOCMQVWQ7CNM4ORP7QEFPPHO6JC63HRGLW6PYQAG7) | 🟢 Active | - -> 🔗 **Explorer**: [View on Stellar Expert](https://stellar.expert/explorer/testnet/contract/CD6EUPL7Z255BTDPOCMQVWQ7CNM4ORP7QEFPPHO6JC63HRGLW6PYQAG7) +| **Testnet** | [``](https://stellar.expert/explorer/testnet/contract/) | 🟢 Active | --- -## 📋 Overview +## 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: @@ -31,7 +29,7 @@ This example demonstrates how to build on-chain game logic on the Stellar blockc --- -## 🔧 Why Cougr-Core? +## 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: @@ -79,7 +77,7 @@ impl Bullet { --- -## 🏗️ Quick Start +## ️ Quick Start ### Prerequisites @@ -123,7 +121,7 @@ cargo test --- -## 📖 Contract API +## Contract API ### Core Functions @@ -147,7 +145,7 @@ cargo test --- -## 🎮 Game Mechanics +## Game Mechanics ### Invaders @@ -183,13 +181,13 @@ cargo test --- -## 🌐 Deploy to Testnet +## Deploy to Testnet ### 1. Setup Identity ```bash # Generate a new identity -stellar keys generate --global deployer --network testnet +stellar keys generate --global deployer --network # Fund the account stellar keys address deployer | xargs -I {} curl "https://friendbot.stellar.org?addr={}" @@ -205,7 +203,7 @@ stellar contract build stellar contract deploy \ --wasm target/wasm32v1-none/release/space_invaders.wasm \ --source deployer \ - --network testnet + --network ``` ### 3. Initialize & Play @@ -215,18 +213,18 @@ stellar contract deploy \ CONTRACT_ID="your_contract_id_here" # Initialize game -stellar contract invoke --id $CONTRACT_ID --source deployer --network testnet -- init_game +stellar contract invoke --id $CONTRACT_ID --source deployer --network -- init_game # Play! -stellar contract invoke --id $CONTRACT_ID --network testnet -- move_ship --direction 1 -stellar contract invoke --id $CONTRACT_ID --network testnet -- shoot -stellar contract invoke --id $CONTRACT_ID --network testnet -- update_tick -stellar contract invoke --id $CONTRACT_ID --network testnet -- get_score +stellar contract invoke --id $CONTRACT_ID --network -- move_ship --direction 1 +stellar contract invoke --id $CONTRACT_ID --network -- shoot +stellar contract invoke --id $CONTRACT_ID --network -- update_tick +stellar contract invoke --id $CONTRACT_ID --network -- get_score ``` --- -## 📁 Project Structure +## Project Structure ``` examples/space_invaders/ @@ -240,6 +238,6 @@ examples/space_invaders/ --- -## 📄 License +## License MIT OR Apache-2.0 diff --git a/examples/spawn_and_move/.gitignore b/examples/spawn_and_move/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/spawn_and_move/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/spawn_and_move/Cargo.toml b/examples/spawn_and_move/Cargo.toml index 9e149deb..e3aa73f1 100644 --- a/examples/spawn_and_move/Cargo.toml +++ b/examples/spawn_and_move/Cargo.toml @@ -11,11 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } -cougr-core = { path = "../../", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/spawn_and_move/README.md b/examples/spawn_and_move/README.md index 2e30253d..efc12214 100644 --- a/examples/spawn_and_move/README.md +++ b/examples/spawn_and_move/README.md @@ -1,83 +1,75 @@ # spawn_and_move -The canonical Cougr starter game demonstrating the full ECS lifecycle on Soroban. - -A player calls `spawn` to enter the world and receives an entity ID. They then call -`move_entity` with a direction to walk around a 2D grid. Every position change emits -an indexed Soroban event so off-chain clients can track movement in real time without -polling. - -## What this example demonstrates - -| Pattern | Where | -|---|---| -| `impl_component_observed!` — ECS component with indexer events | `Position` | -| `impl_component!` — private ECS component (no events needed) | `Moves` | -| `SorobanGame` + `impl_soroban_game!` — standard load/save | `SpawnAndMove` | -| Typed ECS access (`get_typed`, `set_typed_observed`) | `move_entity` | -| Multi-entity independence | test suite | - -## Quick start - -```toml -# Cargo.toml -[dependencies] -cougr-core = "1.1.0" -soroban-sdk = "25.1.0" -``` +**Canonical** example demonstrating `SorobanGame`, `impl_component_observed!`, and typed ECS. -```rust -use cougr_core::game::SorobanGame; -use cougr_core::{impl_component_observed, impl_soroban_game}; -use soroban_sdk::{contract, contractimpl, contracttype, Env}; - -#[contracttype] -#[derive(Clone, Debug)] -pub struct Position { pub x: i32, pub y: i32 } -impl_component_observed!(Position, "position", Table, { x: i32, y: i32 }); - -#[contract] -pub struct MyGame; -impl_soroban_game!(MyGame, "world"); - -#[contractimpl] -impl MyGame { - pub fn spawn(env: Env) -> u32 { - let mut world = MyGame::load_world(&env); - let player = world.spawn_entity(); - world.set_typed_observed(&env, player, &Position { x: 0, y: 0 }); - MyGame::save_world(&env, &world); - player - } -} -``` +## Purpose and pattern -## Build and test +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` | Retrieves the current `Position` of the given entity. | +| `moves` | `entity_id: u32` | `Option` | Retrieves the current `Moves` component of the given entity. | +| `entity_count` | - | `u32` | Retrieves the total number of entities spawned in the world. | + +## Architecture overview -```bash -cargo test -stellar contract build ``` + ┌────────────────────────┐ + │ 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`. -## Directions +## Storage model -| Value | Direction | Effect | -|---|---|---| -| `0` | North | `y += 1` | -| `1` | East | `x += 1` | -| `2` | South | `y -= 1` | -| `3` | West | `x -= 1` | +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. -## Events emitted +## Main gameplay flow -Every `set_typed_observed` call publishes a Soroban event with topics -`("COUGR", "set", "position")` and data `{ entity_id, data }`. Subscribe to -this topic from your frontend or indexer to track all position changes in -real time. +1. **Initialization / Spawn**: The user calls `spawn`. An entity is spawned at `(0,0)` with 10 moves remaining. A `("COUGR", "set", "position")` event is emitted. +2. **Action / Movement**: The user calls `move_entity` with `direction` (0=North, 1=East, 2=South, 3=West). The remaining moves decrement, the position updates, and a position set event is emitted. +3. **Query**: The user reads the entity's position or moves remaining. + +## Cougr APIs used + +- `SorobanGame` and `impl_soroban_game!`: Wires up standard boilerplate for loading/saving world state from instance storage. +- `impl_component_observed!`: Implements component layout with automated indexer events on set. +- `impl_component!`: Defines standard components without indexing event overhead. +- `SimpleWorld`: Provides structured, entity-component key-value management. + +## 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 + +```bash +cargo test +stellar contract build +``` -## Next steps +## Known limitations -- Add an `Address` field to a `Players` component using `impl_rich_component!` -- Add win conditions and a game-over state -- Explore ZK hidden state with `impl_component_observed!` + `zk::stable` for - fog-of-war mechanics +- Simple grid coordinates without map size constraints or collision checks. +- Unauthenticated entity movement (any caller can move any entity ID). diff --git a/examples/spawn_and_move/src/lib.rs b/examples/spawn_and_move/src/lib.rs index 6be6330d..097f3ac2 100644 --- a/examples/spawn_and_move/src/lib.rs +++ b/examples/spawn_and_move/src/lib.rs @@ -55,9 +55,9 @@ impl_component!(Moves, "moves", Sparse, { remaining: u32, last_direction: u32 }) // ─── Direction constants ────────────────────────────────────────────────────── pub const NORTH: u32 = 0; // +y -pub const EAST: u32 = 1; // +x +pub const EAST: u32 = 1; // +x pub const SOUTH: u32 = 2; // −y -pub const WEST: u32 = 3; // −x +pub const WEST: u32 = 3; // −x // ─── Contract ──────────────────────────────────────────────────────────────── diff --git a/examples/spawn_and_move/src/sandbox_tests.rs b/examples/spawn_and_move/src/sandbox_tests.rs index b5e5c3b3..98f4e367 100644 --- a/examples/spawn_and_move/src/sandbox_tests.rs +++ b/examples/spawn_and_move/src/sandbox_tests.rs @@ -1,7 +1,6 @@ #![cfg(test)] -use crate::{SpawnAndMove, SpawnAndMoveClient, NORTH}; -use cougr_core::game::SorobanGame; +use crate::{SpawnAndMove, SpawnAndMoveClient, EAST, NORTH, SOUTH}; use cougr_core::test::{GameHarness, Scenario, SnapshotAssert, WorldFixture}; use soroban_sdk::Env; @@ -59,4 +58,36 @@ fn sandbox_multi_turn_movement_sequence() { assert_eq!((pos.x, pos.y), (0, 2)); } }); -} \ No newline at end of file +} + +#[test] +fn sandbox_three_step_movement_scenario() { + let env = Env::default(); + let harness = GameHarness::new(env, SpawnAndMove); + let client = SpawnAndMoveClient::new(harness.env(), harness.contract_id()); + let id = client.spawn(); + + Scenario::new("three step movement") + .turns(3) + .run(&harness, |_player, turn, h| { + let c = SpawnAndMoveClient::new(h.env(), h.contract_id()); + match turn.0 { + 0 => { + c.move_entity(&id, &NORTH); + let pos = c.position(&id).unwrap(); + assert_eq!((pos.x, pos.y), (0, 1)); + } + 1 => { + c.move_entity(&id, &EAST); + let pos = c.position(&id).unwrap(); + assert_eq!((pos.x, pos.y), (1, 1)); + } + 2 => { + c.move_entity(&id, &SOUTH); + let pos = c.position(&id).unwrap(); + assert_eq!((pos.x, pos.y), (1, 0)); + } + _ => {} + } + }); +} diff --git a/examples/sudoku/.gitignore b/examples/sudoku/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/sudoku/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/sudoku/README.md b/examples/sudoku/README.md index 6b4797b0..89ad1b9e 100644 --- a/examples/sudoku/README.md +++ b/examples/sudoku/README.md @@ -156,33 +156,33 @@ After building the WASM (`stellar contract build`), deploy and play on Testnet: ```bash # Generate a player identity and fund via Friendbot stellar keys generate sudoku_player -stellar keys fund sudoku_player --network testnet +stellar keys fund sudoku_player --network # Deploy the contract CONTRACT_ID=$(stellar contract deploy \ --wasm target/wasm32v1-none/release/sudoku.wasm \ - --network testnet \ + --network \ --source sudoku_player) # Initialise the puzzle (pass a flat 81-element JSON array; 0=empty, 1–9=fixed) -stellar contract invoke --id $CONTRACT_ID --network testnet --source sudoku_player \ +stellar contract invoke --id $CONTRACT_ID --network --source sudoku_player \ -- init_game \ --puzzle '[5,3,0,0,7,0,0,0,0,6,0,0,1,9,5,0,0,0,0,9,8,0,0,0,0,6,0,8,0,0,0,6,0,0,0,3,4,0,0,8,0,3,0,0,1,7,0,0,0,2,0,0,0,6,0,6,0,0,0,0,2,8,0,0,0,0,4,1,9,0,0,5,0,0,0,0,8,0,0,7,9]' # Read the initial state -stellar contract invoke --id $CONTRACT_ID --network testnet --source sudoku_player \ +stellar contract invoke --id $CONTRACT_ID --network --source sudoku_player \ -- get_state # Read a cell -stellar contract invoke --id $CONTRACT_ID --network testnet --source sudoku_player \ +stellar contract invoke --id $CONTRACT_ID --network --source sudoku_player \ -- get_cell --row 0 --col 2 # Submit a value -stellar contract invoke --id $CONTRACT_ID --network testnet --source sudoku_player \ +stellar contract invoke --id $CONTRACT_ID --network --source sudoku_player \ -- submit_value --row 0 --col 2 --value 4 # Check if solved -stellar contract invoke --id $CONTRACT_ID --network testnet --source sudoku_player \ +stellar contract invoke --id $CONTRACT_ID --network --source sudoku_player \ -- is_solved ``` diff --git a/examples/tap_battle/README.md b/examples/tap_battle/README.md index 958a51e6..13371c7f 100644 --- a/examples/tap_battle/README.md +++ b/examples/tap_battle/README.md @@ -1,4 +1,4 @@ -# 🎮 Tap Battle — Mobile-First Competitive Tapping Game +# Tap Battle — Mobile-First Competitive Tapping Game [![Build Status](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/salazarsebas/Cougr) [![Tests](https://img.shields.io/badge/tests-15%20passing-brightgreen)](https://github.com/salazarsebas/Cougr) @@ -6,7 +6,7 @@ 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. -## 🔐 Why Passkeys? +## Why Passkeys? Traditional blockchain games require seed phrases and per-transaction wallet prompts. **Tap Battle eliminates both**: @@ -21,7 +21,7 @@ Passkeys use the **secp256r1** curve (the same as WebAuthn/FIDO2), enabling biom --- -## 🚀 Mobile-First Authentication Flow +## Mobile-First Authentication Flow ``` ┌─────────────────────────────────────────────────────────┐ @@ -50,7 +50,7 @@ Passkeys use the **secp256r1** curve (the same as WebAuthn/FIDO2), enabling biom --- -## 🔧 Cougr-Core Integration +## Cougr-Core Integration This example showcases two key cougr-core features: @@ -93,7 +93,7 @@ let scope = SessionBuilder::new(&env) --- -## 📖 Contract API +## Contract API ### Authentication @@ -122,7 +122,7 @@ let scope = SessionBuilder::new(&env) --- -## 🎮 Game Mechanics +## Game Mechanics ### Combo System @@ -148,7 +148,7 @@ Taps within **5 ledger sequences** of each other maintain a combo streak: --- -## 🏗️ Quick Start +## ️ Quick Start ### Prerequisites @@ -196,7 +196,7 @@ cargo test --- -## 📁 Project Structure +## Project Structure ``` examples/tap_battle/ @@ -212,6 +212,6 @@ examples/tap_battle/ --- -## 📄 License +## License MIT OR Apache-2.0 diff --git a/examples/tetris/.gitignore b/examples/tetris/.gitignore index 44133c10..af23882e 100644 --- a/examples/tetris/.gitignore +++ b/examples/tetris/.gitignore @@ -1,4 +1,5 @@ /target test_snapshots/ *.wasm -Cargo.lock + +target/ diff --git a/examples/tetris/Cargo.toml b/examples/tetris/Cargo.toml index 43908bfe..e12ed288 100644 --- a/examples/tetris/Cargo.toml +++ b/examples/tetris/Cargo.toml @@ -1,4 +1,5 @@ [package] +description = "Tetris puzzle game with piece rotation and board clearing on Stellar Soroban" name = "tetris" version = "0.1.0" edition = "2021" diff --git a/examples/tetris/README.md b/examples/tetris/README.md index 62d92838..b33ceffb 100644 --- a/examples/tetris/README.md +++ b/examples/tetris/README.md @@ -2,14 +2,14 @@ An on-chain Tetris game implementation using the Cougr-Core ECS framework on Stellar's Soroban platform. -## 📋 Overview +## Overview This example demonstrates how to build a fully functional game as a smart contract using: - **Soroban** - Stellar's smart contract platform - **Cougr-Core** - ECS framework for on-chain games - **Rust** - Smart contract programming language -## 🎮 Game Features +## Game Features | Feature | Description | |---------|-------------| @@ -20,7 +20,7 @@ This example demonstrates how to build a fully functional game as a smart contra | **Scoring** | Points based on lines cleared | | **Leveling** | Difficulty increases every 10 lines | -## 🚀 Quick Start +## Quick Start ### Prerequisites @@ -44,7 +44,7 @@ cargo test stellar contract build ``` -## 📦 Deployment +## Deployment ### Testnet Deployment ```bash @@ -52,39 +52,38 @@ stellar contract build stellar contract deploy \ --wasm target/wasm32v1-none/release/tetris.wasm \ --source \ - --network testnet + --network ``` -**Deployed Contract:** - **Network**: Stellar Testnet -- **Contract ID**: `CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF` -- **Explorer**: `https://stellar.expert/explorer/testnet/contract/CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF` +- **Contract ID**: `` +- **Explorer**: `https://stellar.expert/explorer/testnet/contract/` ### Invoke Functions ```bash # Initialize a new game stellar contract invoke \ - --id CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF \ + --id \ --source \ - --network testnet \ + --network \ -- init_game # Move piece left stellar contract invoke \ - --id CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF \ + --id \ --source \ - --network testnet \ + --network \ -- move_left # Update game tick (gravity + line clearing) stellar contract invoke \ - --id CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF \ + --id \ --source \ - --network testnet \ + --network \ -- update_tick ``` -## 🎯 Benefits of Using Cougr-Core +## Benefits of Using Cougr-Core ### Traditional Soroban vs. Cougr-Core @@ -125,7 +124,7 @@ stellar contract invoke \ - Easier to understand and debug - Modular architecture -## 🧪 Testing +## Testing ```bash # Run all tests cargo test @@ -149,7 +148,7 @@ cargo test test_rotate | `test_update_tick` | Tests game tick and line clearing | | `test_game_over` | Tests end game detection | -## 📁 Project Structure +## Project Structure ``` examples/tetris/ ├── Cargo.toml # Dependencies & build config @@ -159,7 +158,7 @@ examples/tetris/ └── lib.rs # Smart contract implementation ``` -## 🔧 Configuration +## Configuration **Cargo.toml** ```toml @@ -168,17 +167,17 @@ soroban-sdk = "25.1.0" cougr-core = "1.0.0" ``` -## 📚 Resources +## Resources - [Soroban Documentation](https://developers.stellar.org/docs/build/smart-contracts) - [Stellar Documentation](https://developers.stellar.org/) - [Cougr Repository](https://github.com/salazarsebas/Cougr) - [Rust Book](https://doc.rust-lang.org/book/) -## 🤝 Contributing +## Contributing This example is part of the Cougr framework. Contributions are welcome! -## 📄 License +## License Licensed under MIT OR Apache-2.0 \ No newline at end of file diff --git a/examples/tic_tac_toe/.gitignore b/examples/tic_tac_toe/.gitignore index 0dc20183..11f4068e 100644 --- a/examples/tic_tac_toe/.gitignore +++ b/examples/tic_tac_toe/.gitignore @@ -1,6 +1,5 @@ # Build artifacts target/ -Cargo.lock # Test snapshots test_snapshots/ @@ -17,3 +16,4 @@ test_snapshots/ # OS .DS_Store Thumbs.db +*.wasm diff --git a/examples/tic_tac_toe/Cargo.toml b/examples/tic_tac_toe/Cargo.toml index 3acca32b..8e8126ed 100644 --- a/examples/tic_tac_toe/Cargo.toml +++ b/examples/tic_tac_toe/Cargo.toml @@ -11,10 +11,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "25.1.0" -cougr-core = { path = "../../" } +cougr-core = "1.1.0" [dev-dependencies] soroban-sdk = { version = "25.1.0", features = ["testutils"] } +cougr-core = { version = "1.1.0", features = ["testutils"] } [profile.release] opt-level = "z" diff --git a/examples/tic_tac_toe/README.md b/examples/tic_tac_toe/README.md index fe656dd3..5db9fd3c 100644 --- a/examples/tic_tac_toe/README.md +++ b/examples/tic_tac_toe/README.md @@ -2,12 +2,6 @@ A fully functional Tic Tac Toe game implemented as a Soroban smart contract on the Stellar blockchain, demonstrating the **Cougr-Core** ECS (Entity Component System) framework for on-chain gaming. -| | | -| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| **Contract ID** | `CCCJRYJE32PICS6IN3MVNOZFUYRDXTI6RXRZWTFMVJSLKROLSXV75Z2P` | -| **Network** | Stellar Testnet | -| **Explorer** | [View on Stellar Expert](https://stellar.expert/explorer/testnet/contract/CCCJRYJE32PICS6IN3MVNOZFUYRDXTI6RXRZWTFMVJSLKROLSXV75Z2P) | - ## Why Cougr-Core? Cougr-Core provides an ECS architecture that simplifies on-chain game development. Here's how it compares to vanilla Soroban: @@ -171,7 +165,7 @@ ECSWorldState ```bash # Generate funded account -stellar keys generate deployer --network testnet --fund +stellar keys generate deployer --network --fund # Build contract stellar contract build @@ -180,7 +174,7 @@ stellar contract build stellar contract deploy \ --wasm target/tic_tac_toe.wasm \ --source deployer \ - --network testnet + --network ``` ### Interact with Deployed Contract @@ -188,24 +182,24 @@ stellar contract deploy \ ```bash # Initialize a game stellar contract invoke \ - --id CCCJRYJE32PICS6IN3MVNOZFUYRDXTI6RXRZWTFMVJSLKROLSXV75Z2P \ - --network testnet \ + --id \ + --network \ -- init_game \ --player_x \ --player_o # Make a move stellar contract invoke \ - --id CCCJRYJE32PICS6IN3MVNOZFUYRDXTI6RXRZWTFMVJSLKROLSXV75Z2P \ - --network testnet \ + --id \ + --network \ -- make_move \ --player \ --position 4 # Get game state stellar contract invoke \ - --id CCCJRYJE32PICS6IN3MVNOZFUYRDXTI6RXRZWTFMVJSLKROLSXV75Z2P \ - --network testnet \ + --id \ + --network \ -- get_state ``` diff --git a/examples/tic_tac_toe/src/lib.rs b/examples/tic_tac_toe/src/lib.rs index a9659127..b7b3c8b4 100644 --- a/examples/tic_tac_toe/src/lib.rs +++ b/examples/tic_tac_toe/src/lib.rs @@ -8,7 +8,6 @@ #![no_std] -use cougr_core::component::ComponentTrait; use cougr_core::game::SorobanGame; use cougr_core::{impl_component, impl_rich_component, impl_soroban_game}; use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Address, Env, Symbol, Vec}; @@ -87,6 +86,7 @@ pub struct MoveResult { const GAME_ENTITY: u32 = 1; #[contract] +#[derive(Clone)] pub struct TicTacToeContract; impl_soroban_game!(TicTacToeContract, "ttt_world"); @@ -101,7 +101,15 @@ impl TicTacToeContract { world.set_rich(&env, GAME_ENTITY, &Board::new(&env)); world.set_rich(&env, GAME_ENTITY, &Players { player_x, player_o }); - world.set_typed(&env, GAME_ENTITY, &TurnState { is_x_turn: true, move_count: 0, status: 0 }); + world.set_typed( + &env, + GAME_ENTITY, + &TurnState { + is_x_turn: true, + move_count: 0, + status: 0, + }, + ); TicTacToeContract::save_world(&env, &world); Self::read_game_state(&env, &world) @@ -111,11 +119,14 @@ impl TicTacToeContract { pub fn make_move(env: Env, player: Address, position: u32) -> MoveResult { let mut world = TicTacToeContract::load_world(&env); - let players: Players = world.get_rich::(&env, GAME_ENTITY) + let players: Players = world + .get_rich::(&env, GAME_ENTITY) .unwrap_or_else(|| panic!("game not initialised")); - let turn: TurnState = world.get_typed::(&env, GAME_ENTITY) + let turn: TurnState = world + .get_typed::(&env, GAME_ENTITY) .unwrap_or_else(|| panic!("game not initialised")); - let mut board: Board = world.get_rich::(&env, GAME_ENTITY) + let mut board: Board = world + .get_rich::(&env, GAME_ENTITY) .unwrap_or_else(|| panic!("game not initialised")); // Validate @@ -146,14 +157,22 @@ impl TicTacToeContract { let new_move_count = turn.move_count + 1; let new_status = Self::detect_winner(&board.cells, new_move_count); - let new_is_x_turn = if new_status == 0 { !turn.is_x_turn } else { turn.is_x_turn }; + let new_is_x_turn = if new_status == 0 { + !turn.is_x_turn + } else { + turn.is_x_turn + }; world.set_rich(&env, GAME_ENTITY, &board); - world.set_typed(&env, GAME_ENTITY, &TurnState { - is_x_turn: new_is_x_turn, - move_count: new_move_count, - status: new_status, - }); + world.set_typed( + &env, + GAME_ENTITY, + &TurnState { + is_x_turn: new_is_x_turn, + move_count: new_move_count, + status: new_status, + }, + ); TicTacToeContract::save_world(&env, &world); @@ -205,7 +224,8 @@ impl TicTacToeContract { /// Reset the board but keep the same players. pub fn reset_game(env: Env) -> GameState { let world = TicTacToeContract::load_world(&env); - let players: Players = world.get_rich::(&env, GAME_ENTITY) + let players: Players = world + .get_rich::(&env, GAME_ENTITY) .unwrap_or_else(|| panic!("game not initialised")); Self::init_game(env, players.player_x, players.player_o) } @@ -213,12 +233,19 @@ impl TicTacToeContract { // ─── Internal helpers ───────────────────────────────────────────────────── fn read_game_state(env: &Env, world: &cougr_core::simple_world::SimpleWorld) -> GameState { - let board: Board = world.get_rich::(env, GAME_ENTITY) + let board: Board = world + .get_rich::(env, GAME_ENTITY) .unwrap_or_else(|| Board::new(env)); - let players: Players = world.get_rich::(env, GAME_ENTITY) + let players: Players = world + .get_rich::(env, GAME_ENTITY) .unwrap_or_else(|| panic!("players not found")); - let turn: TurnState = world.get_typed::(env, GAME_ENTITY) - .unwrap_or(TurnState { is_x_turn: true, move_count: 0, status: 0 }); + let turn: TurnState = world + .get_typed::(env, GAME_ENTITY) + .unwrap_or(TurnState { + is_x_turn: true, + move_count: 0, + status: 0, + }); GameState { cells: board.cells, @@ -230,7 +257,11 @@ impl TicTacToeContract { } } - fn failure(env: &Env, world: &cougr_core::simple_world::SimpleWorld, msg: Symbol) -> MoveResult { + fn failure( + env: &Env, + world: &cougr_core::simple_world::SimpleWorld, + msg: Symbol, + ) -> MoveResult { MoveResult { success: false, game_state: Self::read_game_state(env, world), @@ -240,9 +271,14 @@ impl TicTacToeContract { fn detect_winner(cells: &Vec, move_count: u32) -> u32 { const LINES: [[u32; 3]; 8] = [ - [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows - [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns - [0, 4, 8], [2, 4, 6], // diagonals + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], // rows + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], // columns + [0, 4, 8], + [2, 4, 6], // diagonals ]; for line in LINES.iter() { let a = cells.get(line[0]).unwrap_or(0); @@ -252,9 +288,15 @@ impl TicTacToeContract { return a; // 1 = X wins, 2 = O wins } } - if move_count >= 9 { 3 } else { 0 } + if move_count >= 9 { + 3 + } else { + 0 + } } } +#[cfg(test)] +mod sandbox_tests; #[cfg(test)] mod test; diff --git a/examples/tic_tac_toe/src/sandbox_tests.rs b/examples/tic_tac_toe/src/sandbox_tests.rs new file mode 100644 index 00000000..84717eb6 --- /dev/null +++ b/examples/tic_tac_toe/src/sandbox_tests.rs @@ -0,0 +1,46 @@ +#![cfg(test)] + +use crate::{TicTacToeContract, TicTacToeContractClient}; +use cougr_core::test::{GameHarness, PlayerSlot, Scenario}; +use soroban_sdk::Env; + +#[test] +fn sandbox_three_move_game_scenario() { + let env = Env::default(); + let mut harness = GameHarness::new(env, TicTacToeContract); + harness.mock_players(2); + harness.mock_all_auths(); + + let client = TicTacToeContractClient::new(harness.env(), harness.contract_id()); + let p_x = harness.player(PlayerSlot(0)).clone(); + let p_o = harness.player(PlayerSlot(1)).clone(); + + // Initialize the game + client.init_game(&p_x, &p_o); + + Scenario::new("three moves") + .players(2) + .turns(3) + .run(&harness, |player_slot, turn, h| { + let c = TicTacToeContractClient::new(h.env(), h.contract_id()); + let p = h.player(player_slot).clone(); + match turn.0 { + 0 => { + let res = c.make_move(&p, &0u32); // player X moves at 0 + assert!(res.success); + assert_eq!(res.game_state.cells.get(0).unwrap(), 1); + } + 1 => { + let res = c.make_move(&p, &4u32); // player O moves at 4 + assert!(res.success); + assert_eq!(res.game_state.cells.get(4).unwrap(), 2); + } + 2 => { + let res = c.make_move(&p, &1u32); // player X moves at 1 + assert!(res.success); + assert_eq!(res.game_state.cells.get(1).unwrap(), 1); + } + _ => {} + } + }); +} diff --git a/examples/tower_defense/.gitignore b/examples/tower_defense/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/tower_defense/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/trading_card_game/.gitignore b/examples/trading_card_game/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/trading_card_game/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/examples/trading_card_game/Cargo.toml b/examples/trading_card_game/Cargo.toml index cf7372f0..94724bde 100644 --- a/examples/trading_card_game/Cargo.toml +++ b/examples/trading_card_game/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "tower_defense" +name = "trading_card_game" version = "0.0.1" edition = "2021" authors = ["Cougr Contributors"] -description = "Tower defense on-chain game example using cougr-core ECS framework" +description = "Two-player trading card game with atomic batch turns and session keys" [lib] crate-type = ["cdylib", "rlib"] diff --git a/examples/treasure_hunt/.gitignore b/examples/treasure_hunt/.gitignore new file mode 100644 index 00000000..3593b9ea --- /dev/null +++ b/examples/treasure_hunt/.gitignore @@ -0,0 +1,17 @@ +# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/scripts/enforce_hygiene.sh b/scripts/enforce_hygiene.sh new file mode 100755 index 00000000..c9267e32 --- /dev/null +++ b/scripts/enforce_hygiene.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# enforce_hygiene.sh — Enforce repository hygiene standards across examples/ (#225) +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +EXAMPLES_DIR="$REPO_ROOT/examples" + +echo "=== Cougr Examples Hygiene Enforcement (#225) ===" + +while IFS= read -r target_dir; do + rm -rf "$target_dir" || true +done < <(find "$EXAMPLES_DIR" -type d -name "target") +while IFS= read -r wasm_file; do + rm -f "$wasm_file" || true +done < <(find "$EXAMPLES_DIR" -type f -name "*.wasm") + +GITIGNORE_CONTENT='# Build artifacts +target/ +*.wasm + +# Soroban +.soroban/ +test_snapshots/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db +' + +for example_dir in "$EXAMPLES_DIR"/*/; do + [ -d "$example_dir" ] || continue + gitignore_file="$example_dir/.gitignore" + if [ ! -f "$gitignore_file" ]; then + printf '%s' "$GITIGNORE_CONTENT" > "$gitignore_file" + else + grep -qE '^Cargo\.lock$' "$gitignore_file" && sed -i.bak '/^Cargo\.lock$/d' "$gitignore_file" && rm -f "$gitignore_file.bak" + grep -qE '^(/)?target/' "$gitignore_file" || printf '\ntarget/\n' >> "$gitignore_file" + grep -qE '^\*\.wasm' "$gitignore_file" || printf '*.wasm\n' >> "$gitignore_file" + fi +done + +python3 - "$EXAMPLES_DIR" <<'PY' +import re, sys +from pathlib import Path +examples_dir = Path(sys.argv[1]) +descriptions = { + "ai_dungeon_master_arena": "AI Dungeon Master Arena with x402-paid actions and stellar-zk run proofs", + "angry_birds": "Angry Birds turn-based physics puzzle using Cougr-Core ECS on Stellar Soroban", + "arkanoid": "Arkanoid on-chain game using Cougr-Core ECS framework on Stellar Soroban", + "asteroids": "Arcade asteroid shooter with entity movement and collision systems", + "battleship": "Battleship with hidden board using commit-reveal pattern on Stellar Soroban", + "blind_auction": "Canonical Cougr ZK example: sealed bids via cougr_core::circuits::sealed_bid", + "bomberman": "Grid action game with tile updates and timed hazards on Stellar Soroban", + "checkers": "On-chain Checkers game example using Cougr ECS on Soroban", + "chess": "Verifiable Chess with ZK move validation using Cougr-Core on Stellar Soroban", + "connect_four": "Connect Four on-chain game using Cougr-Core ECS framework on Stellar Soroban", + "cross_asset_racing_league": "Cross-asset racing league with payment-driven gameplay and stellar-zk anti-cheat", + "dice_duel": "Canonical Cougr ZK example: fair dice via cougr_core::circuits::fair_dice", + "flappy_bird": "Flappy Bird on-chain game example using cougr-core ECS framework", + "fog_explorer": "Canonical Cougr ZK example: fog of war via cougr_core::circuits::fog_of_war", + "geometry_dash": "Geometry Dash on-chain game example using cougr-core ECS framework", + "guild_arena": "PvP arena game with guild-based social recovery and multi-device support using Cougr-Core", + "guild_treasury_wars": "Guild Treasury Wars with DAO-governed factions and stellar-zk commitments", + "hidden_hand": "Canonical Cougr ZK example: hidden card deals via cougr_core::circuits::hidden_cards", + "memory_match": "Memory Match card game on-chain using Cougr-Core ECS framework on Stellar Soroban", + "minesweeper": "Minesweeper on-chain game using Cougr-Core ECS framework on Stellar Soroban", + "murdoku": "Murdoku puzzle registry and creator contract using cougr-core ECS framework", + "pac_man": "Pac-Man on-chain game example using Cougr-Core for Stellar/Soroban", + "pokemon_mini": "Pokémon Mini on-chain game example using cougr-core ECS framework", + "pong": "Pong on-chain game using Cougr-Core ECS framework on Stellar Soroban", + "proof_of_hunt": "Proof-of-Hunt Soroban example with stellar-zk proof validation and x402 premium actions", + "reversi": "Reversi on-chain game using Cougr-Core ECS framework on Stellar Soroban", + "rock_paper_scissors": "Rock Paper Scissors with commit-reveal using Poseidon2 hashing on Stellar Soroban", + "session_arena": "Canonical Cougr session UX example: approve once, play frictionlessly, renew on expiry", + "shadow_draft_card_game": "Shadow Draft card game with hidden-hand draft gameplay and stellar-zk card validation", + "snake": "Snake on-chain game example using cougr-core ECS framework", + "space_invaders": "Space Invaders on-chain game example using cougr-core", + "spawn_and_move": "Canonical Cougr starter: spawn a player entity and move it around a 2D world on Stellar Soroban", + "sudoku": "Sudoku puzzle on-chain using Cougr-Core ECS framework on Stellar Soroban", + "tap_battle": "Tap Battle on-chain game with passkey authentication using cougr-core", + "tetris": "Tetris puzzle game with piece rotation and board clearing on Stellar Soroban", + "tic_tac_toe": "Tic Tac Toe on-chain game using Cougr-Core ECS framework on Stellar Soroban", + "tower_defense": "Tower defense on-chain game example using cougr-core ECS framework", + "trading_card_game": "Two-player trading card game with atomic batch turns and session keys", + "treasure_hunt": "Treasure Hunt Soroban example using Merkle map commitments and sparse fog-of-war", +} +for example_dir in sorted(examples_dir.iterdir()): + if not example_dir.is_dir(): continue + cargo = example_dir / "Cargo.toml" + if not cargo.exists(): continue + text = cargo.read_text(encoding="utf-8") + changed = False + if example_dir.name == "trading_card_game" and 'name = "tower_defense"' in text: + text = text.replace('name = "tower_defense"', 'name = "trading_card_game"', 1) + changed = True + desc = descriptions.get(example_dir.name) + if desc: + m = re.search(r'^description\s*=\s*"(.*)"\s*$', text, re.MULTILINE) + cur = m.group(1) if m else None + if cur != desc: + if m: + text = re.sub(r'^description\s*=.*$', f'description = "{desc}"', text, count=1, flags=re.MULTILINE) + else: + text = text.replace("[package]\n", f'[package]\ndescription = "{desc}"\n', 1) + changed = True + if example_dir.name == "murdoku" and "[workspace]" not in text: + text = text.rstrip() + "\n\n[workspace]\n" + changed = True + if changed: + cargo.write_text(text, encoding="utf-8") +PY + +TIC_TAC_TOE_README="$EXAMPLES_DIR/tic_tac_toe/README.md" +if [ -f "$TIC_TAC_TOE_README" ] && grep -q '>' "$TIC_TAC_TOE_README" 2>/dev/null; then + git -C "$REPO_ROOT" show main:examples/tic_tac_toe/README.md > "$TIC_TAC_TOE_README" +fi +for readme in "$EXAMPLES_DIR"/*/README.md; do + [ -f "$readme" ] && python3 "$REPO_ROOT/scripts/sanitize_readme.py" "$readme" +done + +grep -qE '^Cargo\.lock$' "$REPO_ROOT/.gitignore" 2>/dev/null && sed -i.bak '/^Cargo\.lock$/d' "$REPO_ROOT/.gitignore" && rm -f "$REPO_ROOT/.gitignore.bak" + +"$REPO_ROOT/scripts/verify_hygiene.sh" \ No newline at end of file diff --git a/scripts/sanitize_readme.py b/scripts/sanitize_readme.py new file mode 100644 index 00000000..c17e02a0 --- /dev/null +++ b/scripts/sanitize_readme.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Sanitize example README.md files for issue #225.""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +CONTRACT_ID_RE = re.compile(r"C[A-Z2-7]{55}") +TX_HASH_RE = re.compile(r"\b[a-f0-9]{64}\b") +PROMO_EMOJI_RE = re.compile( + r"[\U0001F300-\U0001FAFF\U00002600-\U000027BF]" + r"|✅|✓|🔗|🟢|🔴|🟡|⚡|💎|⭐|🔥|🚀|🎮|🎯|✨|💪|📦|🏆|🔧" +) + +DEPLOYMENT_PATTERNS = [ + re.compile(r"^\|\s*\*?\*?Contract ID\*?\*?\s*\|.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\|\s*\*?\*?Transaction Hash\*?\*?\s*\|.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\|\s*\*?\*?Explorer\*?\*?\s*\|.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\|\s*\*?\*?Network\*?\*?\s*\|\s*Stellar (Testnet|Mainnet|Futurenet).*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\|\s*Testnet\s*\|.*C[A-Z2-7]{55}.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\*\*✅ Successfully Deployed.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^>\s*🔗.*$", re.MULTILINE), + re.compile(r"^>\s*\*\*Contract ID:\*\*.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^CONTRACT_ID=\"C[A-Z2-7]{55}\".*$", re.MULTILINE), + re.compile(r"^\*\*Contract ID\*\*:\s*`C[A-Z2-7]{55}`.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\*\*Explorer Link\*\*:.*C[A-Z2-7]{55}.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^### this the deployed testnet link.*$", re.MULTILINE | re.IGNORECASE), + re.compile(r"^\*\*Deployed Contract:\*\*.*$", re.MULTILINE | re.IGNORECASE), +] + + +def sanitize_readme(filepath: Path) -> bool: + original = filepath.read_text(encoding="utf-8") + content = original + + for pattern in DEPLOYMENT_PATTERNS: + content = pattern.sub("", content) + + content = CONTRACT_ID_RE.sub("", content) + + lines = [] + for line in content.splitlines(keepends=True): + if re.search(r"(transaction|hash|deploy|tx)", line, re.IGNORECASE): + line = TX_HASH_RE.sub("", line) + lines.append(line) + content = "".join(lines) + + content = re.sub(r"--id\s+C[A-Z2-7]{55}", "--id ", content) + content = re.sub( + r"--network\s+(testnet|mainnet|futurenet)\b", + "--network ", + content, + flags=re.IGNORECASE, + ) + + cleaned_lines = [] + for line in content.splitlines(keepends=True): + if line.lstrip().startswith("#") or re.match(r"^>\s", line) or re.match(r"^\|\s*[^|]+\|\s*[🟢✅🔗]", line): + line = PROMO_EMOJI_RE.sub("", line) + line = re.sub(r"\s{2,}", " ", line) + cleaned_lines.append(line) + content = "".join(cleaned_lines) + content = re.sub(r"\n{3,}", "\n\n", content) + + if content != original: + filepath.write_text(content, encoding="utf-8") + return True + return False + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("files", nargs="+") + args = parser.parse_args() + for filepath_str in args.files: + filepath = Path(filepath_str) + if not filepath.exists(): + print(f" Not found: {filepath}", file=sys.stderr) + continue + print(f" {'Sanitized' if sanitize_readme(filepath) else 'No changes'}: {filepath}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) \ No newline at end of file diff --git a/scripts/verify_hygiene.sh b/scripts/verify_hygiene.sh new file mode 100755 index 00000000..fb97ebae --- /dev/null +++ b/scripts/verify_hygiene.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# verify_hygiene.sh — Verify repository hygiene standards from issue #225 +# + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +EXAMPLES_DIR="$REPO_ROOT/examples" +EXIT_CODE=0 + +echo "=== Hygiene Verification (#225) ===" +echo "" + +echo "Check 1: No tracked target/ directories" +tracked_targets=$(git -C "$REPO_ROOT" ls-files 'examples/**/target/**' 2>/dev/null || true) +if [ -z "$tracked_targets" ]; then + echo " PASS: No target/ files tracked" +else + echo " FAIL: Tracked target/ files found:" + echo "$tracked_targets" | sed 's/^/ /' + EXIT_CODE=1 +fi +echo "" + +echo "Check 2: No tracked .wasm files" +tracked_wasm=$(git -C "$REPO_ROOT" ls-files 'examples/**/*.wasm' 2>/dev/null || true) +if [ -z "$tracked_wasm" ]; then + echo " PASS: No .wasm files tracked" +else + echo " FAIL: Tracked .wasm files found:" + echo "$tracked_wasm" | sed 's/^/ /' + EXIT_CODE=1 +fi +echo "" + +echo "Check 3: No hardcoded contract IDs in README.md files" +contract_ids=$(grep -rE 'C[A-Z2-7]{55}' "$EXAMPLES_DIR"/*/README.md 2>/dev/null || true) +if [ -z "$contract_ids" ]; then + echo " PASS: No hardcoded contract IDs found" +else + echo " FAIL: Hardcoded contract IDs found:" + echo "$contract_ids" | sed 's/^/ /' + EXIT_CODE=1 +fi +echo "" + +echo "Check 4: .gitignore in every example directory" +missing_gitignore=0 +for example_dir in "$EXAMPLES_DIR"/*/; do + if [ ! -f "$example_dir/.gitignore" ]; then + echo " FAIL: Missing .gitignore in $(basename "$example_dir")" + missing_gitignore=$((missing_gitignore + 1)) + EXIT_CODE=1 + fi +done +if [ $missing_gitignore -eq 0 ]; then + echo " PASS: All examples have .gitignore" +fi +echo "" + +echo "Check 5: .gitignore excludes target/ and *.wasm" +missing_rules=0 +for gitignore in "$EXAMPLES_DIR"/*/.gitignore; do + example_name=$(basename "$(dirname "$gitignore")") + if ! grep -qE '^(/)?target/' "$gitignore"; then + echo " FAIL: examples/$example_name/.gitignore does not exclude target/" + missing_rules=$((missing_rules + 1)) + EXIT_CODE=1 + fi + if ! grep -qE '^\*\.wasm' "$gitignore"; then + echo " FAIL: examples/$example_name/.gitignore does not exclude *.wasm" + missing_rules=$((missing_rules + 1)) + EXIT_CODE=1 + fi +done +if [ $missing_rules -eq 0 ]; then + echo " PASS: All .gitignore files exclude target/ and *.wasm" +fi +echo "" + +echo "Check 6: .gitignore does not exclude Cargo.lock" +cargo_lock_ignored=0 +for gitignore in "$EXAMPLES_DIR"/*/.gitignore; do + example_name=$(basename "$(dirname "$gitignore")") + if grep -qE '^Cargo\.lock$' "$gitignore"; then + echo " FAIL: examples/$example_name/.gitignore excludes Cargo.lock" + cargo_lock_ignored=$((cargo_lock_ignored + 1)) + EXIT_CODE=1 + fi +done +if [ $cargo_lock_ignored -eq 0 ]; then + echo " PASS: No example .gitignore excludes Cargo.lock" +fi +echo "" + +echo "Check 7: Root .gitignore does not exclude Cargo.lock" +if grep -qE '^Cargo\.lock$' "$REPO_ROOT/.gitignore" 2>/dev/null; then + echo " FAIL: Root .gitignore excludes Cargo.lock" + EXIT_CODE=1 +else + echo " PASS: Root .gitignore does not exclude Cargo.lock" +fi +echo "" + +echo "Check 8: cargo metadata --no-deps succeeds for all examples" +metadata_failed=0 +for example_dir in "$EXAMPLES_DIR"/*/; do + example_name=$(basename "$example_dir") + if (cd "$example_dir" && cargo metadata --no-deps --format-version 1 >/dev/null 2>&1); then + : + else + echo " FAIL: cargo metadata failed for $example_name" + metadata_failed=$((metadata_failed + 1)) + EXIT_CODE=1 + fi +done +if [ $metadata_failed -eq 0 ]; then + echo " PASS: cargo metadata succeeds for all examples" +fi +echo "" + +if [ $EXIT_CODE -eq 0 ]; then + echo "=== ALL CHECKS PASSED ===" +else + echo "=== SOME CHECKS FAILED ===" +fi + +exit $EXIT_CODE \ No newline at end of file