Profiled a long night at a full table — 24 seats, 200 logged hands — because
that's the state the tool is slowest in and the one a dealer reaches at 2am.
A full re-render cost 30ms, and it runs after every single action. Broken down,
25.7ms of that was renderHandHistory: tearing down and rebuilding 200 rows of a
panel that hadn't changed and that nobody is looking at mid-hand. Everything
else on the table put together came to about 4ms.
The log only changes when a hand is settled or the history is cleared — maybe
once every thirty taps. It now carries a cheap signature (length, hand counter,
and the newest row) and is skipped when that hasn't moved. Entries are immutable
once written and only ever added at the front, so the signature catches a
recorded hand, a cleared history, and an undo that puts a hand back.
renderAll, every action 30.07ms -> 4.18ms
renderHandHistory, skipped 25.74ms -> 0ms
renderHandHistory, rebuild 25.74ms -> 9.05ms
The rebuild got faster too, for the times it does run — once per hand, where
25ms of jank lands exactly on the "next hand" tap. toLocaleTimeString was the
single most expensive call in the loop and a logged hand's timestamp never
changes, so the formatted string is kept; and the rows are built into a fragment
so the panel lays out once instead of two hundred times.
A cache like this is only worth having if it can't go stale — a dealer reading
last hand's payouts while settling this one is worse than a slow panel. 10 tests
cover the invalidation directly: unchanged through a street of betting, changed
by settling, by clearing, by an undo back to a shorter log, by two hands that
differ only in the pot, and by the counter moving on.
Validated with Playwright against the real page: the rendered rows match the
stored history through settling a hand in the wizard, undoing it, clearing the
log and recording again. Touch and layout guarantees from the earlier rounds
still hold — 0 of 87 controls on touch-action auto, 0 tap-highlighted, every
stack still inside its own puck. 109 tests passing.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
86 lines
3.0 KiB
JavaScript
86 lines
3.0 KiB
JavaScript
// Loads the dealer's browser modules into Node so they can be tested directly.
|
|
//
|
|
// The modules are plain script-tag files: each one declares a `const` namespace and
|
|
// ends with a `module.exports` guard. In the browser they find each other as globals,
|
|
// so the same names have to exist on `globalThis` here or `Betting` can't reach
|
|
// `HoldemState`. Load order matches the <script> order in TexasHoldem.cshtml.
|
|
const path = require("path");
|
|
|
|
const SRC = path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem");
|
|
|
|
function load(name) {
|
|
const mod = require(path.join(SRC, `${name}.js`));
|
|
globalThis[name.charAt(0).toUpperCase() + name.slice(1)] = mod;
|
|
return mod;
|
|
}
|
|
|
|
const Money = load("money");
|
|
const PotCalculator = load("potCalculator");
|
|
const SplitCalculator = load("splitCalculator");
|
|
const Betting = load("betting");
|
|
const HandHistoryExport = load("handHistoryExport");
|
|
|
|
// render.js exports { Render } rather than the namespace directly.
|
|
const Render = require(path.join(SRC, "render.js")).Render;
|
|
globalThis.Render = Render;
|
|
|
|
// state.js exports `HoldemState`, which doesn't match its filename.
|
|
const HoldemState = require(path.join(SRC, "state.js"));
|
|
globalThis.HoldemState = HoldemState;
|
|
|
|
// ---- Table builders ----------------------------------------------------------
|
|
|
|
// A table with `seatCount` chairs where `seated` (a list of seat indexes) are dealt
|
|
// in with `stack` chips each. Everything else is the app's own defaults, so a test
|
|
// that forgets to set a field gets whatever a real dealer would get.
|
|
function makeTable({ seatCount = 6, seated = null, stack = 100, smallBlind = 1, bigBlind = 2, dealerSeatIndex = 0 } = {}) {
|
|
const state = HoldemState.createDefault();
|
|
HoldemState.resizeSeats(state, seatCount);
|
|
state.smallBlind = smallBlind;
|
|
state.bigBlind = bigBlind;
|
|
state.dealerSeatIndex = dealerSeatIndex;
|
|
|
|
const sitting = seated || state.seats.map((_, i) => i);
|
|
state.seats.forEach((player, index) => {
|
|
player.inHand = sitting.includes(index);
|
|
player.stack = sitting.includes(index) ? stack : null;
|
|
player.name = `P${index + 1}`;
|
|
});
|
|
return state;
|
|
}
|
|
|
|
// Total chips the table is accountable for: what's in front of everyone, plus what
|
|
// they've pushed in this street, plus what's already banked into the pot.
|
|
function chipsOnTable(state) {
|
|
return Money.round(
|
|
state.seats.reduce(
|
|
(sum, p) => sum + (Number(p.stack) || 0) + (Number(p.committed) || 0) + (Number(p.amountOverride) || 0),
|
|
0
|
|
)
|
|
);
|
|
}
|
|
|
|
// Contributions in the shape PotCalculator wants them.
|
|
function contributions(state) {
|
|
return state.seats
|
|
.filter(p => p.inHand)
|
|
.map(p => ({
|
|
id: p.id,
|
|
contribution: Betting.handTotal(p),
|
|
folded: p.folded,
|
|
}));
|
|
}
|
|
|
|
module.exports = {
|
|
Money,
|
|
Render,
|
|
HandHistoryExport,
|
|
PotCalculator,
|
|
SplitCalculator,
|
|
Betting,
|
|
HoldemState,
|
|
makeTable,
|
|
chipsOnTable,
|
|
contributions,
|
|
};
|