Skip to content
Closed
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
35 changes: 32 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,39 @@ jobs:
- uses: Swatinem/rust-cache@v2
# Formatting and lints are enforced on the rebuilt crates only. The legacy
# `libs/` crates still build and test but are slated for removal.
- run: cargo fmt -p rustywings-core -p rustywings-cli --check
- run: cargo clippy -p rustywings-core -p rustywings-cli --all-targets -- -D warnings
- run: cargo fmt -p rustywings-core -p rustywings-cli -p rustywings-wasm --check
- run: cargo clippy -p rustywings-core -p rustywings-cli -p rustywings-wasm --all-targets -- -D warnings
- run: cargo test --workspace
- run: cargo check -p rustywings-core --target wasm32-unknown-unknown
# The wasm-bindgen facade only compiles for wasm32; lint it there too.
- run: cargo clippy -p rustywings-wasm --target wasm32-unknown-unknown -- -D warnings

web:
name: wasm · typecheck · vitest · vite build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: web/pnpm-lock.yaml
- run: curl -sSf https://rustwasm.github.io/wasm-pack/installer/init.sh | sh
- run: pnpm install --frozen-lockfile
working-directory: web
# `build` runs wasm-pack, tsc and vite in that order; the vitest suite
# then loads the freshly built wasm in Node and checks the golden
# checksum against the native one.
- run: pnpm run build
working-directory: web
- run: pnpm test
working-directory: web

determinism:
name: identical checksum on ${{ matrix.os }}
Expand Down
53 changes: 39 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ reproduction, death and heritable body traits. The simulation is a Rust crate
compiled both natively (CLI) and to WebAssembly (browser). Design rationale is
in `docs/ARCHITECTURE.md` and `docs/decisions/`.

The repo is mid-rebuild. `crates/` is the new code and the future; `libs/` and
`www/` are the original tutorial-derived app, still built and deployed until
the new frontend replaces it. Do not extend `libs/` or `www/`.
The repo is mid-rebuild. `crates/` and `web/` are the new code and what is
deployed; `libs/` and `www/` are the original tutorial-derived app, still
compiled by `cargo test --workspace` until milestone 3 deletes them. Do not
extend `libs/` or `www/`.

## Layout

```
crates/rustywings-core simulation library (no I/O, no threads, wasm-clean)
crates/rustywings-cli `rustywings` binary: run, bench, verify, arena, config
crates/rustywings-wasm wasm-bindgen facade (milestone 2)
web/ Vite + TypeScript frontend (milestone 2)
crates/rustywings-wasm wasm-bindgen facade: `Sim`, frame packing (frame.rs)
web/ Vite + TypeScript frontend, no framework; sim on a Worker,
WebGL2 renderer, SVG charts. Built wasm lands in web/src/wasm
(gitignored). vercel.json at the root builds it on Vercel.
design/mockups/ the HTML mockups the UI was chosen from; D is the reference
docs/ ARCHITECTURE.md and decision records
libs/, www/ legacy code, slated for removal
Expand All @@ -35,8 +38,16 @@ cargo test --workspace # everything, old and
cargo test -p rustywings-core # the new core
cargo test -p rustywings-core -- --ignored # slow: 40k-tick learning test
cargo clippy -p rustywings-core -p rustywings-cli --all-targets -- -D warnings
cargo fmt -p rustywings-core -p rustywings-cli
cargo check -p rustywings-core --target wasm32-unknown-unknown
cargo fmt -p rustywings-core -p rustywings-cli -p rustywings-wasm
cargo clippy -p rustywings-wasm --target wasm32-unknown-unknown -- -D warnings

cd web
pnpm install
pnpm build:wasm # wasm-pack → src/wasm/ (needed before dev, check or test)
pnpm dev # Vite dev server
pnpm check # tsc --noEmit
pnpm test # vitest, includes loading the wasm in Node and checking the golden checksum
pnpm build # build:wasm + check + vite build → dist/

cargo build --release -p rustywings-cli
./target/release/rustywings run --seed 1 --ticks 60000 --every 500 --out stats.csv
Expand All @@ -47,9 +58,11 @@ cargo build --release -p rustywings-cli
./target/release/rustywings config > defaults.json # edit, then pass --config
```

CI (`.github/workflows/ci.yml`) runs fmt, clippy, tests, the wasm check, the
golden-checksum test on Linux/macOS/Windows, and the arena learning test.
Formatting and clippy are enforced on the new crates only.
CI (`.github/workflows/ci.yml`) runs fmt, clippy (native and wasm32), tests,
the golden-checksum test on Linux/macOS/Windows, the arena learning test, and
the web job (wasm-pack, tsc, vitest, vite build). Formatting and clippy are
enforced on the new crates only. Rust needs `export PATH="$HOME/.cargo/bin:$PATH"`
in a fresh shell on this machine.

## Rules that keep determinism

Expand All @@ -67,16 +80,28 @@ The whole design rests on a seed meaning the same thing on every platform.

## Simulation conventions

- World is the unit torus. Heading is radians from +x toward +y. The frontend
flips y for screen space.
- World is the unit torus. Heading is radians from +x toward +y. The renderer
keeps +y up on screen (WebGL clip space), so no flip is needed.
- `Agents` is struct-of-arrays; removing a bird means `Agents::swap_remove`,
which moves every column. Never remove from one column alone.
- Phases in `World::step` destructure `self` into disjoint field borrows. Keep
it that way rather than cloning to satisfy the borrow checker.
- `Config` uses `deny_unknown_fields`; every field has a doc comment in
human units because the UI shows them as tooltips.
- `Species` is stored as `u8` in `Agents::species` so it can be exported to JS
without copying.
- `Species` is stored as `u8` in `Agents::species` so frame packing is a
straight copy.

## Browser boundary conventions

- Seeds cross as hex strings, ids and ticks as `f64`, never `BigInt`.
- Config crosses as JSON text (`JSON.stringify` on the JS side) because
`serde_wasm_bindgen` silently drops unknown keys; `serde_json` honours
`deny_unknown_fields`. Stats and inspections come back as plain objects.
- The frame layout lives in `crates/rustywings-wasm/src/frame.rs` and is
mirrored by the constants in `web/src/sim/protocol.ts`. Change both.
- The worker samples stats every `SAMPLE_STRIDE` (25) ticks; charts are
windows in ticks, not wall-clock time.
- UI copy is plain language: sparrows, hawks, seeds; never herbivore/predator.

## Tuning workflow

Expand Down
36 changes: 35 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

158 changes: 75 additions & 83 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,77 @@
# rustyWings

> **Rebuild in progress.** rustyWings is being rebuilt from a tutorial-derived
> genetic-algorithm demo into an open-ended, co-evolving ecosystem simulator:
> energy, reproduction and death instead of generations; sparrows *and* hawks;
> heritable body traits; a deterministic core that produces identical results
> natively and in WebAssembly; a headless CLI with a benchmark and a learning
> test that CI runs on every push. The new code lives in `crates/`, the design
> in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and
> [`docs/decisions/`](docs/decisions/). The text below describes the currently
> deployed original app, which stays live until the new frontend replaces it.

## Neural Network Bird Simulation

----
**Simulation of Evolution**

Powered by Neural Networks and Genetic Algorithms, rustyWings simulates a world where triangular birds navigate their environment in search of food represented by circles.

----

**About**

* Each bird has:
* An eye with a limited field of vision visualized as a circle around the bird.
* A neural network brain that determines its movement direction and speed.
* The simulation starts with randomly initialized brains for each bird.
* After 2500 turns (approximately 40 seconds), birds with the most food are selected for reproduction. Their offspring forms the next generation.
* Over generations, the birds, through a genetic algorithm, become adept at finding food - it's like they're learning on their own!
* **Note:** This is not the boids algorithm. Bird behavior is not pre-programmed, it's learned over time.

----

**Interaction**

Influence the simulation by entering commands in the input box:

* `train (t)`: Fast-forwards the simulation to observe rapid learning.
* Explore the source code: [https://github.com/mahim37/rustyWings](https://github.com/mahim37/rustyWings)

**Enjoy the simulation!**

----

**Commands**

* `p / pause`: Pauses or resumes the simulation.
* `r / reset [parameter=value ...]`: Restarts the simulation with optional parameters:
* `a / animals`: Number of birds (default: ${config.world_animals})
* `f / foods`: Number of food items (default: ${config.world_foods})
* `n / neurons`: Number of brain neurons per bird (default: ${config.brain_neurons})
* `p / photoreceptors`: Number of eye cells per bird (default: ${config.eye_cells})
* Examples:
* `reset animals=100 foods=100`
* `r a=100 f=100`
* `r p=3`
* `(t)rain [generations]`: Fast-forwards one or more generations.
* Examples:
* `train`
* `t 5`

----

**Advanced Tips**

* Modify any parameter within the `reset` command.
* Examples:
* `r i:integer_param=123 f:float_param=123`
* `r a=200 f=200 f:food_size=0.002`
* Parameter names can be found in the source code.

----

**Interesting Scenarios**

* `r i:ga_reverse=1 f:sim_speed_min=0.003`: Birds avoid food (try to escape?)
* `r i:brain_neurons=1`: Single-neuron "zombie" birds
* `r f:food_size=0.05`: Larger food items
* `r f:eye_fov_angle=0.45`: Birds with a narrow field of view

----

**Note:**

* `${config.world_animals}`, `${config.world_foods}`, `${config.brain_neurons}`, and `${config.eye_cells}` represent default values defined in the code.
**A tiny evolving world.** Sparrows eat seeds. Hawks eat sparrows. Nobody is
programmed to do anything: every bird's brain is inherited, mutated, and kept
only if it worked.

Live: **https://rusty-wings-iota.vercel.app**

rustyWings is an open-ended artificial-life simulation. Thousands of birds
share a wrapping world with seeds that regrow in drifting patches. Each bird
has an energy budget (moving, seeing and being alive all cost energy), a body
plan it can pass on (field of view, sight range, top speed, size) and a small
neural network that turns what its retina sees into steering. Birds with
enough energy have chicks; birds that run out die. There are no generations
and no fitness function. What you see is what survived.

The core is a Rust crate compiled both natively and to WebAssembly, and it is
deterministic to the bit: a seed means the same world on Linux, macOS,
Windows and in your browser. The checksum in the app's status bar is the same
number the command-line tool prints for the same seed and tick.

## What is in the box

```
crates/rustywings-core the simulation: no I/O, no threads, no platform math
crates/rustywings-cli rustywings run | bench | verify | arena | config
crates/rustywings-wasm the browser facade
web/ Vite + TypeScript frontend: Web Worker, WebGL2, SVG charts
docs/ ARCHITECTURE.md and the decision records
design/mockups/ the HTML mockups the UI was chosen from
```

`libs/` and `www/` hold the original tutorial-derived app and are scheduled
for removal.

## Run it

Rust stable with the `wasm32-unknown-unknown` target (pinned by
`rust-toolchain.toml`), [wasm-pack](https://rustwasm.github.io/wasm-pack/),
Node 22+ and pnpm.

```bash
# the browser app
cd web && pnpm install && pnpm build:wasm && pnpm dev

# the same world, headless
cargo build --release -p rustywings-cli
./target/release/rustywings run --seed 1 --ticks 60000 --every 500 --out stats.csv
./target/release/rustywings verify --seed 1 --ticks 2000 # prints the checksum the app shows
./target/release/rustywings arena --seeds 3 --ticks 30000 # do evolved birds beat random ones?
./target/release/rustywings bench --agents 5000 --ticks 200
```

The "Copy config" button in the app produces the JSON that `rustywings run
--config` reads, so a world tuned in the browser can be run for a million
ticks on the command line.

## How it is checked

- `cargo test --workspace`: unit tests, a golden checksum for seed 1 at
tick 2,000 (run on three operating systems in CI), and an ecology test
that both species must survive 6,000 ticks without the rescue rule.
- `rustywings arena`: a standardised foraging arena scores a genome. CI
fails if sparrows evolved for 30,000 ticks do not out-eat random ones.
- `web`: vitest loads the built wasm in Node and asserts the same golden
checksum, then type-checks and builds the site.

Design rationale lives in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and
[`docs/decisions/`](docs/decisions/).

## Credit

The idea of evolving neural-network birds with a retina of angular cells
comes from Patryk Wychowaniec's *Learning to Fly* series. The ecosystem,
the deterministic core, the tooling and the frontend here are original work.

MIT licensed, see [LICENSE.md](LICENSE.md).
Loading
Loading