From be6955813c0a60fdd0970afe4f8b82c27c9cd72c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Salazar=20Solano?= <112297389+salazarsebas@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:52:54 -0600 Subject: [PATCH] Revert "feat: Cougr integration in tetris and space invaders" --- examples/space_invaders/Cargo.lock | 2 - examples/space_invaders/README.md | 285 ++++++++++----- examples/space_invaders/src/components.rs | 33 -- examples/space_invaders/src/game_state.rs | 268 ++++++++++++++- examples/space_invaders/src/lib.rs | 392 +++++++++++++++++---- examples/space_invaders/src/systems.rs | 400 ---------------------- examples/space_invaders/src/test.rs | 13 - examples/tetris/Cargo.lock | 240 ++++++------- examples/tetris/Cargo.toml | 2 +- examples/tetris/README.md | 221 ++++++++---- examples/tetris/src/lib.rs | 341 +++++++----------- 11 files changed, 1181 insertions(+), 1016 deletions(-) delete mode 100644 examples/space_invaders/src/components.rs delete mode 100644 examples/space_invaders/src/systems.rs diff --git a/examples/space_invaders/Cargo.lock b/examples/space_invaders/Cargo.lock index 85ad6a4..7dc34f4 100644 --- a/examples/space_invaders/Cargo.lock +++ b/examples/space_invaders/Cargo.lock @@ -278,8 +278,6 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cougr-core" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee5f72b6d594ecb5ea5d42340cced45bbf8e13bf1506e48209eab64a4d7a931d" dependencies = [ "soroban-sdk", "wee_alloc", diff --git a/examples/space_invaders/README.md b/examples/space_invaders/README.md index 0263460..028a787 100644 --- a/examples/space_invaders/README.md +++ b/examples/space_invaders/README.md @@ -1,114 +1,245 @@ -# Space Invaders โ€” On-Chain Game Example +# ๐ŸŽฎ Space Invaders - On-Chain Game Example -A Space Invaders game as a Soroban smart contract using **cougr-core** ECS. +[![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) +[![Stellar](https://img.shields.io/badge/Stellar-Testnet-blue)](https://stellar.org) -## Purpose and pattern +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. -This example demonstrates entity-centric gameplay on Soroban: +## ๐Ÿš€ Live Deployment -- **All gameplay objects** (ship, invaders, bullets) are entities in a persisted `SimpleWorld` -- **Components** use cougr-core `Position`, `Velocity`, and `Health`, plus local marker and type components -- **Systems** in `systems.rs` use `SimpleQueryBuilder` to scan entities by marker and update components each tick -- **Meta state** (`score`, `tick`, `cooldown`, `game_over`) lives in a small `GameState` struct for cheap reads +| Network | Contract ID | Status | +|---------|-------------|--------| +| **Testnet** | [`CD6EUPL7Z255BTDPOCMQVWQ7CNM4ORP7QEFPPHO6JC63HRGLW6PYQAG7`](https://stellar.expert/explorer/testnet/contract/CD6EUPL7Z255BTDPOCMQVWQ7CNM4ORP7QEFPPHO6JC63HRGLW6PYQAG7) | ๐ŸŸข Active | -For the recommended `GameApp` + staged schedule pattern, see [`snake`](../snake). +> ๐Ÿ”— **Explorer**: [View on Stellar Expert](https://stellar.expert/explorer/testnet/contract/CD6EUPL7Z255BTDPOCMQVWQ7CNM4ORP7QEFPPHO6JC63HRGLW6PYQAG7) -## Public contract API +--- + +## ๐Ÿ“‹ Overview + +This example demonstrates how to build on-chain game logic on the Stellar blockchain using **cougr-core's ECS architecture**. The game focuses exclusively on smart contract logic (no graphical interface) and includes: + +| Feature | Description | +|---------|-------------| +| ๐Ÿš€ **Ship Control** | Left/right movement with bounds checking | +| ๐Ÿ‘พ **Invader Grid** | 4ร—8 formation with wave-based movement | +| ๐Ÿ’ฅ **Bullet System** | Player and enemy projectiles with velocity | +| ๐ŸŽฏ **Collision Detection** | Position-based hit detection | +| โค๏ธ **Health System** | Lives tracking using Health components | +| ๐Ÿ† **Scoring** | Point-based scoring by invader type | + +--- + +## ๐Ÿ”ง Why Cougr-Core? + +**Cougr-Core** provides an ECS (Entity-Component-System) architecture specifically designed for Soroban smart contracts. Here's how it benefits this project: + +### Benefits of Using Cougr-Core + +| Benefit | Description | Example in This Project | +|---------|-------------|------------------------| +| **Modular Components** | Reusable data structures attached to entities | `EntityPosition`, `Velocity`, `Health` used by Ship, Invaders, and Bullets | +| **Separation of Concerns** | Logic (Systems) separated from data (Components) | Movement System updates all entities with Velocity | +| **Type Safety** | Rust's type system prevents component misuse | `CougrPosition` ensures consistent coordinate handling | +| **WASM Optimization** | ECS optimizes memory access patterns for WASM | Efficient iteration over entity components | +| **Scalability** | Easy to add new features without refactoring | Adding new entity types only requires new components | +| **On-Chain Ready** | Designed for blockchain state persistence | Components serialize to Soroban storage | + +### ECS Architecture in Practice + +```rust +// Using cougr-core's Position component +use cougr_core::Position as CougrPosition; + +// Entity with Position, Velocity, and Health components +pub struct Bullet { + pub position: EntityPosition, // Where the bullet is + pub velocity: Velocity, // How it moves + pub active: bool, // Entity state +} + +// Movement System: Apply velocity to position +impl Bullet { + pub fn update(&mut self) { + self.velocity.apply_to(&mut self.position); + } +} +``` + +### Cougr-Core vs Traditional Approach + +| Aspect | Traditional | With Cougr-Core | +|--------|-------------|-----------------| +| Entity Data | Scattered structs | Unified Component pattern | +| Position Tracking | Manual x/y fields | `EntityPosition` + `CougrPosition` | +| Movement Logic | Per-entity methods | Velocity component + System | +| Health Management | Ad-hoc fields | `Health` component with damage API | +| Entity Creation | Manual construction | `SimpleWorld::spawn_entity()` + components | + +--- + +## ๐Ÿ—๏ธ Quick Start + +### Prerequisites + +| Tool | Version | Installation | +|------|---------|--------------| +| Rust | 1.70.0+ | [rustup.rs](https://rustup.rs) | +| Stellar CLI | Latest | [Stellar Docs](https://developers.stellar.org/docs/tools/cli) | +| WASM Target | - | `rustup target add wasm32v1-none` | + +### Build + +```bash +# Standard Rust build +cargo build + +# Build WASM for Soroban deployment +stellar contract build +``` + +### Test + +```bash +cargo test +``` + +**Test Results**: 13 tests passing โœ… + +| Test | Description | +|------|-------------| +| `test_init_game` | Game initializes with correct defaults | +| `test_move_ship_left/right` | Ship movement works correctly | +| `test_move_ship_left/right_bounds` | Ship respects boundaries | +| `test_shoot` | Shooting creates bullets | +| `test_shoot_cooldown` | Cooldown prevents rapid fire | +| `test_shoot_after_cooldown` | Shooting works after cooldown | +| `test_update_tick` | Game loop advances correctly | +| `test_score_increase` | Score increases on hits | +| `test_invader_destruction` | Invaders can be destroyed | +| `test_game_over_no_lives` | Game ends when lives = 0 | +| `test_no_move_when_game_over` | No actions after game over | + +--- + +## ๐Ÿ“– Contract API + +### Core Functions | Function | Parameters | Returns | Description | |----------|------------|---------|-------------| -| `init_game` | โ€” | โ€” | Spawn ship + invader grid in `SimpleWorld` | +| `init_game` | - | - | Initialize new game with ECS World | | `move_ship` | `direction: i32` | `i32` | Move ship (-1=left, 1=right) | -| `shoot` | โ€” | `bool` | Spawn player bullet entity | -| `update_tick` | โ€” | `bool` | Run movement, collision, and wave systems | -| `get_score` | โ€” | `u32` | Current score | -| `get_lives` | โ€” | `u32` | Ship `Health` component | -| `get_ship_position` | โ€” | `i32` | Ship `Position.x` | -| `check_game_over` | โ€” | `bool` | Game over flag | -| `get_active_invaders` | โ€” | `u32` | Invaders with `Health > 0` | -| `get_entity_count` | โ€” | `u32` | Total entities in world | +| `shoot` | - | `bool` | Fire bullet (true if successful) | +| `update_tick` | - | `bool` | Advance game (true if running) | -## Architecture overview +### Query Functions -``` -init_game - โ””โ”€ SimpleWorld: spawn ship (Position, Health, ShipMarker) - spawn 32 invaders (Position, Health, InvaderType, InvaderMarker) - -update_tick - โ”œโ”€ Movement: query bullets by marker โ†’ apply Velocity to Position - โ”œโ”€ Collision: query player bullets ร— invaders โ†’ decrement Health, despawn bullets - โ”œโ”€ Collision: query enemy bullets ร— ship โ†’ decrement ship Health - โ”œโ”€ Invader wave: query invaders โ†’ shift Position, reverse on bounds - โ””โ”€ Enemy fire: spawn EnemyBulletMarker entities with Velocity - -Storage: DataKey::World (SimpleWorld) + DataKey::State (meta) -``` +| Function | Returns | Description | +|----------|---------|-------------| +| `get_score` | `u32` | Current player score | +| `get_lives` | `u32` | Remaining lives | +| `get_ship_position` | `i32` | Ship X coordinate | +| `check_game_over` | `bool` | Game over status | +| `get_active_invaders` | `u32` | Remaining invader count | +| `get_entity_count` | `u32` | Cougr-core entity count | + +--- + +## ๐ŸŽฎ Game Mechanics -## Storage model +### Invaders -| Key | Type | Contents | -|-----|------|----------| -| `State` (instance) | `GameState` | Score, tick, direction, cooldown, game over, ship entity id | -| `World` (instance) | `SimpleWorld` | Ship, invaders, bullets and all components | -| `Initialized` (instance) | `bool` | Init flag | +| Type | Position | Points | Health | +|------|----------|--------|--------| +| ๐Ÿฆ‘ Squid | Top row | 30 pts | 1 HP | +| ๐Ÿฆ€ Crab | Middle rows | 20 pts | 1 HP | +| ๐Ÿ™ Octopus | Bottom row | 10 pts | 1 HP | -Gameplay positions and health are **only** in the world โ€” not duplicated in parallel vectors. +**Behavior**: +- Move horizontally in formation +- Descend when reaching screen edge +- Game over if they reach player's row -## Main gameplay flow +### Player Ship -1. `init_game` โ€” 33 entities (1 ship + 32 invaders), no bullets -2. Player `move_ship` / `shoot` โ€” update ship `Position` or spawn bullet entity -3. Each `update_tick`: - - Move bullet entities via `Velocity` - - Resolve bulletโ€“invader and bulletโ€“ship hits via position overlap - - Every 5 ticks: move invader formation; reverse at edges - - Every 7 ticks: one active invader fires -4. Win when all invaders destroyed; lose when ship health reaches 0 or invaders reach the player row +| Property | Value | +|----------|-------| +| Starting Lives | 3 | +| Position | Center of game board | +| Movement | Left/Right within bounds | +| Shoot Cooldown | 3 ticks | -## Cougr APIs used +### Game Constants -| API | Why | -|-----|-----| -| `SimpleWorld` | Central store for ship, invaders, and bullets | -| `Position`, `Velocity`, `Health` | Standard cougr-core gameplay components | -| `impl_component!` / `impl_marker_component!` | Invader type + entity role markers | -| `SimpleQueryBuilder` | Scan bullets and invaders by sparse marker each tick | -| `set_typed` / `get_typed` | Component read/write on entities | -| `RuntimeWorld::entity_count` | Exposed via `get_entity_count` | +| Constant | Value | Description | +|----------|-------|-------------| +| `GAME_WIDTH` | 40 | Board width | +| `GAME_HEIGHT` | 30 | Board height | +| `INVADER_COLS` | 8 | Invaders per row | +| `INVADER_ROWS` | 4 | Invader rows | +| `BULLET_SPEED` | 2 | Positions per tick | -Not used here: `GameApp` (systems are plain functions called from `update_tick`). The query + component pattern matches Cougrโ€™s recommended data model. +--- -## Build and test +## ๐ŸŒ Deploy to Testnet + +### 1. Setup Identity ```bash -cd examples/space_invaders -cargo test +# Generate a new identity +stellar keys generate --global deployer --network testnet + +# Fund the account +stellar keys address deployer | xargs -I {} curl "https://friendbot.stellar.org?addr={}" +``` + +### 2. Build & Deploy + +```bash +# Build WASM stellar contract build + +# Deploy to testnet +stellar contract deploy \ + --wasm target/wasm32v1-none/release/space_invaders.wasm \ + --source deployer \ + --network testnet ``` -**Tests**: 14 passing, including `test_world_entity_count` for Cougr integration. +### 3. Initialize & Play + +```bash +# Set your contract ID +CONTRACT_ID="your_contract_id_here" + +# Initialize game +stellar contract invoke --id $CONTRACT_ID --source deployer --network testnet -- init_game -## Known limitations +# 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 +``` -- Systems run sequentially in `update_tick` rather than through `GameApp` stages -- Collision uses grid tolerance, not pixel physics -- Enemy shooting selects invaders by spawn-order index, not spatial AI +--- -## Project structure +## ๐Ÿ“ Project Structure ``` examples/space_invaders/ -โ”œโ”€โ”€ Cargo.toml -โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ Cargo.toml # Dependencies including cougr-core +โ”œโ”€โ”€ README.md # This documentation โ””โ”€โ”€ src/ - โ”œโ”€โ”€ lib.rs # Contract entrypoints - โ”œโ”€โ”€ components.rs # Marker and type components - โ”œโ”€โ”€ game_state.rs # Meta state and constants - โ”œโ”€โ”€ systems.rs # Query-driven movement and collision - โ””โ”€โ”€ test.rs # Unit tests + โ”œโ”€โ”€ lib.rs # Contract entry points & ECS systems + โ”œโ”€โ”€ game_state.rs # ECS Components (Position, Velocity, Health) + โ””โ”€โ”€ test.rs # Unit tests (13 tests) ``` -## License +--- + +## ๐Ÿ“„ License MIT OR Apache-2.0 diff --git a/examples/space_invaders/src/components.rs b/examples/space_invaders/src/components.rs deleted file mode 100644 index cce9285..0000000 --- a/examples/space_invaders/src/components.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Cougr components for Space Invaders entities. - -use cougr_core::{impl_component, impl_marker_component}; -use soroban_sdk::contracttype; - -/// Marks the player ship entity. -pub struct ShipMarker; - -impl_marker_component!(ShipMarker, "ship", Sparse); - -/// Marks an invader entity. -pub struct InvaderMarker; - -impl_marker_component!(InvaderMarker, "invader", Sparse); - -/// Marks a player-fired bullet entity. -pub struct PlayerBulletMarker; - -impl_marker_component!(PlayerBulletMarker, "p_bull", Sparse); - -/// Marks an enemy-fired bullet entity. -pub struct EnemyBulletMarker; - -impl_marker_component!(EnemyBulletMarker, "e_bull", Sparse); - -/// Invader type encoded as u32 (matches `InvaderType` enum). -#[contracttype] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct InvaderTypeComponent { - pub invader_type: u32, -} - -impl_component!(InvaderTypeComponent, "inv_type", Table, { invader_type: u32 }); diff --git a/examples/space_invaders/src/game_state.rs b/examples/space_invaders/src/game_state.rs index 03ca155..4eecd28 100644 --- a/examples/space_invaders/src/game_state.rs +++ b/examples/space_invaders/src/game_state.rs @@ -1,7 +1,17 @@ -//! Meta game state and domain types for Space Invaders. +//! Game state structures for Space Invaders +//! +//! This module defines all the data structures needed to represent +//! the game state on-chain using Soroban's storage. +//! +//! **Cougr-Core Integration**: This module demonstrates how to use +//! cougr-core's ECS components for game entity data management. use soroban_sdk::contracttype; +// Import cougr-core Position component for entity position tracking +// This demonstrates proper integration of cougr-core into game logic +pub use cougr_core::Position as CougrPosition; + /// Direction for ship movement #[contracttype] #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -16,12 +26,16 @@ pub enum Direction { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u32)] pub enum InvaderType { + /// Top row invaders - 30 points Squid = 0, + /// Middle row invaders - 20 points Crab = 1, + /// Bottom row invaders - 10 points Octopus = 2, } impl InvaderType { + /// Get points for destroying this invader type pub fn points(&self) -> u32 { match self { InvaderType::Squid => 30, @@ -29,66 +43,290 @@ impl InvaderType { InvaderType::Octopus => 10, } } +} + +/// Entity position component - wraps cougr-core's Position +/// This demonstrates the ECS component pattern from cougr-core +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EntityPosition { + /// X coordinate on the game grid + pub x: i32, + /// Y coordinate on the game grid + pub y: i32, +} + +impl EntityPosition { + pub fn new(x: i32, y: i32) -> Self { + Self { x, y } + } + + /// Convert to cougr-core Position for ECS integration + pub fn to_cougr_position(&self) -> CougrPosition { + CougrPosition { + x: self.x, + y: self.y, + } + } + + /// Create from cougr-core Position + pub fn from_cougr_position(pos: &CougrPosition) -> Self { + Self { x: pos.x, y: pos.y } + } +} + +/// Velocity component for moving entities +/// Follows cougr-core's ECS component pattern +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Velocity { + /// Horizontal velocity + pub dx: i32, + /// Vertical velocity + pub dy: i32, +} + +impl Velocity { + pub fn new(dx: i32, dy: i32) -> Self { + Self { dx, dy } + } + + /// Apply velocity to a position (movement system pattern) + pub fn apply_to(&self, pos: &mut EntityPosition) { + pos.x += self.dx; + pos.y += self.dy; + } +} + +/// Health component for entities that can be damaged +/// Follows cougr-core's ECS component pattern +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Health { + /// Current health points + pub current: u32, + /// Maximum health points + pub max: u32, +} + +impl Health { + pub fn new(max: u32) -> Self { + Self { current: max, max } + } + + pub fn take_damage(&mut self, amount: u32) { + if self.current > amount { + self.current -= amount; + } else { + self.current = 0; + } + } + + pub fn is_alive(&self) -> bool { + self.current > 0 + } +} + +/// Represents a single invader in the grid +/// Uses ECS component pattern with Position and Health +#[contracttype] +#[derive(Clone, Debug)] +pub struct Invader { + /// Position component (cougr-core pattern) + pub position: EntityPosition, + /// Type of invader + pub invader_type: InvaderType, + /// Health component (cougr-core pattern) + pub health: Health, + /// Whether the invader is still alive + pub active: bool, +} + +impl Invader { + pub fn new(x: i32, y: i32, invader_type: InvaderType) -> Self { + Self { + position: EntityPosition::new(x, y), + invader_type, + health: Health::new(1), + active: true, + } + } + + /// Get X position (convenience accessor) + pub fn x(&self) -> i32 { + self.position.x + } + + /// Get Y position (convenience accessor) + pub fn y(&self) -> i32 { + self.position.y + } +} + +/// Represents a bullet (player or enemy) +/// Uses ECS component pattern with Position and Velocity +#[contracttype] +#[derive(Clone, Debug)] +pub struct Bullet { + /// Position component (cougr-core pattern) + pub position: EntityPosition, + /// Velocity component (cougr-core pattern) + pub velocity: Velocity, + /// Whether the bullet is still active + pub active: bool, +} + +impl Bullet { + pub fn new(x: i32, y: i32, direction: i32) -> Self { + Self { + position: EntityPosition::new(x, y), + velocity: Velocity::new(0, direction * BULLET_SPEED), + active: true, + } + } + + /// Create a player bullet (moves up) + pub fn player_bullet(x: i32, y: i32) -> Self { + Self::new(x, y, -1) + } + + /// Create an enemy bullet (moves down) + pub fn enemy_bullet(x: i32, y: i32) -> Self { + Self::new(x, y, 1) + } - pub fn as_u32(self) -> u32 { - self as u32 + /// Update bullet position using velocity component + pub fn update(&mut self) { + self.velocity.apply_to(&mut self.position); } - pub fn from_u32(value: u32) -> Self { - match value { - 0 => InvaderType::Squid, - 1 => InvaderType::Crab, - _ => InvaderType::Octopus, + /// Get X position (convenience accessor) + pub fn x(&self) -> i32 { + self.position.x + } + + /// Get Y position (convenience accessor) + pub fn y(&self) -> i32 { + self.position.y + } +} + +/// Player ship entity with ECS components +#[contracttype] +#[derive(Clone, Debug)] +pub struct Ship { + /// Position component (cougr-core pattern) + pub position: EntityPosition, + /// Health component (lives) + pub health: Health, +} + +impl Ship { + pub fn new(x: i32, y: i32, lives: u32) -> Self { + Self { + position: EntityPosition::new(x, y), + health: Health::new(lives), } } } -/// Non-ECS meta state persisted alongside the Cougr world. +/// Main game state structure #[contracttype] #[derive(Clone, Debug)] pub struct GameState { + /// Player's ship entity with components + pub ship: Ship, + /// Player's current score pub score: u32, + /// Whether the game is over pub game_over: bool, + /// Current invader movement direction (1 = right, -1 = left) pub invader_direction: i32, + /// Current game tick (for pacing) pub tick: u32, + /// Cooldown for player shooting (ticks until can shoot again) pub shoot_cooldown: u32, - pub ship_entity_id: u32, } impl GameState { - pub fn new(ship_entity_id: u32) -> Self { + /// Create a new game state with default values + pub fn new() -> Self { Self { + ship: Ship::new(GAME_WIDTH / 2, SHIP_Y, 3), score: 0, game_over: false, invader_direction: 1, tick: 0, shoot_cooldown: 0, - ship_entity_id, + } + } + + /// Get ship X position (backwards compatibility) + pub fn ship_x(&self) -> i32 { + self.ship.position.x + } + + /// Set ship X position (backwards compatibility) + pub fn set_ship_x(&mut self, x: i32) { + self.ship.position.x = x; + } + + /// Get remaining lives + pub fn lives(&self) -> u32 { + self.ship.health.current + } + + /// Take damage (lose a life) + pub fn take_damage(&mut self) { + self.ship.health.take_damage(1); + if !self.ship.health.is_alive() { + self.game_over = true; } } } impl Default for GameState { fn default() -> Self { - Self::new(0) + Self::new() } } +// Game constants +/// Width of the game board pub const GAME_WIDTH: i32 = 40; +/// Height of the game board pub const GAME_HEIGHT: i32 = 30; +/// Number of invader columns pub const INVADER_COLS: u32 = 8; +/// Number of invader rows pub const INVADER_ROWS: u32 = 4; +/// Ship's Y position (fixed at bottom) pub const SHIP_Y: i32 = GAME_HEIGHT - 2; +/// Y position where invaders cause game over pub const INVADER_WIN_Y: i32 = SHIP_Y - 2; +/// Shoot cooldown in ticks pub const SHOOT_COOLDOWN: u32 = 3; +/// Bullet speed (positions per tick) pub const BULLET_SPEED: i32 = 2; +/// Invader movement speed (ticks between moves) pub const INVADER_MOVE_INTERVAL: u32 = 5; -/// Storage keys for Soroban instance storage. +/// Storage keys for Soroban persistent storage #[contracttype] #[derive(Clone)] pub enum DataKey { + /// Main game state State, - World, + /// List of invaders + Invaders, + /// List of player bullets + PlayerBullets, + /// List of enemy bullets + EnemyBullets, + /// Flag indicating if game has been initialized Initialized, + /// Count of cougr-core entities (demonstrates ECS integration) + EntityCount, + /// Cougr-core World state (serialized) + WorldState, } diff --git a/examples/space_invaders/src/lib.rs b/examples/space_invaders/src/lib.rs index 20fe2a7..54f5982 100644 --- a/examples/space_invaders/src/lib.rs +++ b/examples/space_invaders/src/lib.rs @@ -1,166 +1,430 @@ -//! Space Invaders - On-Chain Game Using Cougr +//! Space Invaders - On-Chain Game Using Cougr-Core //! -//! Gameplay entities (ship, invaders, bullets) live in a persisted `SimpleWorld`. -//! Meta state (score, tick, cooldown) is stored separately for cheap reads. +//! This smart contract implements Space Invaders game logic on the Stellar blockchain +//! using the cougr-core ECS framework. It demonstrates how to build on-chain games +//! with efficient state management using Entity-Component-System architecture. +//! +//! # Cougr-Core Integration +//! This example showcases the use of cougr-core's ECS pattern: +//! - **World**: Central container for all game entities and components +//! - **Entity**: Game objects (ship, invaders, bullets) with unique IDs +//! - **Component**: Data attached to entities (Position, Velocity, Health) +//! - **Position**: cougr-core's Position component for entity location +//! - **Event**: Game events (Collision, Damage, Score) +//! +//! # Benefits of Using Cougr-Core +//! - **Modular Design**: Components can be reused across different entity types +//! - **Efficient Data Layout**: ECS optimizes memory access patterns for WASM +//! - **Scalability**: Easy to add new components and systems without refactoring +//! - **Type Safety**: Rust's type system prevents component misuse +//! +//! # Contract Functions +//! - `init_game`: Initialize a new game using cougr-core World +//! - `move_ship`: Move the player's ship left or right +//! - `shoot`: Fire a bullet from the player's ship +//! - `update_tick`: Advance the game by one tick (main game loop) +//! - `get_score`: Get the current score +//! - `get_lives`: Get remaining lives +//! - `get_ship_position`: Get the ship's X position +//! - `check_game_over`: Check if the game is over #![no_std] -mod components; mod game_state; -mod systems; #[cfg(test)] mod test; use crate::game_state::*; -use cougr_core::{RuntimeWorld, SimpleWorld}; -use soroban_sdk::{contract, contractimpl, Env}; +use soroban_sdk::{contract, contractimpl, Env, Vec}; -pub use game_state::{Direction, InvaderType, GAME_HEIGHT, GAME_WIDTH, INVADER_COLS, INVADER_ROWS}; +// Import cougr-core ECS framework components +// These are actively used for entity management and position tracking +use cougr_core::{Position as CougrPosition, SimpleWorld}; + +// Re-export game state types for external use +pub use game_state::{ + Bullet, DataKey, Direction, EntityPosition, GameState, Health, Invader, InvaderType, Ship, + Velocity, GAME_HEIGHT, GAME_WIDTH, INVADER_COLS, INVADER_ROWS, +}; #[contract] pub struct SpaceInvadersContract; #[contractimpl] impl SpaceInvadersContract { - /// Initialize a new game with a Cougr `SimpleWorld` containing all entities. + /// Initialize a new game with cougr-core ECS World + /// + /// This function demonstrates full cougr-core integration by: + /// 1. Creating a new ECS World for entity management + /// 2. Spawning entities for ship, invaders with cougr-core + /// 3. Using cougr-core's Position component for location tracking + /// + /// The ECS World manages all game entities while Soroban storage + /// persists the game state on-chain. pub fn init_game(env: Env) { - let (world, ship_entity_id) = systems::init_world(&env); - let state = GameState::new(ship_entity_id); + // Create cougr-core ECS World for entity management + let mut world = SimpleWorld::new(&env); + + // Spawn player ship entity in ECS World + let _ship_entity = world.spawn_entity(); + + // Create initial game state with Ship component + let state = GameState::new(); + + // Convert ship position to cougr-core Position for ECS tracking + let _ship_cougr_pos: CougrPosition = state.ship.position.to_cougr_position(); env.storage().instance().set(&DataKey::State, &state); - env.storage().instance().set(&DataKey::World, &world); + + // Create invader grid using cougr-core entity system + let mut invaders = Vec::new(&env); + for row in 0..INVADER_ROWS { + let invader_type = match row { + 0 => InvaderType::Squid, + 1 | 2 => InvaderType::Crab, + _ => InvaderType::Octopus, + }; + + for col in 0..INVADER_COLS { + // Spawn invader entity in cougr-core World + let _invader_entity = world.spawn_entity(); + + let x = (col as i32 * 4) + 4; + let y = (row as i32 * 3) + 2; + let invader = Invader::new(x, y, invader_type); + + // Each invader's position can be converted to cougr-core Position + let _invader_cougr_pos: CougrPosition = invader.position.to_cougr_position(); + + invaders.push_back(invader); + } + } + env.storage().instance().set(&DataKey::Invaders, &invaders); + + // Store ECS world entity count (1 ship + 32 invaders) + env.storage() + .instance() + .set(&DataKey::EntityCount, &(world.next_entity_id() - 1)); + + // Initialize empty bullet lists + let player_bullets: Vec = Vec::new(&env); + let enemy_bullets: Vec = Vec::new(&env); + env.storage() + .instance() + .set(&DataKey::PlayerBullets, &player_bullets); + env.storage() + .instance() + .set(&DataKey::EnemyBullets, &enemy_bullets); + + // Mark as initialized env.storage().instance().set(&DataKey::Initialized, &true); } - /// Move the player's ship left or right. + /// Move the player's ship left or right + /// + /// Uses EntityPosition component for tracking ship location, + /// following cougr-core's component-based design. + /// + /// # Arguments + /// * `direction` - -1 for left, 1 for right + /// + /// # Returns + /// The new ship X position pub fn move_ship(env: Env, direction: i32) -> i32 { - let state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); - let mut world: SimpleWorld = env.storage().instance().get(&DataKey::World).unwrap(); + let mut state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); if state.game_over { - return systems::ship_x(&world, &env, state.ship_entity_id); + return state.ship_x(); } - let new_x = systems::ship_x(&world, &env, state.ship_entity_id) + direction; + // Update ship's EntityPosition component + let new_x = state.ship_x() + direction; if (1..GAME_WIDTH - 1).contains(&new_x) { - systems::set_ship_x(&mut world, &env, state.ship_entity_id, new_x); - env.storage().instance().set(&DataKey::World, &world); + state.set_ship_x(new_x); + env.storage().instance().set(&DataKey::State, &state); } - systems::ship_x(&world, &env, state.ship_entity_id) + state.ship_x() } - /// Fire a bullet from the player's ship. + /// Fire a bullet from the player's ship + /// + /// Creates a new Bullet entity with Position and Velocity components, + /// demonstrating cougr-core's entity spawning pattern. + /// + /// # Returns + /// `true` if bullet was fired, `false` if on cooldown or game over pub fn shoot(env: Env) -> bool { let mut state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); - let mut world: SimpleWorld = env.storage().instance().get(&DataKey::World).unwrap(); if state.game_over || state.shoot_cooldown > 0 { return false; } - systems::spawn_player_bullet(&mut world, &env, state.ship_entity_id); - state.shoot_cooldown = SHOOT_COOLDOWN; + // Create new bullet with Position and Velocity components + // Following cougr-core ECS pattern for entity creation + let bullet = Bullet::player_bullet(state.ship_x(), SHIP_Y - 1); + + let mut player_bullets: Vec = env + .storage() + .instance() + .get(&DataKey::PlayerBullets) + .unwrap(); + player_bullets.push_back(bullet); + env.storage() + .instance() + .set(&DataKey::PlayerBullets, &player_bullets); - env.storage().instance().set(&DataKey::World, &world); + // Set cooldown + state.shoot_cooldown = SHOOT_COOLDOWN; env.storage().instance().set(&DataKey::State, &state); + true } - /// Advance the game by one tick using Cougr queries and component updates. + /// Advance the game by one tick - main game loop using ECS systems + /// + /// This function demonstrates cougr-core system patterns: + /// - Movement System: Updates entity positions using Velocity components + /// - Collision System: Detects overlapping entities using Position + /// - Health System: Manages damage using Health components + /// + /// # Returns + /// `true` if the game is still running, `false` if game over pub fn update_tick(env: Env) -> bool { let mut state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); - let mut world: SimpleWorld = env.storage().instance().get(&DataKey::World).unwrap(); if state.game_over { return false; } state.tick += 1; + + // Reduce shoot cooldown if state.shoot_cooldown > 0 { state.shoot_cooldown -= 1; } - systems::move_player_bullets(&mut world, &env); - systems::move_enemy_bullets(&mut world, &env); + // === MOVEMENT SYSTEM === + // Update entity positions using Velocity components (cougr-core pattern) - state.score += systems::resolve_player_bullet_hits(&mut world, &env); + // Move player bullets using their Velocity component + let player_bullets: Vec = env + .storage() + .instance() + .get(&DataKey::PlayerBullets) + .unwrap(); + let mut new_player_bullets = Vec::new(&env); + + for i in 0..player_bullets.len() { + let mut bullet = player_bullets.get(i).unwrap(); + // Apply Velocity to Position (ECS movement system) + bullet.update(); + + // Keep bullet if still on screen + if bullet.y() > 0 && bullet.active { + new_player_bullets.push_back(bullet); + } + } - if systems::resolve_enemy_bullet_hits(&mut world, &env, state.ship_entity_id) { - state.game_over = true; + // Move enemy bullets using their Velocity component + let enemy_bullets: Vec = env + .storage() + .instance() + .get(&DataKey::EnemyBullets) + .unwrap(); + let mut new_enemy_bullets = Vec::new(&env); + + for i in 0..enemy_bullets.len() { + let mut bullet = enemy_bullets.get(i).unwrap(); + // Apply Velocity to Position (ECS movement system) + bullet.update(); + + // Keep bullet if still on screen + if bullet.y() < GAME_HEIGHT && bullet.active { + new_enemy_bullets.push_back(bullet); + } + } + + // Load invaders + let mut invaders: Vec = env.storage().instance().get(&DataKey::Invaders).unwrap(); + + // === COLLISION SYSTEM === + // Detect entity overlaps using Position components (cougr-core pattern) + + // Check player bullet collisions with invaders + let mut updated_player_bullets = Vec::new(&env); + for i in 0..new_player_bullets.len() { + let bullet = new_player_bullets.get(i).unwrap(); + let mut hit = false; + + for j in 0..invaders.len() { + let mut invader = invaders.get(j).unwrap(); + if invader.active + && Self::check_collision(bullet.x(), bullet.y(), invader.x(), invader.y(), 2) + { + // Collision detected - update Health component + invader.health.take_damage(1); + invader.active = invader.health.is_alive(); + invaders.set(j, invader.clone()); + state.score += invader.invader_type.points(); + hit = true; + break; + } + } + + if !hit { + updated_player_bullets.push_back(bullet); + } + } + + // Check enemy bullet collisions with player + let mut updated_enemy_bullets = Vec::new(&env); + for i in 0..new_enemy_bullets.len() { + let bullet = new_enemy_bullets.get(i).unwrap(); + + if Self::check_collision(bullet.x(), bullet.y(), state.ship_x(), SHIP_Y, 2) { + // Player hit - update ship's Health component + state.take_damage(); + } else { + updated_enemy_bullets.push_back(bullet); + } } + // === INVADER MOVEMENT SYSTEM === + // Update invader Position components with wave pattern if state.tick.is_multiple_of(INVADER_MOVE_INTERVAL) { - let (should_reverse, should_descend) = - systems::invader_bounds_reached(&world, &env, state.invader_direction); - if systems::move_invaders( - &mut world, - &env, - state.invader_direction, - should_descend, - ) { - state.game_over = true; + let mut should_descend = false; + let mut should_reverse = false; + + // Check bounds using Position components + for i in 0..invaders.len() { + let invader = invaders.get(i).unwrap(); + if invader.active { + let new_x = invader.x() + state.invader_direction; + if new_x <= 0 || new_x >= GAME_WIDTH - 1 { + should_reverse = true; + should_descend = true; + break; + } + } + } + + // Update all invader Position components + for i in 0..invaders.len() { + let mut invader = invaders.get(i).unwrap(); + if invader.active { + if should_descend { + invader.position.y += 1; + } else { + invader.position.x += state.invader_direction; + } + + // Check game over condition + if invader.y() >= INVADER_WIN_Y { + state.game_over = true; + } + + invaders.set(i, invader); + } } + if should_reverse { state.invader_direction *= -1; } } + // === ENEMY SHOOTING SYSTEM === + // Spawn enemy bullet entities with Position and Velocity if state.tick.is_multiple_of(7) { - if let Some((x, y)) = systems::first_active_invader_for_shot(&world, &env, state.tick) { - systems::spawn_enemy_bullet(&mut world, &env, x, y); + for i in 0..invaders.len() { + let invader = invaders.get(i).unwrap(); + if invader.active && (state.tick / 7) % INVADER_COLS == i % INVADER_COLS { + // Create bullet with Position and Velocity components + let bullet = Bullet::enemy_bullet(invader.x(), invader.y() + 1); + updated_enemy_bullets.push_back(bullet); + break; + } + } + } + + // === WIN CONDITION CHECK === + let mut all_destroyed = true; + for i in 0..invaders.len() { + let invader = invaders.get(i).unwrap(); + if invader.active { + all_destroyed = false; + break; } } - if systems::active_invader_count(&world, &env) == 0 { + if all_destroyed { state.game_over = true; } + // === PERSIST STATE === env.storage().instance().set(&DataKey::State, &state); - env.storage().instance().set(&DataKey::World, &world); + env.storage().instance().set(&DataKey::Invaders, &invaders); + env.storage() + .instance() + .set(&DataKey::PlayerBullets, &updated_player_bullets); + env.storage() + .instance() + .set(&DataKey::EnemyBullets, &updated_enemy_bullets); + !state.game_over } + /// Get the current score pub fn get_score(env: Env) -> u32 { - env.storage() - .instance() - .get(&DataKey::State) - .map(|state: GameState| state.score) - .unwrap_or(0) + let state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); + state.score } + /// Get remaining lives from ship's Health component pub fn get_lives(env: Env) -> u32 { let state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); - let world: SimpleWorld = env.storage().instance().get(&DataKey::World).unwrap(); - systems::lives(&world, &env, state.ship_entity_id) + state.lives() } + /// Get the ship's X position from its Position component pub fn get_ship_position(env: Env) -> i32 { let state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); - let world: SimpleWorld = env.storage().instance().get(&DataKey::World).unwrap(); - systems::ship_x(&world, &env, state.ship_entity_id) + state.ship_x() } + /// Check if the game is over pub fn check_game_over(env: Env) -> bool { - env.storage() - .instance() - .get(&DataKey::State) - .map(|state: GameState| state.game_over) - .unwrap_or(true) + let state: GameState = env.storage().instance().get(&DataKey::State).unwrap(); + state.game_over } + /// Get the number of active invaders remaining pub fn get_active_invaders(env: Env) -> u32 { - let world: SimpleWorld = env.storage().instance().get(&DataKey::World).unwrap(); - systems::active_invader_count(&world, &env) + let invaders: Vec = env.storage().instance().get(&DataKey::Invaders).unwrap(); + + let mut count = 0u32; + for i in 0..invaders.len() { + let invader = invaders.get(i).unwrap(); + if invader.active { + count += 1; + } + } + count } - /// Returns the number of entities currently in the Cougr world. + /// Get the cougr-core entity count (demonstrates ECS integration) pub fn get_entity_count(env: Env) -> u32 { env.storage() .instance() - .get(&DataKey::World) - .map(|world: SimpleWorld| world.entity_count() as u32) + .get(&DataKey::EntityCount) .unwrap_or(0) } + + /// Helper function to check collision between two Position components + /// This follows cougr-core's collision detection pattern using positions + fn check_collision(x1: i32, y1: i32, x2: i32, y2: i32, tolerance: i32) -> bool { + (x1 - x2).abs() < tolerance && (y1 - y2).abs() < tolerance + } } diff --git a/examples/space_invaders/src/systems.rs b/examples/space_invaders/src/systems.rs deleted file mode 100644 index ef6e2dd..0000000 --- a/examples/space_invaders/src/systems.rs +++ /dev/null @@ -1,400 +0,0 @@ -//! Game systems operating on the Cougr `SimpleWorld`. - -use crate::components::{ - EnemyBulletMarker, InvaderMarker, InvaderTypeComponent, PlayerBulletMarker, ShipMarker, -}; -use crate::game_state::{ - InvaderType, BULLET_SPEED, GAME_HEIGHT, GAME_WIDTH, INVADER_COLS, INVADER_ROWS, - INVADER_WIN_Y, SHIP_Y, -}; -use cougr_core::component::{Health, Velocity}; -use cougr_core::{Position, SimpleQueryBuilder, SimpleWorld}; -use soroban_sdk::{symbol_short, Env, Vec}; - -pub fn init_world(env: &Env) -> (SimpleWorld, u32) { - let mut world = SimpleWorld::new(env); - - let ship_id = world.spawn_entity(); - world.set_typed(env, ship_id, &Position::new(GAME_WIDTH / 2, SHIP_Y)); - world.set_typed( - env, - ship_id, - &Health { - current: 3, - max: 3, - }, - ); - world.set_typed(env, ship_id, &ShipMarker); - - for row in 0..INVADER_ROWS { - let invader_type = match row { - 0 => InvaderType::Squid, - 1 | 2 => InvaderType::Crab, - _ => InvaderType::Octopus, - }; - - for col in 0..INVADER_COLS { - let entity_id = world.spawn_entity(); - let x = (col as i32 * 4) + 4; - let y = (row as i32 * 3) + 2; - - world.set_typed(env, entity_id, &Position::new(x, y)); - world.set_typed( - env, - entity_id, - &Health { - current: 1, - max: 1, - }, - ); - world.set_typed( - env, - entity_id, - &InvaderTypeComponent { - invader_type: invader_type.as_u32(), - }, - ); - world.set_typed(env, entity_id, &InvaderMarker); - } - } - - (world, ship_id) -} - -pub fn ship_x(world: &SimpleWorld, env: &Env, ship_entity_id: u32) -> i32 { - world - .get_typed::(env, ship_entity_id) - .map(|pos| pos.x) - .unwrap_or(GAME_WIDTH / 2) -} - -pub fn set_ship_x(world: &mut SimpleWorld, env: &Env, ship_entity_id: u32, x: i32) { - if let Some(pos) = world.get_typed::(env, ship_entity_id) { - world.set_typed(env, ship_entity_id, &Position::new(x, pos.y)); - } -} - -pub fn lives(world: &SimpleWorld, env: &Env, ship_entity_id: u32) -> u32 { - world - .get_typed::(env, ship_entity_id) - .map(|health| health.current as u32) - .unwrap_or(0) -} - -pub fn spawn_player_bullet(world: &mut SimpleWorld, env: &Env, ship_entity_id: u32) { - let ship_x = ship_x(world, env, ship_entity_id); - let entity_id = world.spawn_entity(); - world.set_typed(env, entity_id, &Position::new(ship_x, SHIP_Y - 1)); - world.set_typed( - env, - entity_id, - &Velocity::new(0, -BULLET_SPEED), - ); - world.set_typed(env, entity_id, &PlayerBulletMarker); -} - -pub fn spawn_enemy_bullet(world: &mut SimpleWorld, env: &Env, x: i32, y: i32) { - let entity_id = world.spawn_entity(); - world.set_typed(env, entity_id, &Position::new(x, y + 1)); - world.set_typed( - env, - entity_id, - &Velocity::new(0, BULLET_SPEED), - ); - world.set_typed(env, entity_id, &EnemyBulletMarker); -} - -pub fn move_player_bullets(world: &mut SimpleWorld, env: &Env) { - move_bullets(world, env, symbol_short!("p_bull"), true); -} - -pub fn move_enemy_bullets(world: &mut SimpleWorld, env: &Env) { - move_bullets(world, env, symbol_short!("e_bull"), false); -} - -fn move_bullets( - world: &mut SimpleWorld, - env: &Env, - marker: soroban_sdk::Symbol, - player_bullet: bool, -) { - let query = SimpleQueryBuilder::new(env) - .with_component(marker) - .include_sparse() - .build(); - let entities = query.execute(world, env); - - let mut to_despawn = Vec::new(env); - for i in 0..entities.len() { - let entity_id = entities.get(i).unwrap(); - if let (Some(mut pos), Some(vel)) = ( - world.get_typed::(env, entity_id), - world.get_typed::(env, entity_id), - ) { - pos.x += vel.x; - pos.y += vel.y; - let off_screen = if player_bullet { - pos.y <= 0 - } else { - pos.y >= GAME_HEIGHT - }; - if off_screen { - to_despawn.push_back(entity_id); - } else { - world.set_typed(env, entity_id, &pos); - } - } - } - - for i in 0..to_despawn.len() { - world.despawn_entity(to_despawn.get(i).unwrap()); - } -} - -pub fn move_invaders( - world: &mut SimpleWorld, - env: &Env, - direction: i32, - descend: bool, -) -> bool { - let query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("invader")) - .include_sparse() - .build(); - let entities = query.execute(world, env); - - let mut reached_win_line = false; - for i in 0..entities.len() { - let entity_id = entities.get(i).unwrap(); - if let Some(health) = world.get_typed::(env, entity_id) { - if health.current == 0 { - continue; - } - } else { - continue; - } - - if let Some(mut pos) = world.get_typed::(env, entity_id) { - if descend { - pos.y += 1; - } else { - pos.x += direction; - } - if pos.y >= INVADER_WIN_Y { - reached_win_line = true; - } - world.set_typed(env, entity_id, &pos); - } - } - - reached_win_line -} - -pub fn invader_bounds_reached(world: &SimpleWorld, env: &Env, direction: i32) -> (bool, bool) { - let query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("invader")) - .include_sparse() - .build(); - let entities = query.execute(world, env); - - let mut should_reverse = false; - let mut should_descend = false; - for i in 0..entities.len() { - let entity_id = entities.get(i).unwrap(); - if let Some(health) = world.get_typed::(env, entity_id) { - if health.current == 0 { - continue; - } - } else { - continue; - } - - if let Some(pos) = world.get_typed::(env, entity_id) { - let new_x = pos.x + direction; - if new_x <= 0 || new_x >= GAME_WIDTH - 1 { - should_reverse = true; - should_descend = true; - break; - } - } - } - - (should_reverse, should_descend) -} - -pub fn resolve_player_bullet_hits(world: &mut SimpleWorld, env: &Env) -> u32 { - let bullet_query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("p_bull")) - .include_sparse() - .build(); - let bullets = bullet_query.execute(world, env); - - let invader_query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("invader")) - .include_sparse() - .build(); - let invaders = invader_query.execute(world, env); - - let mut score = 0u32; - let mut bullets_to_remove = Vec::new(env); - - for i in 0..bullets.len() { - let bullet_id = bullets.get(i).unwrap(); - let bullet_pos = match world.get_typed::(env, bullet_id) { - Some(pos) => pos, - None => continue, - }; - - let mut hit = false; - for j in 0..invaders.len() { - let invader_id = invaders.get(j).unwrap(); - let health = match world.get_typed::(env, invader_id) { - Some(health) if health.current > 0 => health, - _ => continue, - }; - - let invader_pos = match world.get_typed::(env, invader_id) { - Some(pos) => pos, - None => continue, - }; - - if check_collision( - bullet_pos.x, - bullet_pos.y, - invader_pos.x, - invader_pos.y, - 2, - ) { - let mut updated_health = health; - if updated_health.current > 0 { - updated_health.current -= 1; - } - world.set_typed(env, invader_id, &updated_health); - - if let Some(invader_type) = - world.get_typed::(env, invader_id) - { - score += InvaderType::from_u32(invader_type.invader_type).points(); - } - - hit = true; - break; - } - } - - if hit { - bullets_to_remove.push_back(bullet_id); - } - } - - for i in 0..bullets_to_remove.len() { - world.despawn_entity(bullets_to_remove.get(i).unwrap()); - } - - score -} - -pub fn resolve_enemy_bullet_hits( - world: &mut SimpleWorld, - env: &Env, - ship_entity_id: u32, -) -> bool { - let bullet_query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("e_bull")) - .include_sparse() - .build(); - let bullets = bullet_query.execute(world, env); - - let ship_pos = match world.get_typed::(env, ship_entity_id) { - Some(pos) => pos, - None => return false, - }; - - let mut hit_ship = false; - let mut bullets_to_remove = Vec::new(env); - - for i in 0..bullets.len() { - let bullet_id = bullets.get(i).unwrap(); - let bullet_pos = match world.get_typed::(env, bullet_id) { - Some(pos) => pos, - None => continue, - }; - - if check_collision( - bullet_pos.x, - bullet_pos.y, - ship_pos.x, - ship_pos.y, - 2, - ) { - bullets_to_remove.push_back(bullet_id); - hit_ship = true; - } - } - - for i in 0..bullets_to_remove.len() { - world.despawn_entity(bullets_to_remove.get(i).unwrap()); - } - - if hit_ship { - if let Some(mut health) = world.get_typed::(env, ship_entity_id) { - if health.current > 0 { - health.current -= 1; - } - world.set_typed(env, ship_entity_id, &health); - return health.current == 0; - } - } - - false -} - -pub fn active_invader_count(world: &SimpleWorld, env: &Env) -> u32 { - let query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("invader")) - .include_sparse() - .build(); - let entities = query.execute(world, env); - - let mut count = 0u32; - for i in 0..entities.len() { - let entity_id = entities.get(i).unwrap(); - if let Some(health) = world.get_typed::(env, entity_id) { - if health.current > 0 { - count += 1; - } - } - } - count -} - -pub fn first_active_invader_for_shot(world: &SimpleWorld, env: &Env, tick: u32) -> Option<(i32, i32)> { - let query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("invader")) - .include_sparse() - .build(); - let entities = query.execute(world, env); - - let target_col = (tick / 7) % INVADER_COLS; - for i in 0..entities.len() { - if i % INVADER_COLS != target_col { - continue; - } - let entity_id = entities.get(i).unwrap(); - if let Some(health) = world.get_typed::(env, entity_id) { - if health.current == 0 { - continue; - } - } else { - continue; - } - if let Some(pos) = world.get_typed::(env, entity_id) { - return Some((pos.x, pos.y)); - } - } - None -} - -fn check_collision(x1: i32, y1: i32, x2: i32, y2: i32, tolerance: i32) -> bool { - (x1 - x2).abs() < tolerance && (y1 - y2).abs() < tolerance -} diff --git a/examples/space_invaders/src/test.rs b/examples/space_invaders/src/test.rs index ab9a59a..a4b3eed 100644 --- a/examples/space_invaders/src/test.rs +++ b/examples/space_invaders/src/test.rs @@ -254,16 +254,3 @@ fn test_no_move_when_game_over() { assert_eq!(pos, new_pos); } } - -/// Test that Cougr world is persisted with all gameplay entities -#[test] -fn test_world_entity_count() { - let env = Env::default(); - let contract_id = env.register(SpaceInvadersContract, ()); - let client = SpaceInvadersContractClient::new(&env, &contract_id); - - client.init_game(); - - // 1 ship + 32 invaders - assert_eq!(client.get_entity_count(), 1 + INVADER_COLS * INVADER_ROWS); -} diff --git a/examples/tetris/Cargo.lock b/examples/tetris/Cargo.lock index 03ac8ea..29796f8 100644 --- a/examples/tetris/Cargo.lock +++ b/examples/tetris/Cargo.lock @@ -196,9 +196,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -215,14 +215,14 @@ dependencies = [ "num-bigint", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "cc" -version = "1.2.54" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -248,14 +248,14 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -367,7 +367,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -382,12 +382,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -401,21 +401,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -426,18 +425,18 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.21.3", + "darling_core 0.23.0", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -458,9 +457,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -485,7 +484,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -603,9 +602,9 @@ checksum = "2bfcf67fea2815c2fc3b90873fae90957be12ff417335dfadc7f52927feb03b2" [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "ff" @@ -625,9 +624,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -696,9 +695,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heapless" @@ -742,9 +741,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -783,12 +782,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -810,15 +809,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -838,18 +837,18 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures", ] [[package]] name = "libc" -version = "0.2.180" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" @@ -871,14 +870,14 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memory_units" @@ -898,9 +897,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -910,7 +909,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -933,9 +932,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "p256" @@ -987,7 +986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1010,18 +1009,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -1064,7 +1063,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1117,9 +1116,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -1142,9 +1141,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1173,7 +1172,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1191,18 +1190,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.8.22", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -1211,14 +1210,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1234,9 +1233,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest", "keccak", @@ -1273,7 +1272,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1354,14 +1353,14 @@ dependencies = [ "serde", "serde_json", "stellar-xdr", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "soroban-ledger-snapshot" -version = "25.1.0" +version = "25.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c5285c83e7a5581879b7a65033eae53b24ac9689975aa6887f1d8ee3e941c9" +checksum = "2ca06e6c5029d1285e66219cb387a234224e26969ce8ad2bc2d5017e9395d63b" dependencies = [ "serde", "serde_json", @@ -1373,9 +1372,9 @@ dependencies = [ [[package]] name = "soroban-sdk" -version = "25.1.0" +version = "25.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1262aa83e99a0fb3e8cd56d6e5ca4c28ac4f9871ac7173f65301a8b9a12c20f" +checksum = "4502f2e018f238a4c5d3212d7d20ea6abcdc6e58babd63b642b693739db30fd1" dependencies = [ "arbitrary", "bytes-lit", @@ -1397,9 +1396,9 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" -version = "25.1.0" +version = "25.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b62c526917a1e77b6dce3cd841b6c271f0fff344ea93ad92a8c45afe8051b6" +checksum = "ca03e9cf61d241cb9afdd6ddf41f6c25698b3f566a875e7009ea799b89e2bf0a" dependencies = [ "darling 0.20.11", "heck", @@ -1412,16 +1411,17 @@ dependencies = [ "soroban-spec", "soroban-spec-rust", "stellar-xdr", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "soroban-spec" -version = "25.1.0" +version = "25.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0186c943a78de7038ce7eee478f521f7a7665440101ae0d24b4a59833fb6d833" +checksum = "aa02e07f507cc27406ae0834db4dcf309b78c4cc8776eb3b2d662d66e8859d25" dependencies = [ "base64", + "sha2", "stellar-xdr", "thiserror", "wasmparser", @@ -1429,9 +1429,9 @@ dependencies = [ [[package]] name = "soroban-spec-rust" -version = "25.1.0" +version = "25.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a948196ed0633be3a4125e0c7a4fc0bb6337942e538813b1f171331738f9058" +checksum = "6835bb510763ef3fa5405e89036e3c8ea6ef5abe55fc52cfe9ac0e38be9d531c" dependencies = [ "prettyplease", "proc-macro2", @@ -1439,7 +1439,7 @@ dependencies = [ "sha2", "soroban-spec", "stellar-xdr", - "syn 2.0.114", + "syn 2.0.117", "thiserror", ] @@ -1549,9 +1549,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1583,14 +1583,14 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -1609,9 +1609,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -1619,15 +1619,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "version_check" @@ -1643,7 +1643,7 @@ checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1654,9 +1654,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -1667,9 +1667,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1677,22 +1677,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -1721,7 +1721,7 @@ version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a58e28b80dd8340cb07b8242ae654756161f6fc8d0038123d679b7b99964fa50" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "semver", ] @@ -1789,7 +1789,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1800,7 +1800,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1829,22 +1829,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1864,11 +1864,11 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/tetris/Cargo.toml b/examples/tetris/Cargo.toml index fa6a44a..3f9aa18 100644 --- a/examples/tetris/Cargo.toml +++ b/examples/tetris/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -soroban-sdk = "=25.1.0" +soroban-sdk = "25.1.0" cougr-core = "1.0.0" [dev-dependencies] diff --git a/examples/tetris/README.md b/examples/tetris/README.md index f4aa957..62d9283 100644 --- a/examples/tetris/README.md +++ b/examples/tetris/README.md @@ -1,103 +1,184 @@ # Tetris Smart Contract -An on-chain Tetris game using **cougr-core** on Stellar Soroban. +An on-chain Tetris game implementation using the Cougr-Core ECS framework on Stellar's Soroban platform. -> **Hybrid ECS example**: The locked board stays in a compact `GameState` struct; the active falling piece lives in a persisted `SimpleWorld`. For a full `GameApp` tick model, see [`snake`](../snake). +## ๐Ÿ“‹ Overview -## Purpose and pattern +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 -This example demonstrates a practical split when part of the game state is a dense grid and part is a small set of moving entities: +## ๐ŸŽฎ Game Features -- **Board, score, level** โ€” instance storage as `GameState` (bit-packed rows) -- **Active piece** โ€” one entity in `SimpleWorld` with `Position`, `TetrominoComponent`, and `ActivePieceMarker` -- **Queries** โ€” `SimpleQueryBuilder` locates the active piece before moves and locks +| Feature | Description | +|---------|-------------| +| **Game Board** | 20x10 grid with collision detection | +| **Tetrominoes** | All 7 classic shapes (I, J, L, O, S, T, Z) | +| **Rotation** | Full 360ยฐ rotation system | +| **Line Clearing** | Automatic detection and scoring | +| **Scoring** | Points based on lines cleared | +| **Leveling** | Difficulty increases every 10 lines | -The board is not modeled as 200 cell entities because that would be expensive on-chain. Cougr owns the piece lifecycle (spawn, move, rotate, despawn, respawn). +## ๐Ÿš€ Quick Start -## Public contract API +### Prerequisites -| Function | Parameters | Returns | Description | -|----------|------------|---------|-------------| -| `init_game` | โ€” | `GameState` | Create board, spawn active piece in `SimpleWorld` | -| `move_left` | โ€” | `bool` | Move active piece left | -| `move_right` | โ€” | `bool` | Move active piece right | -| `move_down` | โ€” | `bool` | Soft drop; locks piece if blocked | -| `rotate` | โ€” | `bool` | Rotate active piece clockwise | -| `drop` | โ€” | `u32` | Hard drop; returns cells dropped | -| `update_tick` | โ€” | `GameState` | Gravity tick | -| `get_state` | โ€” | `GameState` | Current board + piece (piece read from world) | -| `get_entity_count` | โ€” | `u32` | Entities in `SimpleWorld` (0 or 1 while playing) | +| Tool | Version | Installation | +|------|---------|-------------| +| Rust | 1.70.0+ | `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` | +| Stellar CLI | Latest | `cargo install --locked stellar-cli --features opt` | +| WASM Target | - | `rustup target add wasm32v1-none` | -## Architecture overview - -``` -Contract entrypoint - โ”‚ - โ”œโ”€ load GameState (board, score, next piece, meta) - โ”œโ”€ load SimpleWorld (active piece entity) - โ”‚ - โ”œโ”€ SimpleQuery โ†’ find entity with ActivePieceMarker - โ”œโ”€ read/update Position + TetrominoComponent - โ”œโ”€ collision against board rows - โ”‚ - โ””โ”€ save GameState + SimpleWorld -``` +### Build & Test +```bash +cd examples/tetris -On lock: piece coordinates are written into the board bitfield, the entity is despawned, and a new piece entity is spawned from `next_piece`. +# Build the contract +cargo build --release -## Storage model +# Run tests +cargo test -| Key | Type | Contents | -|-----|------|----------| -| `game` (instance) | `GameState` | Board rows, next piece preview, score, level, lines, game over | -| `world` (instance) | `SimpleWorld` | Active piece entity and components | +# Build for Soroban +stellar contract build +``` -Both are updated together after every move or tick. +## ๐Ÿ“ฆ Deployment -## Main gameplay flow +### Testnet Deployment +```bash +# Deploy to testnet +stellar contract deploy \ + --wasm target/wasm32v1-none/release/tetris.wasm \ + --source \ + --network testnet +``` -1. `init_game` โ€” empty board, spawn first piece entity, generate `next_piece` -2. Player calls `move_*`, `rotate`, or `drop` โ€” world components update after collision check -3. When the piece cannot move down, it locks: board updated, entity despawned, new entity spawned -4. Line clears update score/level in `GameState` -5. If the new piece collides immediately, set `game_over` +**Deployed Contract:** +- **Network**: Stellar Testnet +- **Contract ID**: `CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF` +- **Explorer**: `https://stellar.expert/explorer/testnet/contract/CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF` -## Cougr APIs used +### Invoke Functions +```bash +# Initialize a new game +stellar contract invoke \ + --id CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF \ + --source \ + --network testnet \ + -- init_game + +# Move piece left +stellar contract invoke \ + --id CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF \ + --source \ + --network testnet \ + -- move_left + +# Update game tick (gravity + line clearing) +stellar contract invoke \ + --id CBWENGWFZHPNJPIHQAHXE5K34BGV2G5MOQIQ24PE44M6P42YULMQZYSF \ + --source \ + --network testnet \ + -- update_tick +``` -| API | Why | -|-----|-----| -| `SimpleWorld` | Persisted runtime for the active piece | -| `Position` | Piece x/y on the grid | -| `impl_component!` / `impl_marker_component!` | `TetrominoComponent`, `ActivePieceMarker` | -| `SimpleQueryBuilder` | Find the single active piece entity (sparse marker) | -| `set_typed` / `get_typed` | Read and write components on the piece entity | +## ๐ŸŽฏ Benefits of Using Cougr-Core + +### Traditional Soroban vs. Cougr-Core + +| Aspect | Traditional Soroban | With Cougr-Core ECS | +|--------|-------------------|-------------------| +| **Code Organization** | Monolithic contract logic | Modular components & systems | +| **State Management** | Manual storage handling | Automatic entity-component management | +| **Game Logic** | Tightly coupled functions | Reusable, composable systems | +| **Scalability** | Difficult to extend | Easy to add new features | +| **Code Reuse** | Limited | High - components are portable | +| **Testing** | Complex integration tests | Unit testable components | + +### Cougr-Core Advantages + +1. **Entity-Component-System Pattern** + - Separates data (components) from logic (systems) + - Makes code more maintainable and testable + - Enables parallel processing of game logic + +2. **Simplified State Management** +```rust + // Traditional Soroban + env.storage().instance().set(&DataKey::GameState, &state); + + // With Cougr-Core + world.spawn_empty() + .insert(Position { x: 5, y: 0 }) + .insert(Tetromino { shape: Shape::I }); +``` -Not used here (by design): `GameApp`, multi-entity simulation of the full board. See `snake` or `space_invaders` for those patterns. +3. **Reusable Components** + - Components can be shared across different game types + - Systems can be reused for similar game mechanics + - Reduces development time for new games -## Build and test +4. **Better Code Organization** + - Clear separation of concerns + - Easier to understand and debug + - Modular architecture +## ๐Ÿงช Testing ```bash -cd examples/tetris +# Run all tests cargo test -stellar contract build -``` -## Known limitations +# Run with output +cargo test -- --nocapture -- Only the falling piece is an ECS entity; locked cells live in the board vector -- No `GameApp` scheduler โ€” move/lock logic is inline because each contract call is one player action -- Piece randomness uses Soroban PRNG; not suitable for competitive play without commit-reveal +# Test specific function +cargo test test_rotate +``` + +### Test Coverage -## Project structure +| Test | Description | +|------|-------------| +| `test_init_game` | Verifies game initialization | +| `test_move_left` | Tests left movement | +| `test_move_right` | Tests right movement | +| `test_move_down` | Tests downward movement | +| `test_rotate` | Tests piece rotation | +| `test_update_tick` | Tests game tick and line clearing | +| `test_game_over` | Tests end game detection | +## ๐Ÿ“ Project Structure ``` examples/tetris/ -โ”œโ”€โ”€ Cargo.toml -โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ Cargo.toml # Dependencies & build config +โ”œโ”€โ”€ .gitignore # Git ignore patterns +โ”œโ”€โ”€ README.md # This file โ””โ”€โ”€ src/ - โ””โ”€โ”€ lib.rs # Contract, components, and game logic + โ””โ”€โ”€ lib.rs # Smart contract implementation +``` + +## ๐Ÿ”ง Configuration + +**Cargo.toml** +```toml +[dependencies] +soroban-sdk = "25.1.0" +cougr-core = "1.0.0" ``` -## License +## ๐Ÿ“š 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 + +This example is part of the Cougr framework. Contributions are welcome! + +## ๐Ÿ“„ License -MIT OR Apache-2.0 +Licensed under MIT OR Apache-2.0 \ No newline at end of file diff --git a/examples/tetris/src/lib.rs b/examples/tetris/src/lib.rs index 1fbd377..0397bd0 100644 --- a/examples/tetris/src/lib.rs +++ b/examples/tetris/src/lib.rs @@ -1,10 +1,12 @@ #![no_std] -use cougr_core::{ - impl_component, impl_marker_component, Position, RuntimeWorld, SimpleQueryBuilder, SimpleWorld, -}; use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, Env, Vec}; +// We aliasing cougr_core types to avoid confusion if we had local duplicates, +// but here we just import them. +// Note: In a real scenario, we'd ensure cougr_core is compatible with soroban-sdk v21. +use cougr_core::SimpleWorld; + // -------------------------------------------------------------------------------- // Data Structures // -------------------------------------------------------------------------------- @@ -21,36 +23,21 @@ pub enum TetrominoShape { Z = 6, } -impl TetrominoShape { - fn from_u32(value: u32) -> Self { - match value { - 0 => TetrominoShape::I, - 1 => TetrominoShape::J, - 2 => TetrominoShape::L, - 3 => TetrominoShape::O, - 4 => TetrominoShape::S, - 5 => TetrominoShape::T, - _ => TetrominoShape::Z, - } - } - - fn as_u32(self) -> u32 { - self as u32 - } -} - #[contracttype] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Piece { pub shape: TetrominoShape, pub x: i32, pub y: i32, - pub rotation: u32, + pub rotation: u32, // 0, 1, 2, 3 } #[contracttype] #[derive(Clone, Debug, PartialEq, Eq)] pub struct GameState { + // Board is 20x10. We can represent it as a Vec> or flattened. + // For Soroban efficiency, maybe Vec where each u32 is a row? + // 20 rows. 10 bits used per row. pub board: Vec, pub current_piece: Piece, pub next_piece: Piece, @@ -64,20 +51,15 @@ pub struct GameState { // ECS Components // -------------------------------------------------------------------------------- -/// Shape and rotation for the falling tetromino entity. -#[contracttype] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct TetrominoComponent { - pub shape: u32, - pub rotation: u32, -} - -impl_component!(TetrominoComponent, "tetrom", Table, { shape: u32, rotation: u32 }); +// We use cougr-core Components to represent the active piece during logic updates. +// We need to implement serialization for custom components if we want to store them, +// but for this example, we might use standard types or transient World. -/// Marks the single active falling piece entity in the world. -pub struct ActivePieceMarker; +// However, cougr-core v0.0.1 likely requires components to handle Bytes. +// Let's define a helper to map our Piece to ECS components. -impl_marker_component!(ActivePieceMarker, "active", Sparse); +// Position is often a standard component. +// We'll define a custom component for Tetromino info. // -------------------------------------------------------------------------------- // Contract @@ -85,8 +67,6 @@ impl_marker_component!(ActivePieceMarker, "active", Sparse); const BOARD_WIDTH: i32 = 10; const BOARD_HEIGHT: i32 = 20; -const GAME_KEY: soroban_sdk::Symbol = symbol_short!("game"); -const WORLD_KEY: soroban_sdk::Symbol = symbol_short!("world"); #[contract] pub struct TetrisContract; @@ -95,11 +75,11 @@ pub struct TetrisContract; impl TetrisContract { /// Initialize the game pub fn init_game(env: Env) -> GameState { - let board = Vec::from_array(&env, [0u32; 20]); - let next_piece = generate_piece(&env); + let board = Vec::from_array(&env, [0u32; 20]); // 20 empty rows - let mut world = SimpleWorld::new(&env); - let current_piece = spawn_active_piece(&mut world, &env, next_piece.clone()); + // Spawn initial pieces + let current_piece = generate_piece(&env); + let next_piece = generate_piece(&env); let state = GameState { board, @@ -111,7 +91,6 @@ impl TetrisContract { game_over: false, }; - save_world(&env, &world); save_state(&env, &state); state } @@ -123,9 +102,7 @@ impl TetrisContract { return false; } - let mut world = load_world(&env); - if try_move(&env, &mut world, &mut state, -1, 0, 0) { - save_world(&env, &world); + if try_move(&env, &mut state, -1, 0, 0) { save_state(&env, &state); true } else { @@ -140,9 +117,7 @@ impl TetrisContract { return false; } - let mut world = load_world(&env); - if try_move(&env, &mut world, &mut state, 1, 0, 0) { - save_world(&env, &world); + if try_move(&env, &mut state, 1, 0, 0) { save_state(&env, &state); true } else { @@ -157,14 +132,12 @@ impl TetrisContract { return false; } - let mut world = load_world(&env); - if try_move(&env, &mut world, &mut state, 0, 1, 0) { - save_world(&env, &world); + if try_move(&env, &mut state, 0, 1, 0) { save_state(&env, &state); true } else { - lock_piece(&env, &mut world, &mut state); - save_world(&env, &world); + // Lock piece if it can't move down + lock_piece(&env, &mut state); save_state(&env, &state); false } @@ -177,9 +150,8 @@ impl TetrisContract { return false; } - let mut world = load_world(&env); - if try_move(&env, &mut world, &mut state, 0, 0, 1) { - save_world(&env, &world); + // Rotation is +1 to index (clockwise) + if try_move(&env, &mut state, 0, 0, 1) { save_state(&env, &state); true } else { @@ -194,14 +166,12 @@ impl TetrisContract { return 0; } - let mut world = load_world(&env); let mut dropped = 0; - while try_move(&env, &mut world, &mut state, 0, 1, 0) { + while try_move(&env, &mut state, 0, 1, 0) { dropped += 1; } - lock_piece(&env, &mut world, &mut state); - save_world(&env, &world); + lock_piece(&env, &mut state); save_state(&env, &state); dropped } @@ -213,171 +183,85 @@ impl TetrisContract { return state; } - let mut world = load_world(&env); - if !try_move(&env, &mut world, &mut state, 0, 1, 0) { - lock_piece(&env, &mut world, &mut state); + // Try to move down + if !try_move(&env, &mut state, 0, 1, 0) { + lock_piece(&env, &mut state); } - save_world(&env, &world); save_state(&env, &state); state } /// Get current state pub fn get_state(env: Env) -> GameState { - let mut state: GameState = env - .storage() + env.storage() .instance() - .get(&GAME_KEY) - .expect("Game not initialized"); - let world = load_world(&env); - if let Some(piece) = active_piece(&world, &env) { - state.current_piece = piece; - } - state - } - - /// Returns the number of entities in the Cougr world (active piece). - pub fn get_entity_count(env: Env) -> u32 { - load_world(&env).entity_count() as u32 + .get(&symbol_short!("game")) + .expect("Game not initialized") } } // -------------------------------------------------------------------------------- -// World helpers +// Logic & Helpers // -------------------------------------------------------------------------------- -fn load_world(env: &Env) -> SimpleWorld { - env.storage() - .instance() - .get(&WORLD_KEY) - .expect("World not initialized") -} - -fn save_world(env: &Env, world: &SimpleWorld) { - env.storage().instance().set(&WORLD_KEY, world); -} - fn save_state(env: &Env, state: &GameState) { - env.storage().instance().set(&GAME_KEY, state); -} - -fn active_piece_entity(world: &SimpleWorld, env: &Env) -> Option { - let query = SimpleQueryBuilder::new(env) - .with_component(symbol_short!("active")) - .include_sparse() - .build(); - let entities = query.execute(world, env); - if entities.is_empty() { - None - } else { - Some(entities.get(0).unwrap()) - } -} - -fn active_piece(world: &SimpleWorld, env: &Env) -> Option { - let entity_id = active_piece_entity(world, env)?; - piece_from_entity(world, env, entity_id) -} - -fn piece_from_entity(world: &SimpleWorld, env: &Env, entity_id: u32) -> Option { - let position = world.get_typed::(env, entity_id)?; - let tetromino = world.get_typed::(env, entity_id)?; - Some(Piece { - shape: TetrominoShape::from_u32(tetromino.shape), - x: position.x, - y: position.y, - rotation: tetromino.rotation, - }) -} - -fn spawn_active_piece(world: &mut SimpleWorld, env: &Env, piece: Piece) -> Piece { - if let Some(entity_id) = active_piece_entity(world, env) { - world.despawn_entity(entity_id); - } - - let entity_id = world.spawn_entity(); - world.set_typed( - env, - entity_id, - &Position::new(piece.x, piece.y), - ); - world.set_typed( - env, - entity_id, - &TetrominoComponent { - shape: piece.shape.as_u32(), - rotation: piece.rotation, - }, - ); - world.set_typed(env, entity_id, &ActivePieceMarker); - - piece + env.storage().instance().set(&symbol_short!("game"), state); } fn generate_piece(env: &Env) -> Piece { - let shape_idx = env.prng().gen_range::(0..7) as u32; - let shape = TetrominoShape::from_u32(shape_idx); + // Random shape (0-6) + let shape_idx = env.prng().gen_range(0..7); + let shape = match shape_idx { + 0 => TetrominoShape::I, + 1 => TetrominoShape::J, + 2 => TetrominoShape::L, + 3 => TetrominoShape::O, + 4 => TetrominoShape::S, + 5 => TetrominoShape::T, + _ => TetrominoShape::Z, + }; Piece { shape, - x: 3, + x: 3, // Start in middle roughly y: 0, rotation: 0, } } -// -------------------------------------------------------------------------------- -// Game logic -// -------------------------------------------------------------------------------- +// ECS Integration: +// We use a ephemeral World to calculate the move validity. +// This demonstrates usage of cougr-core even if we store state in a simplified struct. +fn try_move(env: &Env, state: &mut GameState, dx: i32, dy: i32, d_rot: i32) -> bool { + // 1. Create ECS World + let _world = SimpleWorld::new(env); -fn try_move( - env: &Env, - world: &mut SimpleWorld, - state: &mut GameState, - dx: i32, - dy: i32, - d_rot: i32, -) -> bool { - let entity_id = match active_piece_entity(world, env) { - Some(id) => id, - None => return false, - }; - - let position = match world.get_typed::(env, entity_id) { - Some(pos) => pos, - None => return false, - }; - let tetromino = match world.get_typed::(env, entity_id) { - Some(data) => data, - None => return false, - }; + // 2. Define Components + // In a full game, we'd have these registered. + // Here we map our `Piece` to `Position` and `Shape` (conceptually). - let shape = TetrominoShape::from_u32(tetromino.shape); - let new_x = position.x + dx; - let new_y = position.y + dy; - let new_rot = (tetromino.rotation as i32 + d_rot).rem_euclid(4) as u32; + // Calculate new parameters + let new_x = state.current_piece.x + dx; + let new_y = state.current_piece.y + dy; + let new_rot = (state.current_piece.rotation as i32 + d_rot).rem_euclid(4) as u32; - if check_collision(env, &state.board, shape, new_x, new_y, new_rot) { + // 3. Collision System logic + if check_collision( + env, + &state.board, + state.current_piece.shape, + new_x, + new_y, + new_rot, + ) { return false; } - world.set_typed(env, entity_id, &Position::new(new_x, new_y)); - world.set_typed( - env, - entity_id, - &TetrominoComponent { - shape: tetromino.shape, - rotation: new_rot, - }, - ); - - state.current_piece = Piece { - shape, - x: new_x, - y: new_y, - rotation: new_rot, - }; + // 4. Update Entity (State) + state.current_piece.x = new_x; + state.current_piece.y = new_y; + state.current_piece.rotation = new_rot; true } @@ -396,10 +280,12 @@ fn check_collision( let abs_x = x + cx; let abs_y = y + cy; + // Wall collision if !(0..BOARD_WIDTH).contains(&abs_x) || abs_y >= BOARD_HEIGHT { return true; } + // Floor/Existing piece collision if abs_y >= 0 { let row = board.get(abs_y as u32).unwrap_or(0); if (row >> abs_x) & 1 == 1 { @@ -410,23 +296,20 @@ fn check_collision( false } -fn lock_piece(env: &Env, world: &mut SimpleWorld, state: &mut GameState) { - let entity_id = match active_piece_entity(world, env) { - Some(id) => id, - None => return, - }; +fn lock_piece(env: &Env, state: &mut GameState) { + let coords = get_piece_coords(state.current_piece.shape, state.current_piece.rotation); - let piece = match piece_from_entity(world, env, entity_id) { - Some(piece) => piece, - None => return, - }; + // check game over + // If piece is locked and any part is above y=0 (or valid board area start), it's game over? + // Actually typically if we can't spawn. + // If we lock at y=0, it might be game over. - let coords = get_piece_coords(piece.shape, piece.rotation); let mut game_over = false; + // Place piece on board for (cx, cy) in coords { - let abs_x = piece.x + cx; - let abs_y = piece.y + cy; + let abs_x = state.current_piece.x + cx; + let abs_y = state.current_piece.y + cy; if abs_y < 0 { game_over = true; @@ -437,18 +320,19 @@ fn lock_piece(env: &Env, world: &mut SimpleWorld, state: &mut GameState) { } } - world.despawn_entity(entity_id); - if game_over { state.game_over = true; return; } + // Clear lines let mut lines = 0; let mut new_board = Vec::new(env); + // We rebuild board skipping full lines for i in 0..state.board.len() { let row = state.board.get(i).unwrap(); + // 10 bits set = 1023 (2^10 - 1) if row == 1023 { lines += 1; } else { @@ -456,11 +340,14 @@ fn lock_piece(env: &Env, world: &mut SimpleWorld, state: &mut GameState) { } } + // Add empty lines at top for _ in 0..lines { - new_board.push_front(0); + new_board.push_front(0); // This might be push_front? Soroban Vec is generic. + // Actually Soroban Vec `push_front` exists. } state.board = new_board; + // Score if lines > 0 { let points = match lines { 1 => 100, @@ -476,10 +363,11 @@ fn lock_piece(env: &Env, world: &mut SimpleWorld, state: &mut GameState) { } } + // Spawn new state.current_piece = state.next_piece.clone(); state.next_piece = generate_piece(env); - state.current_piece = spawn_active_piece(world, env, state.current_piece.clone()); + // Initial collision check for new piece if check_collision( env, &state.board, @@ -488,14 +376,15 @@ fn lock_piece(env: &Env, world: &mut SimpleWorld, state: &mut GameState) { state.current_piece.y, state.current_piece.rotation, ) { - if let Some(entity_id) = active_piece_entity(world, env) { - world.despawn_entity(entity_id); - } state.game_over = true; } } +// Coordinate definitions for shapes +// (x, y) offsets relative to pivot fn get_piece_coords(shape: TetrominoShape, rot: u32) -> [(i32, i32); 4] { + // Simplified rotation system (SRS concepts or basic) + // I, J, L, O, S, T, Z match shape { TetrominoShape::I => match rot { 0 => [(-1, 0), (0, 0), (1, 0), (2, 0)], @@ -503,13 +392,18 @@ fn get_piece_coords(shape: TetrominoShape, rot: u32) -> [(i32, i32); 4] { 2 => [(-1, 1), (0, 1), (1, 1), (2, 1)], _ => [(0, -1), (0, 0), (0, 1), (0, 2)], }, - TetrominoShape::O => [(0, 0), (1, 0), (0, 1), (1, 1)], + TetrominoShape::O => [(0, 0), (1, 0), (0, 1), (1, 1)], // No rotation change visually TetrominoShape::T => match rot { 0 => [(-1, 0), (0, 0), (1, 0), (0, 1)], 1 => [(0, -1), (0, 0), (0, 1), (-1, 0)], 2 => [(-1, 0), (0, 0), (1, 0), (0, -1)], _ => [(0, -1), (0, 0), (0, 1), (1, 0)], }, + // Implement others similarly... + // For brevity in this example, mapping placeholders for J, L, S, Z + // Using T shape for others to ensure compile, but in real generic implementation we'd fill all. + // User asked for "Piece rotation using rotation matrices" or similar. + // I will implement all to satisfy "COMPLETE TETRIS GAME LOGIC". TetrominoShape::J => match rot { 0 => [(-1, 0), (0, 0), (1, 0), (1, 1)], 1 => [(0, -1), (0, 0), (0, 1), (-1, 1)], @@ -525,7 +419,7 @@ fn get_piece_coords(shape: TetrominoShape, rot: u32) -> [(i32, i32); 4] { TetrominoShape::S => match rot { 0 => [(0, 0), (1, 0), (-1, 1), (0, 1)], 1 => [(0, -1), (0, 0), (1, 0), (1, 1)], - 2 => [(0, 0), (1, 0), (-1, 1), (0, 1)], + 2 => [(0, 0), (1, 0), (-1, 1), (0, 1)], // S/Z 2 states _ => [(0, -1), (0, 0), (1, 0), (1, 1)], }, TetrominoShape::Z => match rot { @@ -552,7 +446,6 @@ mod test { let state = client.init_game(); assert_eq!(state.score, 0); assert!(!state.game_over); - assert_eq!(client.get_entity_count(), 1); } #[test] @@ -560,7 +453,11 @@ mod test { let env = Env::default(); let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); client.init_game(); + + // Initial move let _moved = client.move_left(); + // Depends on random spawn, but generally possible if logic is correct + // We verify it returns a boolean } #[test] @@ -568,7 +465,10 @@ mod test { let env = Env::default(); let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); client.init_game(); + + // Try rotate let _rotated = client.rotate(); + // Should execute without panic } #[test] @@ -577,6 +477,8 @@ mod test { let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); client.init_game(); + // Move until hit wall? + // Since we can't easily force state without backdoor, we rely on move returning false eventually for _ in 0..10 { client.move_left(); } @@ -584,9 +486,12 @@ mod test { #[test] fn test_line_clearing() { + // This is hard to test black-box without setting specific board state + // But we can ensure update_tick runs let env = Env::default(); let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); client.init_game(); + let _lines = client.update_tick(); } @@ -595,6 +500,7 @@ mod test { let env = Env::default(); let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); client.init_game(); + assert_eq!(client.get_state().score, 0); } @@ -603,14 +509,7 @@ mod test { let env = Env::default(); let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); client.init_game(); - assert!(!client.get_state().game_over); - } - #[test] - fn test_active_piece_in_world() { - let env = Env::default(); - let client = TetrisContractClient::new(&env, &env.register(TetrisContract, ())); - client.init_game(); - assert_eq!(client.get_entity_count(), 1); + assert!(!client.get_state().game_over); } }