Write Game Boy Advance games in tish (a TS/JS-like language that transpiles to Rust), running on agb 0.25.
Documentation for game developers: chuggie.dev/docs — installation, packages, engine guides, and examples. This repository holds contributor docs (ARCHITECTURE.md, CONTRACT.md, docs/).
Two layers, both usable:
tish-agb— low-level bindings (sprites, backgrounds, input, audio, save, timers, rng, log). Enough to build a whole game by itself.tish-gba-game-engine— an optional RPG-Maker-class framework on top: entities + components with Unity-likestart/updatebehaviours, a fixed per-frame pipeline, dialogue, scenes, and pluggable genre modules (grid RPG, platformer, isoboard battles, versus fighting). Framework games can always drop down totish-agb/ raw agb.
Layer map and dependency rules are in ARCHITECTURE.md; the
plan and phasing live in the approved strategy doc; the cross-track interface is
pinned in CONTRACT.md; spike results in
docs/findings/P0-findings.md.
Screenshots (headless): scripts/screenshot.sh <rom.gba | src/main.tish> [out.png] [frames]
renders a ROM in libmgba with no window (no display / screen-recording permission), so it works
locally and in CI — see .github/workflows/screenshot.yml.
scripts/gif.sh takes the same arguments and records the run as a looping animated GIF instead,
for everything a still cannot show — movement, animation, transitions, particles.
Working on an example: the whole build → drive-headlessly → self-play → strip-probes loop, with
the traps that cost the most time, is written up in
docs/agent-dev-loop.md.
Contributor deep dives (user-facing versions on chuggie.dev):
| Topic | Contributor doc | User doc |
|---|---|---|
| Backgrounds | docs/gba-backgrounds.md |
chuggie.dev/docs/engine/backgrounds |
| Performance | docs/perf-rules.md |
chuggie.dev/docs/advanced/performance |
| Audio | docs/gba-audio.md |
chuggie.dev/docs/engine/audio |
| Memory | docs/MEMORY.md |
chuggie.dev/docs/advanced/memory |
scripts/const_to_let.py applies the biggest perf fix in bulk (see perf-rules).
Pre-1.0 — production demos and CI, not a finished public 1.0 API.
- 120+ examples, 50+ authoring packages, headless mGBA verify in CI
- SoA ECS (
tish-gba-game-engine) + low-leveltish-agbbindings on agb 0.25 - Genre kits (shmup, platformer, top-down, iso, fighter, UI, deck, …)
- npm / crates.io publish still gated (see release workflows); versions are
0.x
Early P0 spikes (hand-written ROM, hecs rejected for atomics → custom SoA) are
documented in docs/findings/P0-findings.md.
Version bumps and GitHub prereleases are driven by sem
(tishlang/sem@v1 in CI). Config lives in .semrc.json.
Use Conventional Commits on main:
| Commit | Release |
|---|---|
feat: |
minor |
fix: / perf: |
patch |
feat!: / fix!: / BREAKING CHANGE: footer |
major |
chore: / docs: / ci: / … |
none |
Flow: green main → CI creates a prerelease with the npm tarball → promote the GitHub
release (uncheck pre-release) → npm-release.yml and crates-release.yml publish.
Preview locally (optional): npx @tishlang/sem --dry-run from the repo root.
- Node.js 22+ and npm — games are driven through npm scripts (they use the
tishCLI, which ships as the@tishlang/tishnpm package). - Rust nightly with
rust-src(pinned viarust-toolchain.toml) +agb-gbafix(cargo install agb-gbafix) — the tish GBA build shells out to cargo under the hood. mgba-qt/mgbaonPATHto play ROMs (brew install mgba).
Every example is a normal npm project that depends on the tish CLI. Pick one and:
npm install # once, at the repo root — links the tish CLI for all examples
npm start -w fonts-demo # build the ROM and open it in mGBAor from inside an example:
cd examples/fonts-demo
npm run build # → fonts-demo.gba
npm start # build + open in mGBA
npm run shot # build + headless screenshot.png (no window; for CI / quick checks)
npm run gif # build + headless screenshot.gif — an animated clip of the same run
npm run clean # remove build artifactsThat's it — no long tish build … --target gba -o … incantations, no separate emulator command.
Every example has a Build and a Play action. Play runs the ROM already on disk and never builds, so it opens instantly — build only when you changed something.
- Run and Debug sidebar: pick
▶ Play <example>once; the green play button / F5 replays it. - Run Task (⇧⌘P):
Play: <example>/Build: <example>for any example, plusPlay: current example/Build: current example, which use whichever example the open file belongs to (editingpackages/*.tish, they fall back to the last example you ran).Build: current exampleis the default build task, so ⇧⌘B builds what you are looking at.
The same thing from a terminal, and what the actions actually call:
scripts/rom.sh play akari # run examples/akari's ROM — no build
scripts/rom.sh build akari # build it
npm run vscode # regenerate .vscode/*.json after adding an examplePlay always starts mGBA windowed — mGBA otherwise remembers fullscreen and re-enters it for every
ROM, and macOS puts a fullscreen window on its own Space, which hides your editor. Window size is
whatever you last dragged it to; MGBA_ARGS="--scale 4" (or any other emulator flags) and
MGBA=/path/to/emulator override the rest.
Local dev against a sibling
tishcheckout. GBA support may not be in the published@tishlang/tishyet, so the examples reference the tish CLI locally (afile:dependency on../tish/tish/npm/tish). Runnpm run setuponce (it builds that checkout'stishand makes it installable); thennpm installuses your local build. A real game just depends on the published@tishlang/tishand skips this step. Pointnpm run setupelsewhere withTISH_REPO=/path/to/tish.
examples/p0-spike is the P0a spike — hand-written no_std Rust (what tish codegen emits), built with
cargo rather than tish. It has npm scripts too (npm run build -w p0-spike), or directly:
cd examples/p0-spike
cargo build --release
agb-gbafix ../../target/thumbv4t-none-eabi/release/p0-spike -o p0-spike.gba
cargo run --release # boot interactively (streams agb::println! to the terminal)Package any example as an HTML5 build (ROM + mGBA WASM player) plus cover art from a headless screenshot:
npm run itch -- publish shmup
# → dist/itch/shmup/shmup-html5.zip
# dist/itch/shmup/cover.png (630×500, upload as Cover Image)
# dist/itch/shmup/embed-bg.png (480×320, Click-to-play background)Then on the itch game edit page: Kind = HTML, upload the zip, set embed size
480×320, enable SharedArrayBuffer / cross-origin isolation, upload
cover.png, and set the Click-to-play background to embed-bg.png.
Optional butler push (directory, not the zip):
ITCH_TARGET=you/your-game:html5 npm run itch -- publish shmupLocal preview (COOP/COEP headers for mGBA WASM threads):
npm run itch -- serve shmup # http://127.0.0.1:4173/
npm run itch -- serve shmup --port 8080See also templates/itch-mgba/README.md.
| Variable | Default | Purpose |
|---|---|---|
TISH_REPO |
../tish/tish |
Path to tish checkout for npm run setup |
MGBA |
mgba-qt / mgba |
Emulator binary |
MGBA_ARGS |
(empty) | Extra emulator flags |
MGBA_PREFIX |
Homebrew prefix | libmgba for headless tools |
ITCH_TARGET |
(unset) | Butler push target |
PORT |
4173 |
itch serve port |
TISH |
npx tish |
Override tish CLI in screenshot scripts |
Cargo features on tish_agb: save-flash-512k, save-flash-1m. See crates/tish-agb/Cargo.toml.
.cargo/config.toml # thumbv4t target, build-std, gba.ld rustflags, mgba runner
rust-toolchain.toml # nightly + rust-src
Cargo.toml # workspace + GBA-tuned profiles (opt-level 3, fat LTO)
CONTRACT.md # compiler ⇄ framework interface (breaking changes tracked here)
DOCUMENTATION.md # where docs live and how to keep them in sync
docs/findings/ # P0 spike results + preserved probes
examples/p0-spike/ # P0a: hand-written "what codegen emits", builds to a ROM
templates/itch-mgba/ # mGBA WASM HTML shell for itch HTML5 packages
crates/ (tish-agb, tish-agb-macros, tish-gba-game-engine, host asset importer) land from
P1 onward. See CONTRACT.md "Workspace host/target split" for why host tooling
isn't a plain workspace member.