kifu
Tracks riichi mahjong stats from Tenhou game logs, for single games and whole careers.
Overview
kifu is a stats tracker for riichi mahjong games played on Tenhou. You add a game by its log ID, and it shows the hands, scores and stats for that game and for each player's career.
Problem
Tenhou keeps detailed logs of every game, but its own pages show almost none of it. Questions like how often I deal in or which yaku I win with had no easy answer.
The logs are XML with compact codes for every draw, discard and call, so they need a real parser before they are useful.
Requirements
- Add a game by its Tenhou log ID
- Show per-game details and career stats per player
- Never send Tenhou bursts of requests
- Let people sign in with Google and choose which saved games are public
Architecture
A Svelte web app talks to a Rust API running on Cloudflare Workers. The API stores games and users in D1. Log fetches go through a Durable Object that runs one fetch at a time. The parser and the stats live in a separate domain crate that both the API and the tests use.
+-----------+ +--------------------+ +-----------+
| Svelte | -----> | Rust API Worker | -----> | D1 |
| web app | | (games, players) | | database |
+-----------+ +--------------------+ +-----------+
|
v
+--------------------+ +-----------+
| fetch queue (DO) | -----> | Tenhou |
| 1 s between calls | | logs |
+--------------------+ +-----------+Technology choices
- Rust
- One language for the parser, the stats and the API.
- Cloudflare Workers and D1
- Cheap to run, with the database next to the API.
- Durable Objects
- One object per queue makes it easy to space out calls to Tenhou.
- Svelte
- Small, fast pages for charts and tables.
Implementation
The domain crate parses Tenhou's XML into hands, calls and results, then counts wins, deal-ins, riichi, calls, han, fu and yaku. The API stores parsed games in D1 and serves game, player and career views.
- A Durable Object waits at least one second between Tenhou fetches.
- Sign-in uses Google OAuth with signed sessions.
- A codegen crate writes the TypeScript types the web app uses.
Major challenges
- Reading the compact Tenhou log codes correctly for every kind of call
- Keeping fetches spaced out even when many people add games at once
- Showing a lot of numbers without making the pages hard to read
Testing
The domain crate is tested against a real Tenhou log checked into the repository. The web app has unit tests for the hand ledger, player names, query parsing and the OAuth flow, and GitHub Actions runs the checks and the deploy.
Results
- Live at kifu-app.pages.dev
- Shows per-game hands and career stats such as deal-in matrices, score trends and yaku counts
What I'd improve
- Import many games at once from a player's history
- Compare two players side by side