Skip to content

Commit e75fb5b

Browse files
corvid-agentclaude
andcommitted
feat: add CI workflow, test suite, and README
Extract game logic (neighbors, step, population, set) into lib.rs with 9 unit tests covering still lifes, oscillators, gliders, birth/death rules, and edge clipping. Add GitHub Actions CI that builds the WASM target, runs clippy, fmt, and tests. Add descriptive README with install and usage instructions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0a60b2e commit e75fb5b

5 files changed

Lines changed: 305 additions & 61 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: dtolnay/rust-toolchain@stable
16+
with:
17+
targets: wasm32-wasip1
18+
components: clippy, rustfmt
19+
20+
- name: Check formatting
21+
run: cargo fmt --check
22+
23+
- name: Clippy
24+
run: cargo clippy --target wasm32-wasip1 -- -D warnings
25+
26+
- name: Build (WASM)
27+
run: cargo build --target wasm32-wasip1 --release
28+
29+
- name: Run tests (host)
30+
run: cargo test

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ name = "fledge-plugin-life"
33
version = "0.1.0"
44
edition = "2021"
55
description = "Conway's Game of Life — visual WASM plugin demo for fledge"
6+
license = "MIT"
7+
repository = "https://github.com/corvid-agent/fledge-plugin-life"
8+
9+
[lib]
10+
name = "fledge_plugin_life"
11+
path = "src/lib.rs"
12+
13+
[[bin]]
14+
name = "fledge-plugin-life"
15+
path = "src/main.rs"
616

717
[profile.release]
818
opt-level = "s"

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# fledge-plugin-life
2+
3+
Conway's Game of Life — visual WASM plugin demo for [fledge](https://github.com/CorvidLabs/fledge).
4+
5+
Runs an animated Game of Life simulation directly in your terminal via the
6+
fledge plugin system. The board is seeded with an R-pentomino, two gliders,
7+
and an LWSS (lightweight spaceship) and evolves for 80 generations with
8+
real-time ANSI rendering.
9+
10+
## Install
11+
12+
```bash
13+
fledge plugins install corvid-agent/fledge-plugin-life
14+
```
15+
16+
## Usage
17+
18+
```bash
19+
fledge life
20+
```
21+
22+
The simulation renders a 50x25 board at ~8 fps, using box-drawing borders
23+
and block characters for live cells. The cursor is hidden during playback
24+
and restored on completion.
25+
26+
## Build from source
27+
28+
Requires Rust with the `wasm32-wasip1` target:
29+
30+
```bash
31+
rustup target add wasm32-wasip1
32+
cargo build --target wasm32-wasip1 --release
33+
```
34+
35+
The compiled WASM binary is written to
36+
`target/wasm32-wasip1/release/fledge-plugin-life.wasm`.
37+
38+
## Tests
39+
40+
```bash
41+
cargo test
42+
```
43+
44+
The test suite covers the core game logic: neighbor counting, B3/S23 rules,
45+
still lifes (block), oscillators (blinker), spaceships (glider), birth,
46+
death, pattern placement, and out-of-bounds clipping.
47+
48+
## Plugin manifest
49+
50+
See `plugin.toml` for the fledge-v1 protocol manifest. This plugin requires
51+
no capabilities (no filesystem, network, exec, or store access).
52+
53+
## License
54+
55+
MIT

src/lib.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/// Grid dimensions.
2+
pub const W: usize = 50;
3+
pub const H: usize = 25;
4+
5+
/// A Game of Life board.
6+
pub type Grid = [[bool; W]; H];
7+
8+
/// Count live neighbors of cell (r, c).
9+
pub fn neighbors(grid: &Grid, r: usize, c: usize) -> u8 {
10+
let mut n = 0u8;
11+
for dr in [-1i32, 0, 1] {
12+
for dc in [-1i32, 0, 1] {
13+
if dr == 0 && dc == 0 {
14+
continue;
15+
}
16+
let nr = r as i32 + dr;
17+
let nc = c as i32 + dc;
18+
if nr >= 0
19+
&& nr < H as i32
20+
&& nc >= 0
21+
&& nc < W as i32
22+
&& grid[nr as usize][nc as usize]
23+
{
24+
n += 1;
25+
}
26+
}
27+
}
28+
n
29+
}
30+
31+
/// Advance the grid by one generation using standard B3/S23 rules.
32+
pub fn step(grid: &Grid) -> Grid {
33+
let mut next = [[false; W]; H];
34+
for r in 0..H {
35+
for c in 0..W {
36+
let n = neighbors(grid, r, c);
37+
next[r][c] = if grid[r][c] { n == 2 || n == 3 } else { n == 3 };
38+
}
39+
}
40+
next
41+
}
42+
43+
/// Count live cells.
44+
pub fn population(grid: &Grid) -> usize {
45+
grid.iter().flat_map(|r| r.iter()).filter(|&&c| c).count()
46+
}
47+
48+
/// Place a pattern on the grid at offset (r, c).
49+
pub fn set(grid: &mut Grid, r: usize, c: usize, cells: &[(i32, i32)]) {
50+
for &(dr, dc) in cells {
51+
let nr = r as i32 + dr;
52+
let nc = c as i32 + dc;
53+
if nr >= 0 && nr < H as i32 && nc >= 0 && nc < W as i32 {
54+
grid[nr as usize][nc as usize] = true;
55+
}
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use super::*;
62+
63+
#[test]
64+
fn empty_grid_stays_empty() {
65+
let grid: Grid = [[false; W]; H];
66+
let next = step(&grid);
67+
assert_eq!(population(&next), 0);
68+
}
69+
70+
#[test]
71+
fn block_is_still_life() {
72+
// 2x2 block is a still life (stable pattern)
73+
let mut grid: Grid = [[false; W]; H];
74+
grid[5][5] = true;
75+
grid[5][6] = true;
76+
grid[6][5] = true;
77+
grid[6][6] = true;
78+
79+
let next = step(&grid);
80+
assert_eq!(population(&next), 4);
81+
assert!(next[5][5] && next[5][6] && next[6][5] && next[6][6]);
82+
}
83+
84+
#[test]
85+
fn blinker_oscillates() {
86+
// Horizontal blinker (period 2)
87+
let mut grid: Grid = [[false; W]; H];
88+
grid[10][10] = true;
89+
grid[10][11] = true;
90+
grid[10][12] = true;
91+
92+
let gen1 = step(&grid);
93+
// Should become vertical
94+
assert!(gen1[9][11] && gen1[10][11] && gen1[11][11]);
95+
assert!(!gen1[10][10] && !gen1[10][12]);
96+
97+
let gen2 = step(&gen1);
98+
// Should return to horizontal
99+
assert!(gen2[10][10] && gen2[10][11] && gen2[10][12]);
100+
assert!(!gen2[9][11] && !gen2[11][11]);
101+
}
102+
103+
#[test]
104+
fn glider_moves() {
105+
// Standard glider heading SE
106+
let mut grid: Grid = [[false; W]; H];
107+
set(&mut grid, 2, 2, &[(-1, 0), (0, 1), (1, -1), (1, 0), (1, 1)]);
108+
109+
let initial_pop = population(&grid);
110+
assert_eq!(initial_pop, 5);
111+
112+
// After 4 generations, a glider translates one cell diagonally
113+
let mut g = grid;
114+
for _ in 0..4 {
115+
g = step(&g);
116+
}
117+
assert_eq!(population(&g), 5, "glider should preserve population");
118+
}
119+
120+
#[test]
121+
fn lone_cell_dies() {
122+
let mut grid: Grid = [[false; W]; H];
123+
grid[10][10] = true;
124+
125+
let next = step(&grid);
126+
assert_eq!(population(&next), 0);
127+
}
128+
129+
#[test]
130+
fn three_in_corner_reproduce() {
131+
// Three cells with a shared neighbor create a new cell
132+
let mut grid: Grid = [[false; W]; H];
133+
grid[0][0] = true;
134+
grid[0][1] = true;
135+
grid[1][0] = true;
136+
137+
let next = step(&grid);
138+
// (1,1) should be born (3 neighbors)
139+
assert!(next[1][1]);
140+
assert_eq!(population(&next), 4); // all original survive + new cell
141+
}
142+
143+
#[test]
144+
fn neighbors_count_correct() {
145+
let mut grid: Grid = [[false; W]; H];
146+
grid[5][5] = true;
147+
grid[5][6] = true;
148+
grid[6][5] = true;
149+
150+
assert_eq!(neighbors(&grid, 5, 5), 2);
151+
assert_eq!(neighbors(&grid, 6, 6), 3); // dead cell with 3 neighbors
152+
assert_eq!(neighbors(&grid, 4, 4), 1);
153+
}
154+
155+
#[test]
156+
fn set_places_pattern() {
157+
let mut grid: Grid = [[false; W]; H];
158+
set(&mut grid, 5, 5, &[(0, 0), (0, 1), (1, 0)]);
159+
160+
assert!(grid[5][5]);
161+
assert!(grid[5][6]);
162+
assert!(grid[6][5]);
163+
assert_eq!(population(&grid), 3);
164+
}
165+
166+
#[test]
167+
fn set_clips_out_of_bounds() {
168+
let mut grid: Grid = [[false; W]; H];
169+
// Placing near edge should not panic
170+
set(&mut grid, 0, 0, &[(-1, -1), (0, 0), (1, 1)]);
171+
// Only (0,0) and (1,1) should be placed; (-1,-1) is out of bounds
172+
assert!(grid[0][0]);
173+
assert!(grid[1][1]);
174+
assert_eq!(population(&grid), 2);
175+
}
176+
}

0 commit comments

Comments
 (0)