Skip to content

BratPokerTable decomposed — the most-visible file made the easiest to change

2026-07-16

packages/game-ui/src/BratPokerTable.tsx was 1,588 lines — the default poker felt every player and every agent-owner looks at, ~4× the project's 400-LOC cap, with eleven distinct units sharing one module scope. Two of its internal units each exceeded the whole-file cap: the Seat component (~403 lines) and the orchestrator body (~650). The cost was not aesthetic — it was modification risk on the highest-visibility surface with zero safety net: game-ui had no render-test harness at all, so nothing pinned the felt's behavior. Every change shipped unverified, caught only by a human watching a live table.

This was a pure structural refactor — zero behavior change, zero pixels moved. The order was load-bearing: a characterization harness and tests came FIRST, then the decomposition proceeded under green.

What shipped

  • A reusable game-ui RTL/jsdom render harness (vitest.config.ts + a manual expect.extend setup mirroring apps/poker — dodging the multi-vitest-version jest-dom matcher maze, codebase-velocity-audit F1). It retroactively covers the bust-countdown / waiting-felt port that had shipped without a game-ui render test.
  • An 11-assertion characterization suite (BratPokerTable.characterization.test.tsx, A1–A11) pinning the felt's render across representative states — live hand, showdown ceremony, bust rebuy countdown (fake timers), waiting-for-players, acting seat, reserved/empty seats, all-in, folded. Authored once against the monolith, green before the first extraction and unchanged and green after the last — that frozen-suite-still-green property IS the proof of zero drift (A12).
  • The monolith decomposed into a cohesive file tree, mirroring the compact/ precedent: types.ts / constants.ts / cards.ts / hooks.ts, leaf components (CardTile, MarkerBadge, ActionBar, DealerChips), a top-level Seat.tsx split into private sub-parts (SeatCard, EmptySeat, SeatCards, SeatBadges) each ≤ cap, the felt (TableFelt + BetLayer), and a useBratTableState hook holding all derived + animation state.
  • BratPokerTable.tsx went 1,588 → 213 LOC — orchestration only (call the hook, render connect-hint + header + <TableFelt> + controls). No file in the tree exceeds 400 LOC.

Result

The felt every player looks at is now the easiest file to change safely instead of the hardest — any future brat-felt change extends the characterization suite (never edits its assertions) and lands under a render-test safety net game-ui did not have before.

See the plan at .indusk/planning/brat-poker-table-refactor/.