A Rust workspace of terminal mini-games, built for parallel development: each game is an isolated crate. When you build a game, stay inside your crate and touch nothing else except the two registry lines below. This keeps concurrent work on other games conflict-free.
crates/
core/ game_core — the Game trait, GameContext, registry. DO NOT EDIT.
app/ cli-games — launcher TUI (menu + frame loop). DO NOT EDIT.
games/
_registry/ games_all — umbrella; the ONLY shared file you append to.
snake/ game_snake — reference game / template. Read it, don't change it.
<game>/ one crate per game — your work lives here.
The launcher discovers games at link time via the inventory crate, so a game
registers itself with the register_game! macro — there is no central list to edit.
-
Scaffold — copy the template:
cp -r crates/games/snake crates/games/<game>
In
crates/games/<game>/Cargo.tomlsetname = "game_<game>". Leave the inherited workspace fields as-is. -
Implement
game_core::Gameincrates/games/<game>/src/lib.rs:fn new() -> Self— fresh state.fn update(&mut self, ctx: &GameContext) -> Transition— advance one tick.fn render(&mut self, frame: &mut Frame, area: Rect)— draw with ratatui.- optional
fn tick_rate(&self) -> Duration(default 50 ms). - End the file with a
register_game! { Type, id: "...", name: "...", description: "...", author: "..." }.
-
Register (the only shared edit — append alphabetically, never reorder):
crates/games/_registry/Cargo.toml→[dependencies]:game_<game> = { path = "../<game>" }crates/games/_registry/src/lib.rs:use game_<game> as _;
Full walkthrough with code: docs/ADD_A_GAME.md.
- Exit to menu on
q/Escby returningTransition::Exit. The runner already handles Ctrl+C globally. - Timing: accumulate
ctx.dtto drive game speed. Never assume a fixed tick. - Input: read it via
ctx.pressed(KeyCode::...)/ctx.keys(). KeyCode is re-exported fromgame_core. - Dependencies: a game depends only on
game_coreandratatui(both.workspace = true). Don't addcrossterm/inventorydirectly — they're re-exported bygame_coreso versions stay unified. Adding any other dep needs a[workspace.dependencies]entry; flag it rather than diverging. - No
unsafe, no panics in the game loop. UseResult-free logic insideupdate/render; handle bad state gracefully (the runner can't recover a panic). - Keep the game self-contained: no global state, no reading/writing files unless
asked. Match the style and comment density of
crates/games/snake/src/lib.rs.
cargo build -p game_<game> # fast iteration on just your crate
cargo build # whole workspace must compile
cargo run -p cli-games -- <game> # launch your game directly
cargo run -p cli-games # menu — confirm your game appears
cargo clippy -p game_<game> # keep it warning-clean
cargo fmt -p game_<game> # format ONLY your crate — never bare `cargo fmt`Never run bare cargo fmt, cargo clippy --fix, or any workspace-wide
formatter. They rewrite shared files (app/, core/, other games) and pollute
your PR with conflicts. Always scope with -p game_<game>. Before committing,
run git status — the ONLY paths you may have changed are crates/games/<game>/,
crates/games/_registry/ (two append lines), and Cargo.lock. If anything else
is modified, revert it: git checkout -- <that-path>.
Your game is done when: it builds clean, appears in the menu, plays, and returns
to the menu on q/Esc.
Give each game its own git worktree so builds and commits don't collide. The simplest way is Claude Code's built-in flag — it creates the worktree, starts a session in it, and cleans up on exit:
claude --worktree <game> "Implement the '<game>' mini-game. Read CLAUDE.md and \
docs/ADD_A_GAME.md, then add ONLY crates/games/<game>/ plus the two append-only \
registry lines. Verify: cargo build && cargo run -p cli-games -- <game>."(Or manually: git worktree add ../cli-games-<game> -b game/<game>.)
Each agent works in its one game only. Merges stay conflict-free because every agent adds files plus append-only registry lines — nothing shared is rewritten.