anthony taguest·sydney --:--
← projectscase study

riichi

A browser riichi mahjong game built to teach the game — not just host it.

live ↗ riichi.anthonyta.devcode ↗SvelteKit · Neon · Clerk · Rust/WASM · Claude

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 GameState value is the single source of truth, and the rules are pure (state) → state functions 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