From 3458a6d14469dc582b903958c2f5958d09cf6d5d Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Mon, 14 Sep 2026 01:31:38 -0600 Subject: [PATCH] Put the two numbers the panel glances at above the prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Table Setup is the one panel the tool opens by itself, on a table nobody has set up yet, so it is most of what a dealer sees the first time they arrive. Measured on a fresh table: the panel stood 886px on a 667px phone, and the whole page 2076. The order was the problem more than the length. The two figures the panel's own summary glances at — "10 seats · $1/$2" — were the seat count at the top and the blinds 218px further down, under three switches and the three paragraphs explaining them: a dedicated dealer, a card sheet at every street, and whether the phone is allowed to sleep. A dealer setting up a $1/$2 game read all of that on the way to the two boxes they came for. The three have gone into "While you deal", with the paragraphs. That is not a new idea in this panel — House take and Without live betting were already nested disclosures — it is the same idea applied to the things that were not about money. Afterwards the panel is 529px, the page 1719 on the same phone and 1275 on a 1280x900 desktop, and the first two rows are the seats and the blinds. A switch nobody can see is a switch nobody remembers turning on, so the disclosure carries a glance of its own: "Asking for cards", "House dealer", or "Screen may sleep" when one of the three is away from where it ships, and silence when none of them is. One of them, not a list: joined they run to 49 characters into a row 44px tall with a label already on it, and "House dealer · Asking for c…" has told nobody anything. 1291 tests pass; the sweep is clean at 428 audits across 13 viewports. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml | 75 ++++--- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 15 ++ .../wwwroot/js/casino/holdem/main.js | 5 + .../wwwroot/js/casino/holdem/render.js | 20 +- tests/holdem/panelGlance.test.js | 4 +- tests/holdem/theFirstScreen.test.js | 210 ++++++++++++++++++ 6 files changed, 297 insertions(+), 32 deletions(-) create mode 100644 tests/holdem/theFirstScreen.test.js diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml index ba75b70..24fac11 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml @@ -89,39 +89,20 @@ half-way through a night. *@

Table Setup

+ @* The numbers first, and only the numbers. + + This panel is the one thing that is open when the tool is first + opened, and it was 886px of a 667px phone — the two figures its own + summary glances at, the blinds, sat below 218px of prose explaining + three switches a dealer may never touch. The three have gone into + "While you deal" below, with the paragraphs that explain them, which + is where House take and Without live betting already were: the panel + had the vocabulary for this and was only using it for money. *@
- - -

Turn this on if a person outside the game deals — no seat shows the dealer button, but blinds still rotate automatically each hand.

- - @* The label used to lead with "Track the board", which is the feature's name - rather than what pressing it does to your evening. What it does is put a - card sheet in front of you at four moments: each street, and the showdown. *@ -

Every time you deal a street, a sheet asks which cards came off the deck, and puts them on the felt. At the showdown it asks for each hand that turns over — then Resolve Pot reads the winner and the amount instead of asking you. Close the sheet at any point to go back to picking the winner yourself.

- - @* On by default, and the only setting here that is. A dealer has to - opt in to the tool doing more than they asked; they should not have - to opt in to it staying readable. *@ -

Stops the phone dimming and locking between the flop and the turn. It only holds while betting is open, so the screen sleeps as usual during breaks — and the tool says so the first time it takes hold.

Blinds post themselves each hand. Seats without their own stack start here.

+ + + @* All three change what the tool does rather than what the table is, + and all three are set once and then left alone. The glance says so + when one of them is not on its default, because a switch you cannot + see is a switch you cannot remember turning on. *@ +
+ While you deal +
+ +

Turn this on if a person outside the game deals — no seat shows the dealer button, but blinds still rotate automatically each hand.

+ + @* The label used to lead with "Track the board", which is the feature's name + rather than what pressing it does to your evening. What it does is put a + card sheet in front of you at four moments: each street, and the showdown. *@ +

Every time you deal a street, a sheet asks which cards came off the deck, and puts them on the felt. At the showdown it asks for each hand that turns over — then Resolve Pot reads the winner and the amount instead of asking you. Close the sheet at any point to go back to picking the winner yourself.

+ + @* On by default, and the only setting here that is. A dealer has to + opt in to the tool doing more than they asked; they should not have + to opt in to it staying readable. *@ +

Stops the phone dimming and locking between the flop and the turn. It only holds while betting is open, so the screen sleeps as usual during breaks — and the tool says so the first time it takes hold.

+
+
House take diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 7e7fdf7..7930334 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -3083,6 +3083,21 @@ input[type="checkbox"] { font-size: var(--fs-sm); } +/* So a glance can hang off the right of it, the way the panel headings above already + do. Without this the span anchors to whichever ancestor is positioned, which is the + sidebar. */ +.subPanel > summary { + position: relative; +} + +/* A step quieter than the panel headings' glance. This one is a footnote on a line that + is itself a footnote — if it read at the same weight as "Off by $40" the eye would + stop at a row saying nothing has gone wrong. */ +.subPanelGlance { + font-size: var(--fs-xs); + color: var(--text-dim); +} + @media (hover: hover) { .subPanel summary:hover { color: #c5c5c5; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index c650632..f600b52 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -349,6 +349,11 @@ document.addEventListener("DOMContentLoaded", () => { // the right panel to have open on a table nobody has touched, and the wrong one to // have open on the fourth hand of a night. // + // That was half the answer. The other half was what the panel held while it was + // open — 886px of it, on a 667px phone — and it has since given three switches and + // the paragraphs explaining them to "While you deal". The panel this opens is 529px + // now, and the two figures its own summary glances at are the first two rows of it. + // // Decided once, here, rather than on every render — a panel that reopened itself // whenever the table emptied would fight the dealer who had just shut it. (() => { diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js index 0a6cbd7..dacc119 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js @@ -161,8 +161,8 @@ const Render = { // on and stays away otherwise — nobody waiting, no notes, no hands yet — because a // row reading "Nobody waiting" all night is the clutter this is here to remove, and // eight rows that always say something are eight rows that say nothing again. - PANEL_GLANCES: ["glanceTableSetup", "glanceStandings", "glanceWaiting", - "glanceHands", "glanceChips", "glanceNotes"], + PANEL_GLANCES: ["glanceTableSetup", "glanceWhileYouDeal", "glanceStandings", + "glanceWaiting", "glanceHands", "glanceChips", "glanceNotes"], // Pure, so the wording can be tested without a page to hang it on. Every value is // read off the state the panel below it renders from, never off the panel's own @@ -204,8 +204,24 @@ const Render = { // over an empty table would be answering for a count that was never taken. const nothingBanked = bank.cashIn === 0 && bank.chipsInPlay === 0; + // The three switches behind "While you deal", which is shut unless somebody + // opens it: a switch nobody can see is a switch nobody remembers turning on, and + // a dealer wondering why a card sheet keeps appearing should be told where it + // lives without having to go looking. + // + // One of them, not all three. This row is 44px of a 320px column with a label + // already on it, and the three joined run to 49 characters — which ellipsizes + // into "House dealer · Asking for c…", a row that has told nobody anything. So + // it names the one most likely to be the surprise, and opening the panel shows + // the rest. Keeping the screen on is the only one that ships on, so it is its + // absence that gets said. + const dealing = state.trackBoard ? "Asking for cards" + : state.dedicatedDealer ? "House dealer" + : state.keepScreenAwake === false ? "Screen may sleep" : ""; + return { glanceTableSetup: glance(`${state.seatCount} seats${blinds}`), + glanceWhileYouDeal: glance(dealing), glanceStandings: nothingBanked ? glance("") : bank.balanced ? glance("Balanced", "ok") : glance(`Off by ${Money.format(Math.abs(bank.difference))}`, "warn"), diff --git a/tests/holdem/panelGlance.test.js b/tests/holdem/panelGlance.test.js index 1f0f1e1..939f297 100644 --- a/tests/holdem/panelGlance.test.js +++ b/tests/holdem/panelGlance.test.js @@ -175,8 +175,8 @@ describe("the rows that stay quiet until they have news", () => { }); describe("what the row is allowed to be", () => { - const ROWS = ["glanceTableSetup", "glanceStandings", "glanceWaiting", - "glanceHands", "glanceChips", "glanceNotes"]; + const ROWS = ["glanceTableSetup", "glanceWhileYouDeal", "glanceStandings", + "glanceWaiting", "glanceHands", "glanceChips", "glanceNotes"]; test("every glance the sidebar renders is one this knows about", () => { // The list drives the DOM writes, so a glance computed and never listed is a diff --git a/tests/holdem/theFirstScreen.test.js b/tests/holdem/theFirstScreen.test.js new file mode 100644 index 0000000..726baae --- /dev/null +++ b/tests/holdem/theFirstScreen.test.js @@ -0,0 +1,210 @@ +// What Table Setup holds while it is open. +// +// It is the one panel the tool opens by itself, on a table nobody has set up yet — so +// it is most of what a dealer sees the first time they arrive. Measured on a fresh +// table, before this: the panel stood 886px on a 667px phone, and the whole page 2076. +// +// The order was the problem more than the length. The two figures the panel's own +// summary glances at — "10 seats · $1/$2" — were the seat count at the top and the +// blinds 218px further down, under three switches and three paragraphs explaining +// them: a dedicated dealer, a card sheet at every street, and whether the phone is +// allowed to sleep. A dealer setting up a $1/$2 game read all of that on the way to +// the two boxes they came for. +// +// The three have gone into "While you deal", with the paragraphs that explain them. +// That is not a new idea in this panel — House take and Without live betting were +// already nested disclosures — it is the same idea applied to the things that were not +// about money. Afterwards: the panel is 529px, the page 1719 on the same phone and +// 1275 on a 1280x900 desktop, and the first two rows are the seats and the blinds. +// +// A switch nobody can see is a switch nobody remembers turning on, so the disclosure +// carries a glance of its own. +const test = require("node:test"); +const { describe } = require("node:test"); +const assert = require("node:assert"); +const fs = require("fs"); +const path = require("path"); +const { Render, makeTable } = require("./load"); + +const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net"); +const PAGE = fs.readFileSync(path.join(ROOT, "Pages", "Casino", "TexasHoldem.cshtml"), "utf8"); +const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8"); +const MAIN = fs.readFileSync(path.join(ROOT, "wwwroot", "js", "casino", "holdem", "main.js"), "utf8"); + +// The Table Setup panel, from its opening tag to the panel after it. +const setupPanel = () => { + const at = PAGE.indexOf('
'); + assert.ok(at >= 0, "there is no Table Setup panel"); + const end = PAGE.indexOf('
at, "the panel after Table Setup has gone"); + return PAGE.slice(at, end); +}; + +// What the panel shows without anything being opened: everything before the first +// nested disclosure. +const alwaysShowing = () => { + const panel = setupPanel(); + const firstSub = panel.indexOf('
'); + assert.ok(firstSub >= 0, "the panel has no nested disclosures at all"); + return panel.slice(0, firstSub); +}; + +const ruleFor = selector => { + const at = CSS.indexOf(`\n${selector} {`); + assert.ok(at >= 0, `there is no \`${selector}\` rule`); + return CSS.slice(at, CSS.indexOf("}", at)); +}; + +const glance = state => (Render.panelGlances(state).glanceWhileYouDeal || {}).text; + +const table = () => makeTable({ seatCount: 9, seated: [], stack: 0 }); + +// --------------------------------------------------------------------------- + +describe("what the open panel leads with", () => { + // The two the summary already glances at. They were first and seventh. + test("the seats and the blinds, in that order, above everything else", () => { + const body = alwaysShowing(); + const seats = body.indexOf('id="seatCountInput"'); + const small = body.indexOf('id="smallBlindInput"'); + const big = body.indexOf('id="bigBlindInput"'); + const stack = body.indexOf('id="defaultStackInput"'); + assert.ok(seats >= 0 && small >= 0 && big >= 0 && stack >= 0, + "one of the four numbers is no longer in the part of the panel that shows"); + assert.ok(seats < small, "the seat count is below the blinds"); + assert.ok(small < big && big < stack, "the blinds and the stack have swapped"); + }); + + test("and nothing in it is a switch about how the tool behaves", () => { + const body = alwaysShowing(); + ["dedicatedDealerToggle", "trackBoardToggle", "keepScreenAwakeToggle"].forEach(id => { + assert.ok(!body.includes(id), `${id} is still on the panel's first screen`); + }); + }); + + // One line, and it is about the boxes directly above it. + test("one paragraph survives, the short one", () => { + const hints = alwaysShowing().match(/

/g) || []; + assert.equal(hints.length, 1, "the panel's first screen is back to being prose"); + assert.match(alwaysShowing(), /Blinds post themselves each hand/); + }); +}); + +describe("where the three switches went", () => { + const dealing = () => { + const panel = setupPanel(); + const at = panel.indexOf("

While you deal"); + assert.ok(at >= 0, 'there is no "While you deal" disclosure'); + return panel.slice(panel.lastIndexOf("", at)); + }; + + test("all three of them, with the paragraphs that explain them", () => { + const body = dealing(); + ["dedicatedDealerToggle", "trackBoardToggle", "keepScreenAwakeToggle"].forEach(id => { + assert.ok(body.includes(id), `${id} did not come with the rest`); + }); + assert.match(body, /no seat shows the dealer button/); + assert.match(body, /a sheet asks which cards came off the deck/); + assert.match(body, /Stops the phone dimming and locking/); + }); + + // The same shape House take and Without live betting already had. A disclosure that + // ships open is a disclosure that has saved nobody anything. + test("it is shut, like the two money ones beside it", () => { + const panel = setupPanel(); + assert.ok(!/
]*\bopen\b/.test(panel), + "a nested disclosure in this panel ships open"); + }); + + test("and it sits above them, because it is not about money", () => { + const panel = setupPanel(); + assert.ok(panel.indexOf("While you deal") < panel.indexOf("House take")); + assert.ok(panel.indexOf("House take") < panel.indexOf("Without live betting")); + }); +}); + +describe("the glance on a shut disclosure", () => { + test("silence when all three are where they ship", () => { + const state = table(); + assert.equal(glance(state), ""); + }); + + test("and a name when one of them is not", () => { + const state = table(); + state.trackBoard = true; + assert.equal(glance(state), "Asking for cards"); + + const other = table(); + other.dedicatedDealer = true; + assert.equal(glance(other), "House dealer"); + }); + + // The only one that ships on, so it is its absence that is worth saying. + test("the screen setting speaks by going quiet, not by being on", () => { + const on = table(); + on.keepScreenAwake = true; + assert.equal(glance(on), ""); + + const off = table(); + off.keepScreenAwake = false; + assert.equal(glance(off), "Screen may sleep"); + }); + + // Joined, the three run to 49 characters, and this row is 44px of a 320px column + // with a label already on it. "House dealer · Asking for c…" has told nobody + // anything; the one most likely to be the surprise has. + test("one of them, never a list of three", () => { + const state = table(); + state.trackBoard = true; + state.dedicatedDealer = true; + state.keepScreenAwake = false; + assert.equal(glance(state), "Asking for cards"); + assert.ok(glance(state).length <= 24); + }); + + test("it is a row this sidebar knows how to write", () => { + assert.ok(Render.PANEL_GLANCES.includes("glanceWhileYouDeal"), + "the glance is computed and never written, so the row keeps its old text all night"); + const panel = setupPanel(); + const at = panel.indexOf('id="glanceWhileYouDeal"'); + assert.ok(at >= 0, "there is nowhere to write it"); + assert.match(panel.slice(panel.lastIndexOf("", at)), /hidden/); + }); +}); + +describe("what had to be true for it to hang there", () => { + // Without this the span anchors to whichever ancestor is positioned, which is the + // sidebar — the glance would appear at the top right of the whole column. + test("the summary it hangs off is a positioned box", () => { + assert.match(ruleFor(".subPanel > summary"), /position:\s*relative/); + }); + + // A footnote on a line that is itself a footnote. At the weight of "Off by $40" the + // eye stops on a row saying nothing has gone wrong. + test("and it is drawn a step quieter than the panel headings' own", () => { + const sub = ruleFor(".subPanelGlance"); + assert.match(sub, /font-size:\s*var\(--fs-xs\)/); + assert.match(sub, /color:\s*var\(--text-dim\)/); + assert.match(ruleFor(".panelGlance"), /font-size:\s*var\(--fs-sm\)/); + }); +}); + +describe("the panel still opens itself only when it should", () => { + test("on a table nobody has touched, and not on the fourth hand", () => { + const at = MAIN.indexOf('document.getElementById("tableSetupPanel")'); + assert.ok(at >= 0, "nothing decides whether the panel starts open"); + const block = MAIN.slice(at, at + 400); + assert.match(block, /Number\(state\.handNumber\) \|\| 1\) === 1/); + assert.match(block, /!state\.seats\.some\(seat => HoldemState\.seatInPlay\(seat\)\)/); + assert.match(block, /panel\.open = fresh/); + }); + + test("what was measured is written beside the decision", () => { + const at = MAIN.indexOf("Table Setup used to ship open"); + assert.ok(at >= 0); + const note = MAIN.slice(at, at + 1200); + assert.match(note, /886px/); + assert.match(note, /529px/); + }); +});