hyhy.gg

Riichy

A table-draw manager for my local riichi mahjong club that optimises for fairness. It went from a blank repo to a deployed product in one afternoon.

Live at riichy.hyhy.gg

  1. 12:35

    Initial commit from Create Next App

  2. 14:28

    Add riichi table-draw manager: the working app, with engine, UI and tests

  3. 15:04 – 17:42

    Polish: header and dark mode, copy pass, favicon and OG metadata, README

Under two hours from a blank scaffold to a working optimiser with tests. Finished, deployed and documented by the end of the afternoon.

What it does

The organiser enters the players and the number of hanchan (rounds). Riichy then generates the seating for the whole event: who sits at which table, in which seat, for every round. It works through three fairness goals in order. First it keeps repeat pairings to a minimum. Then it rotates who sits at three-player (sanma) tables when the player count isn't a multiple of four. Last, it spreads out the high-value seats.

Once a draw is generated, the only control is re-roll. You can't edit the draw by hand. There's no backend and no accounts, and the current event is saved in localStorage. I built it for a club I actually go to, so it had a real user with a need that comes round every week.

Fairness stats for each player, covering pairings, seat counts and sanma rotation, so the organiser can check the draw for themselves.
Fairness stats for each player, covering pairings, seat counts and sanma rotation, so the organiser can check the draw for themselves.

How it's built

  • 36 tests and 1,192 assertions over the draw engine and event state. The engine is plain TypeScript with no React or DOM dependency, and storage sits behind an interface that tests can swap out.
  • A seeded best-of-N random search with near-lexicographic scoring weights, so the optimiser never accepts a repeat pairing in exchange for better seats. The seed is saved with the event, so any draw can be traced back.
  • A CONTEXT.md glossary of terms (organiser, hanchan, yonma, sanma, pairing, re-roll), used the same way in the code, the UI and the docs. Invariants are written down, and features I chose to leave out are recorded as decisions.

Stack

  • Next.js 16.3 (App Router)
  • React 19.2
  • TypeScript 5 strict
  • Bun
  • shadcn/ui on Base UI
  • Tailwind v4
  • No database or auth, runs fully in the browser, hosted on Vercel