A browser auto-battler built in Rust and Bevy, then demade for the Game Boy Advance and the Nintendo 64. All three run the same rules code, so the two consoles play the same match from the same seed.
Every rule lives in one Rust crate, sim: units, shop, economy, synergies, combat, the bots. It uses integers only (#![deny(clippy::float_arithmetic)]), and has no clock, no file system, no threads and no OS randomness. A seed and player commands go in; events come out. The clients only draw what the events say happened.
That rule was written so replays and headless balance runs would be easy. It also meant a console port is mostly a new way of drawing.
sim (the rules: 24.6k lines of Rust, no_std + alloc, integers only)
│
┌───────────────┼──────────────────────────────┐
│ │ │
protocol, records lite (2.8k: console runner: pacing, bots spread over
server (axum) │ frames, HUD and codex as data, party mode, match hash)
│ ├──────────────┐
game (Bevy, 34.8k) │ │
web · desktop · gba (agb, n64/simcore (3.1k: C ABI facade)
Android 5.7k Rust) │ cargo → wasm32 → wasm2c → 8.6 MB of C
│ │
thumbv4t-none-eabi libdragon + tiny3d client (9.2k lines of C)
ARM7TDMI, 16.8 MHz VR4300, big-endian
The GBA takes the crate directly: sim is no_std with std as a default feature, so the cartridge builds it for thumbv4t-none-eabi with default-features = false. The N64's MIPS CPU has no comfortable Rust target, so the rules take a detour:
mmap, explicit bounds checks.memory.grow cost 179 ms a page. A whole match peaks near 60 KB.Both consoles advance the sim 20 times a second: the GBA one tick every three frames at 60 fps, the N64 two every three at 30 fps. An autoplayed seed 42 is therefore the same match on both. A 64-bit FNV hash of every event is pinned in the tests on all three:
lite::SEED_42_HASH = 0x1302295732caf6bb
The N64 build generates its copy of the pin from the Rust constant, the ROM prints it at boot, and an emulator test fails unless the match ends on it. No hash is copied by hand.
| Browser · desktop · Android | GBA | N64 | |
|---|---|---|---|
| Rules: units, shop, synergies, combat | sim | sim | sim via wasm2c |
| Roster (74 units, 13 guilds, 10 banners) | yes | yes | yes |
| Bots and their skill tiers | yes | yes | yes |
| Seeded RNG streams | yes | yes | yes |
| Console runner: pacing, HUD and news as data | no | lite | lite |
| Art source: the 3D models, baked card art | yes | sprites cut from it | models reduced from it |
| Match hash | no | pinned in tests | printed by the ROM, checked |
| Full game | GBA | N64 | |
|---|---|---|---|
| Renderer | Bevy, PBR, glTF characters | tiles, 16×16 sprites | tiny3d, 296–549 triangle characters |
| Input | mouse, touch, drag and drop | D-pad cursor | stick cursor, four pads in party mode |
| Players | 2–16, online rooms | you + 3 bots | you + 3 bots, or 4 humans in party mode |
| Items and loot | yes | no | no |
| Languages | English, Norwegian | English | English |
| Saves | SQLite, local storage, server | 16 bytes of SRAM | 16 bytes of EEPROM |
The consoles play a match the full game could play too: the same sim with a different MatchConfig (a roster drawn from the seed, no loot). Party mode's rules are opt-in fields in sim itself, off by default.
One script regenerates everything a new unit needs on every platform, and CI checks nothing is missing.
The GBA cartridge plays on an Analogue Pocket through its openFPGA GBA core, and the N64 ROM plays on an Analogue 3D through a SummerCart64. Both are tested and working, as well as in mGBA and ares. Download them.
The game, the server, both demakes and their toolchains were built pairing with Claude Code: steering, testing and making the calls on one side, writing most of the code on the other. The architecture rule is what made that pace safe. Every port is a thin client over code that is already tested, and the hashes catch any drift.