The card picker greys out a card that is already dealt and hangs the reason off a `title`. Measured on an iPhone with three hands out: seven of the fifty-two were dead, none of them could take focus, and tapping one changed nothing at all — no toast, no text, not a byte of DOM. The tool's answer to "who has the ace of spades?" existed and was reachable only with a mouse. The header's two buttons were the same, on a new dealer's first screen, and Export CSV was greyed out with no explanation written anywhere. `disabled` is the wrong attribute whenever there is something to say: it drops the control out of the tab order, so a screen reader never lands on it to hear the description, and it eats the tap on a touch screen, where the hover that would have shown the `title` does not exist. `aria-disabled` announces the same state and leaves the control reachable, so one capture listener can turn the press into a sentence — in a toast, or inside a sheet that is standing on top of the toast. It paid for itself in the picker's keyboard too. The arrows hunted past dealt cards because a `disabled` button cannot hold focus, and the hunting did the exact thing the grid's no-wrap rule was written to prevent: Right from A-spades over a dealt A-hearts landed on A-diamonds, so the column shifted and the next press of Down walked a different suit. A cell that can hold focus needs no hunting, and the APG asks for that inside a composite widget anyway. The line that answers is one line by measurement, not by taste. Its box is 318px on a Galaxy S8 at 20px of root text; the first wording drew 332px and wrapped, shoving fifty-two targets 21px down mid-tap, and twenty-four capital Ws draw 434px whatever the wording, so the box is what holds. Two things the sweep found on the way. It opened the card picker by un-hiding an overlay whose grid had never been painted, so for its whole life it audited an empty sheet; it now opens it the way a dealer does, with cards out and the answer showing. And the slot buttons above the grid were named to a screen reader by the em dash they draw when empty. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
133 lines
5.0 KiB
JavaScript
133 lines
5.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 HandRank = load("handRank");
|
|
const PotCalculator = load("potCalculator");
|
|
const SplitCalculator = load("splitCalculator");
|
|
const Betting = load("betting");
|
|
const SettleUp = load("settleUp");
|
|
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;
|
|
|
|
// actionBar.js after state, since its own module-level code reaches for neither but
|
|
// its methods reach for both. Loaded last so nothing it touches is still undefined.
|
|
const ActionBar = load("actionBar");
|
|
|
|
// The picker's slot bookkeeping — which slots it is filling, what is already dealt —
|
|
// is plain state work and testable here. Only render() and open() touch the DOM.
|
|
const CardPicker = load("cardPicker");
|
|
|
|
// The wake lock's rule about when a table needs the screen is plain state work too.
|
|
// Its acquire/release bookkeeping is testable here as well, against a stand-in for
|
|
// navigator.wakeLock — see wakeLock.test.js.
|
|
const WakeLock = load("wakeLock");
|
|
|
|
// feedback.js exports two namespaces and a third; Unavailable is the one with anything
|
|
// to test, and render.js and cardPicker.js reach for it as a global the way the browser
|
|
// hands it to them. See theUnansweredPress.
|
|
const { Toast, Dialog, Unavailable } = require(path.join(SRC, "feedback.js"));
|
|
globalThis.Toast = Toast;
|
|
globalThis.Dialog = Dialog;
|
|
globalThis.Unavailable = Unavailable;
|
|
|
|
// Modal is DOM behaviour through and through, but two pieces of it answer here: the
|
|
// list of what counts as a Tab stop, which is a string, and preservingFocus, which asks
|
|
// so little of a document that a stand-in can answer it. See theKeyboardInASheet.
|
|
const Modal = load("modal");
|
|
|
|
// Quick Split draws itself entirely from the DOM, but the arithmetic that decides what
|
|
// its two steppers are allowed to reach — and how many ways a pot may be chopped — is
|
|
// plain and answers here. See theWaysControl.
|
|
const QuickSplit = load("quickSplit");
|
|
|
|
// The seat menu's list — which actions a seat is offered and which group each one
|
|
// lands in — is decided before anything is drawn, so actionsFor() answers here. Only
|
|
// render() and open() touch the DOM.
|
|
const SeatMenu = load("seatMenu");
|
|
|
|
// ---- 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,
|
|
HandRank,
|
|
Render,
|
|
HandHistoryExport,
|
|
PotCalculator,
|
|
SplitCalculator,
|
|
SettleUp,
|
|
Betting,
|
|
HoldemState,
|
|
ActionBar,
|
|
CardPicker,
|
|
WakeLock,
|
|
SeatMenu,
|
|
Modal,
|
|
QuickSplit,
|
|
Unavailable,
|
|
makeTable,
|
|
chipsOnTable,
|
|
contributions,
|
|
};
|