Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 45 additions & 100 deletions examples/flappy_bird/README.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,65 @@
# Flappy Bird - On-Chain Game Example using Cougr-Core
# Flappy Bird On-Chain Game

This example demonstrates how to build an on-chain game using the `cougr-core` ECS (Entity Component System) framework on the Stellar blockchain via Soroban smart contracts.
> **Transitional example**: This example uses an older Cougr pattern and is preserved
> for compatibility reference. For the current recommended approach, see `snake`.

## Overview
## Purpose and pattern

This is a fully functional Flappy Bird game implemented as a Soroban smart contract. The game demonstrates:
This example demonstrates a side-scroller gravity loop on Soroban with Cougr ECS concepts. It remains transitional while the arcade examples converge on the canonical `snake` `GameApp` architecture.

- **ECS Architecture**: Using cougr-core's Entity Component System for game logic
- **On-Chain State Management**: Storing game state persistently on the blockchain
- **System-Based Logic**: Implementing game mechanics as modular systems
- **Component Serialization**: Proper serialization of game components for blockchain storage
## Public contract API

## Learning Objectives
| Function | Parameters | Return type | Description |
|---|---|---:|---|
| `init_game` | `none` | `()` | Initializes bird, velocity, pipes, score, and tick state. |
| `flap` | `none` | `()` | Applies upward input velocity if the game is still active. |
| `update_tick` | `none` | `()` | Runs one scheduled tick for gravity, movement, pipes, collisions, and scoring. |
| `get_score` | `none` | `u32` | Returns the current score. |
| `check_game_over` | `none` | `bool` | Returns whether the bird has crashed. |
| `get_bird_pos` | `none` | `(i32, i32)` | Returns the bird position. |

By studying this example, you'll learn:

1. How to integrate `cougr-core` into a Soroban smart contract
2. How to serialize and deserialize ECS World state for on-chain storage
3. How to implement game systems (gravity, movement, collision detection)
4. How to create custom components with proper serialization
5. How to structure a turn-based on-chain game

## Prerequisites

Before you begin, ensure you have the following installed:

- **Rust** (1.70.0 or later): [Install Rust](https://www.rust-lang.org/tools/install)
- **Stellar CLI**: [Install Stellar CLI](https://developers.stellar.org/docs/tools/cli/install)
- **wasm32v1-none** target:
```bash
rustup target add wasm32v1-none
```

## Project Structure
## Architecture overview

```text
contract entrypoint
├─ reads game state from Soroban storage
├─ applies input or tick systems
└─ writes updated state back to storage
```
examples/flappy_bird/
├── Cargo.toml # Project dependencies and configuration
├── src/
│ ├── lib.rs # Main contract implementation
│ ├── components.rs # Custom game components (BirdState, PipeConfig)
│ ├── systems.rs # Game systems (gravity, collision, scoring)
└── README.md # This file
```

## Architecture

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

The game uses the following components from the ECS pattern:
## Storage model

1. **Position** (from cougr-core): 2D coordinates (x, y)
2. **Velocity** (from cougr-core): Movement vector (x, y)
3. **BirdState**: Tracks whether the bird is alive
4. **PipeConfig**: Stores pipe gap size and position
5. **PipeMarker**: Identifies entities as pipes and tracks if bird passed them
| Storage class | Data | Why |
|---|---|---|
| Instance storage | Per-contract game state where used by this example. | Keeps small arcade state close to the contract instance. |
| Persistent storage | Player- or world-scoped state where the example needs durable keyed state. | Keeps game progress available across invocations. |
| Temporary storage | Not used. | The examples favor deterministic recalculation over ephemeral caches. |

### Systems
## Main gameplay flow

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

1. **Gravity System**: Applies downward acceleration to the bird
2. **Movement System**: Updates positions based on velocities
3. **Pipe Movement System**: Moves pipes horizontally
4. **Collision System**: Detects bird-pipe and bird-ground collisions
5. **Score System**: Increments score when bird passes pipes
## Cougr APIs used

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

This example follows the recommended Cougr Soroban runtime:

- `GameApp` orchestrates each invocation
- startup systems spawn the initial bird and pipe set
- scheduled systems run in `PreUpdate`, `Update`, and `PostUpdate`
- query-heavy scans use `SimpleQueryBuilder`

### Contract Functions

- `init_game()`: Initialize a new game with bird and pipes
- `flap()`: Make the bird jump (apply upward velocity)
- `update_tick()`: Execute one game tick (run all systems)
- `get_score() -> u32`: Get current score
- `check_game_over() -> bool`: Check if game has ended
- `get_bird_pos() -> (i32, i32)`: Get bird's current position

## Building the Contract

### 1. Build with Cargo

```bash
cd examples/flappy_bird
cargo build --target wasm32v1-none --release
```

### 2. Build with Stellar CLI
## Build and test commands

```bash
cargo test
stellar contract build
```

This will generate the WASM file at:
```
target/wasm32v1-none/release/flappy_bird.wasm
```

### 3. Optimize (Optional)

For production, you can further optimize the WASM:

```bash
stellar contract optimize --wasm target/wasm32v1-none/release/flappy_bird.wasm
```

## Running Tests

Run the comprehensive test suite:

```bash
cargo test
```
## Known limitations

The tests cover:
- Game initialization
Expand Down Expand Up @@ -376,4 +317,8 @@ This example is part of the Cougr project and follows the same license.

## Contributing

Found a bug or have an improvement? Please open an issue or pull request in the main [Cougr repository](https://github.com/salazarsebas/Cougr).

- Transitional code may preserve older storage or scheduling patterns for compatibility reference.
- No authentication, matchmaking, real-time rendering, or production randomness is included.
- One contract instance generally represents one game or one keyed set of player games.
- For new work, prefer the canonical `snake` module split and `GameApp` tick wiring.
153 changes: 2 additions & 151 deletions examples/flappy_bird/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,154 +312,5 @@ impl FlappyBirdContract {
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_init_game() {
let env = Env::default();
let contract_id = env.register(FlappyBirdContract, ());
let client = FlappyBirdContractClient::new(&env, &contract_id);

// Initialize game
client.init_game();

// Check game state
let score = client.get_score();
assert_eq!(score, 0);

let game_over = client.check_game_over();
assert!(!game_over);

// Check bird position
let (x, y) = client.get_bird_pos();
assert_eq!(x, INIT_BIRD_X);
assert_eq!(y, INIT_BIRD_Y);
}

#[test]
fn test_flap() {
let env = Env::default();
let contract_id = env.register(FlappyBirdContract, ());
let client = FlappyBirdContractClient::new(&env, &contract_id);

// Initialize game
client.init_game();

// Flap
client.flap();

// Bird velocity should have changed (will see effect after update_tick)
client.update_tick();

let (_, y) = client.get_bird_pos();
// After flap and one tick, bird should have moved up
assert!(y < INIT_BIRD_Y);
}

#[test]
fn test_gravity() {
let env = Env::default();
let contract_id = env.register(FlappyBirdContract, ());
let client = FlappyBirdContractClient::new(&env, &contract_id);

// Initialize game
client.init_game();

let (_, y_before) = client.get_bird_pos();

// Update multiple ticks without flapping
client.update_tick();
client.update_tick();
client.update_tick();

let (_, y_after) = client.get_bird_pos();

// Bird should have fallen
assert!(y_after > y_before);
}

#[test]
fn test_game_over_on_ground_collision() {
let env = Env::default();
let contract_id = env.register(FlappyBirdContract, ());
let client = FlappyBirdContractClient::new(&env, &contract_id);

// Initialize game
client.init_game();

// Let bird fall to ground
for _ in 0..100 {
client.update_tick();
if client.check_game_over() {
break;
}
}

// Game should be over
assert!(client.check_game_over());
}

#[test]
fn test_score_increases() {
let env = Env::default();
let contract_id = env.register(FlappyBirdContract, ());
let client = FlappyBirdContractClient::new(&env, &contract_id);

// Initialize game
client.init_game();

let initial_score = client.get_score();

// Play for a while
for i in 0..100 {
if i % 5 == 0 {
client.flap(); // Flap periodically to stay alive
}
client.update_tick();

if client.check_game_over() {
break;
}
}

let final_score = client.get_score();

// Score should have increased if we survived long enough
// (might not if we died early)
if !client.check_game_over() {
assert!(final_score >= initial_score);
}
}

#[test]
fn test_cannot_flap_after_game_over() {
let env = Env::default();
let contract_id = env.register(FlappyBirdContract, ());
let client = FlappyBirdContractClient::new(&env, &contract_id);

// Initialize game
client.init_game();

// Let bird fall to ground
for _ in 0..100 {
client.update_tick();
if client.check_game_over() {
break;
}
}

assert!(client.check_game_over());

// Try to flap after game over
client.flap();

// Position should not change after game over
let (x1, y1) = client.get_bird_pos();
client.update_tick();
let (x2, y2) = client.get_bird_pos();

assert_eq!(x1, x2);
assert_eq!(y1, y2);
}
}
#[cfg(test)]
mod test;
Loading
Loading