anthony taguest·sydney --:--
← projectscase study
riichi
A browser riichi mahjong game built to teach the game — not just host it.
the problem
Riichi mahjong is genuinely hard to learn, and good resources are scarce. Every client I tried competes on features and polish — they're places to grind games, not to understand them. I wanted the tool I wished existed: somewhere you learn why a play is good, not just rack up hands.
what I built
- •Solo riichi — one human against three AI, no account to start. Full, real rules and scoring: riichi (double riichi, ippatsu), tsumo/ron, calls and kans, furiten, every dora type, the situational yaku, and Mahjong-Soul-accurate game-end (dealer renchan, tobi, the 30k target + sudden-death overtime).
- •Two tiers of deliberately beatable rule-based AI — learning to read and punish a weaker opponent's mistakes is part of the point.
- •A learning layer: an in-round helper (one grounded discard recommendation), a post-game overview, a daily “best discard” puzzle with streaks, and tile-level deal-in review that names the exact tile and whether it was avoidable.
the interesting part
A working mahjong game is mostly a correctness problem — the rules are notoriously deep. The engineering I'm proudest of is about where the correctness lives and how the teaching stays honest:
- •The model teaches; the libraries are right. Every teaching feature is grounded in provably-correct computation, never the model's guess. Shanten and ukeire come from a dedicated efficiency library; yaku and scoring from a Rust/WASM library. The Hand of the Day's correct answer is computed— so it can't be wrong — and Claude only writes the explanation. Keeping the model in its lane (narration, not correctness) is the whole design.
- •A pure, deterministic engine. One
GameStatevalue is the single source of truth, and the rules are pure(state) → statefunctions with no mutation. That makes the full ruleset testable without a framework, keeps reasoning sane while three AI turns interleave asynchronously, and gives me a deterministic substrate I can replay exactly. - •Replay → a universal log → real analysis. The engine emits a semantic event stream, and a game persists as a compact seed-plus-inputs tape that re-derives byte-for-byte. From it I export MJAI— mahjong's equivalent of chess PGN — so any saved game is readable by a reviewer tool like Mortal, or pasted into an LLM. The same event stream powers tile-level review: it rebuilds exactly what you could see at each costly deal-in, computes which tiles were actually safe mechanically, then asks Claude only for the verdict — avoidable, justified, or unlucky.
- •Knowing what not to build. The correctness-critical maths is delegated to battle-tested libraries, not hand-rolled. The AI opponents are hand-written rules, not Claude — they act dozens of times a game, so a model would blow the budget for zero teaching value. Claude's budget is reserved for the one thing only it does well: explaining.
what I'd change next
- •Improvement tracking — win rate, deal-in rate, and hand-efficiency trends over time.
- •Cache the tile-review verdicts on the game row — a re-click currently re-pays for the analysis.
- •Widen tile-level review beyond deal-ins to riichi declarations and big push/fold junctions — same event-stream extractor.
The model never decides what's correct here — the engine and the libraries do. It just explains. Drawing that line was the whole design.
the hub · warm terminal