a library relocates the bug
updated May 29, 2026
Mahjong scoring is the genuinely hard part of a mahjong app — dozens of yaku, fu edge cases, exact point tables. So riichi doesn’t compute it. It hands scoring to a Rust/WASM library validated against millions of real hands, and spends its own effort on game flow and teaching. “Don’t reinvent the wheel” — obviously right.
What the cliché leaves out: a trusted dependency doesn’t delete your scoring bugs, it relocates every one of them to the calling boundary. Ron never fired for weeks because I passed 14 tiles where the library wanted 13 (the winning tile goes in a separate field), and a broad catch swallowed the exception, so it looked like “this hand just doesn’t win.” A closed tsumo silently lost its pinfu because I built the hand with the winning tile not last, and the library reads the last tile as the drawn one. Ura-dora came out mislabelled because the library has a single dora bucket and I folded both kinds into it. Three separate bugs, none of them in the library — all of them in the two inches of code where my types met its API.
So the engineering went into that boundary. I encode tiles in exactly the library’s own 1–34 ordering, so there’s no translation layer between us to mis-map an honour tile. And the golden tests inline the real WASM and score real hands through it — not a mock. Mocking the scoring engine would have hidden precisely the bugs that actually happen, because the bug was never the math; it was the handshake.
The rule I took away: when you adopt a dependency to de-risk the hard part, your risk concentrates at its API surface — and that’s the surface your tests have to exercise for real. Never mock the oracle you reached for because you couldn’t verify it by hand. Mock everything else, but let the thing you don’t fully trust run.
the hub · warm terminal