diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml index 8fc4291..896ee76 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml @@ -509,37 +509,53 @@ not during it.

- + @* Twelve rulings, shut. -
-

Redeal it — misdeal

+ They used to be twelve headings with all their prose under them — 2,480px of + it on a phone, of which 1,754 was below the fold — and a row of twelve chips + across the top to jump between them. The chips were an index of the twelve + headings printed directly above the same twelve headings, which is the same + list twice: 172px of navigation before the first word, and the dealer who + used it landed in the middle of a scroll with eleven rulings they didn't ask + for still wrapped around them. + + The headings are the index now. NN/g's rule for this shape — a list of + independent topics somebody opens one of — is an accordion, shut by default, + and half of these headings answer the question without being opened at all: + "A short all-in doesn't reopen the betting" is the ruling, not the topic. + + Not exclusive (no `name` attribute): closing one because another opened moves + everything under the dealer's finger, which is the disorientation the + accordion is here to avoid. *@ +
+

Redeal it — misdeal

-
+ -
-

A board card turned too early

+
+

A board card turned too early

If the fourth board card gets turned before the betting round is finished, that card is out of play for the round. Finish the betting, then burn and turn what would have been the fifth card in the fourth card's place.

-
+ -
-

Acting out of turn

+
+

Acting out of turn

A one-off is usually just backed up if no one has acted behind them. Doing it repeatedly is what earns a penalty.

-
+ -
-

Calling the clock on a player

+
+

Calling the clock on a player

Any player may ask for a clock once a decision is taking unreasonably long; it is the dealer, not the table, who grants it. The player then @@ -553,10 +569,10 @@ count once you tap Call the clock, and offers the ruling as one button when it ends.

-
+ -
-

A straddle is a blind raise

+
+

A straddle is a blind raise

The player left of the big blind may put out twice it before the cards, without looking. It is a blind bet, not a raise made in turn, so it does @@ -570,10 +586,10 @@ posts it, puts the right player on the clock and prices the raise; the seat wears an STR badge for the hand.

-
+ -
-

A short all-in doesn't reopen the betting

+
+

A short all-in doesn't reopen the betting

Somebody shoving in less than a full raise — say $3 more when the last raise was $8 — does not give the players behind them another go. Anyone @@ -585,20 +601,20 @@ the minimum raise stays measured off the last full raise, not off the short one. The action bar follows this for you.

-
+ -
-

Chips nobody called come back

+
+

Chips nobody called come back

Bet more than anyone was able to call and the excess is returned to you before the showdown — it was never contested, so it isn't part of the pot. The pot breakdown shows it as "uncalled" rather than pretending it's a side pot you could win.

-
+ -
-

One big chip, nothing said

+
+

One big chip, nothing said

The most common argument at a home table. A player facing a bet who pushes out a single oversized chip without saying anything has @@ -611,10 +627,10 @@ clear verbal declaration in turn is binding either way — if words and chips disagree, whichever came first is the action.

-
+ -
-

A raise has to be a full raise

+
+

A raise has to be a full raise

At least as much again as the last raise, or the big blind if there hasn't been one. Anything between a call and that minimum isn't a raise @@ -622,10 +638,10 @@ in. The raise pad says so if you type one, and won't stop you: your ruling beats the arithmetic.

-
+ -
-

Who turns their hand over first

+
+

Who turns their hand over first

If there was a bet on the river, the last player to bet or raise shows first. If the river was checked all the way round, it's the first live @@ -638,20 +654,20 @@ was. The action bar names the seat and the reason at every showdown, so the call is already made before anyone asks.

-
+ -
-

The odd chip in a split pot

+
+

The odd chip in a split pot

When a chopped pot won't divide evenly, the odd chip goes to the first player left of the button, not to whoever the dealer happened to tick first. Pot payouts here order the winners that way, so the leftover lands on the right seat every time.

-
+ -
-

Betting in one motion — the string bet

+
+

Betting in one motion — the string bet

A bet is whatever goes in on the first trip to the stack, unless the amount was said out loud first. Reaching back for more chips without @@ -664,7 +680,7 @@ fine — the point is that a player can't watch the table react and then decide how much to bet. Say it, or push it all at once.

-
+

Follows the Tournament Directors Association rules, which most rooms and diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 6d42aa1..f323aa9 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -2131,63 +2131,63 @@ button, white-space: nowrap; } -/* One chip per ruling, wrapped across the top of the sheet. Names rather than icons: - a dealer scanning this is looking for the words somebody at the table just said. */ -.rulingsIndex { - display: flex; - flex-wrap: wrap; - gap: var(--sp-1); - margin: 0 0 var(--sp-4); -} - -.rulingsIndexBtn { - box-sizing: border-box; - min-height: 32px; - padding: var(--sp-1) var(--sp-2); - border-radius: var(--r-pill); - border: 1px solid var(--line); - background: var(--surface-2); - color: var(--gold); - font-family: inherit; - font-size: var(--fs-sm); - line-height: 1.25; - text-align: left; - cursor: pointer; -} - -@media (hover: hover) { - .rulingsIndexBtn:hover { - background: var(--surface-3); - border-color: var(--line-strong); - } -} - -/* A thumb needs the same 44px here as everywhere else. */ -@media (pointer: coarse) { - .rulingsIndexBtn { - min-height: 44px; - font-size: var(--fs-sm); - } -} - -/* The heading the index jumps to takes focus, and a focus ring on a heading that the - dealer just asked for reads as noise rather than as help. */ -.ruling h3:focus { - outline: none; -} - +/* A shut ruling is a row you can read; an open one is a passage you can read. The gold + left bar used to run down all twelve at once, which on a list of headings is twelve + vertical rules and no hierarchy. It belongs to the one that is open. */ .ruling { - margin-bottom: var(--sp-4); + border-bottom: 1px solid var(--line); +} + +.ruling[open] { + margin-bottom: var(--sp-2); padding-left: var(--sp-3); border-left: 2px solid rgba(216, 180, 106, 0.4); + border-bottom-color: transparent; } -.ruling h3 { - margin: 0 0 var(--sp-1); - color: var(--gold); +/* Padding rather than a flex box with min-height, because a

only draws its + disclosure triangle while it is `display: list-item` — and content-box means a + min-height would be *added* to the padding. Both traps are the sidebar's, one + stylesheet section over, and they are the same traps here. */ +.ruling > summary { + box-sizing: border-box; + min-height: 44px; + padding: var(--sp-2) 0; + color: var(--text); font-size: var(--fs-base); font-weight: 600; letter-spacing: 0.03em; + line-height: 1.35; + cursor: pointer; +} + +/* The gold arrives when a ruling is opened, and not before. These were all gold while + they were headings with prose between them; twelve of them in a column is the accent + doing the job of body text, which leaves it nothing to say when one of them is the + one the dealer is reading. The sidebar's eight panel names are `--text` for the same + reason — this is the same component doing the same job. */ +.ruling[open] > summary { + margin-bottom: var(--sp-1); + color: var(--gold); +} + +/* A is announced as a button, not as a heading, so the twelve names — which + are now the whole of this sheet's navigation — would be invisible to anybody moving + through it by heading. Same fix as the sidebar's panel names: keep the heading and + make it look like nothing happened. Inline, because a block child of a summary + pushes the disclosure triangle onto its own line. */ +.ruling > summary > h3 { + display: inline; + margin: 0; + font: inherit; + color: inherit; + letter-spacing: inherit; +} + +@media (hover: hover) { + .ruling > summary:hover { + color: var(--gold-bright); + } } .ruling p, @@ -2201,6 +2201,14 @@ button, margin: 0; } +/* Five of the twelve are two paragraphs, and they ran into each other: the sections + were spaced apart from one another but nothing separated the halves of one ruling, + so "the most that chip allows." and "Facing no bet," read as one paragraph. It did + not show while there was a gold heading every few lines to break the column up. */ +.ruling p + p { + margin-top: var(--sp-2); +} + .ruling ul { margin: 0; padding-left: var(--sp-4); diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index 582cee4..9ffa602 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -879,50 +879,19 @@ document.addEventListener("DOMContentLoaded", () => { }); } - // An index across the top of the rulings sheet, one chip per ruling. + // The sheet opens on twelve shut rulings, and it opens that way every time. // - // This is opened mid-argument with players waiting, and it was nine rulings of - // prose with no way in: reaching the last one meant scrolling 1.6 screens on a - // desktop, 2.1 on a phone and 3.5 on a phone held sideways. A dealer looking for - // the odd-chip rule had to read past seven others to find it. - // - // Built from the sections rather than written out beside them, so a ruling added - // later turns up in the index without anyone having to remember to add it twice. - const buildRulingsIndex = () => { - const index = document.getElementById("rulingsIndex"); - if (!index || index.childElementCount > 0) return; - [...rulingsOverlay.querySelectorAll(".ruling")].forEach((section, i) => { - const heading = section.querySelector("h3"); - if (!heading) return; - if (!section.id) section.id = `ruling${i + 1}`; - - const jump = document.createElement("button"); - jump.type = "button"; - jump.className = "rulingsIndexBtn"; - // A short label where the ruling gives one. The headings are sentences — - // "A short all-in doesn't reopen the betting" — and nine of those stacked - // one per row made the index 342px tall on a phone, which is most of the - // sheet before the first ruling. "Short all-in" says the same thing to - // somebody who is looking for it, and three fit on a line. - jump.textContent = section.dataset.short || heading.textContent; - // The full heading is still what it announces, so the short label never - // costs a screen reader the sentence. - if (section.dataset.short) jump.setAttribute("aria-label", heading.textContent); - jump.addEventListener("click", () => { - Render.bringIntoView(section, { block: "start" }); - // The heading is what the dealer is now reading, so that is where the - // keyboard and a screen reader should be too. - heading.setAttribute("tabindex", "-1"); - heading.focus({ preventScroll: true }); - }); - index.appendChild(jump); - }); + // There was an index of chips here that this built from the headings; the headings + // are the index now, so there is nothing to build. What is left is the other half + // of the job: a dealer who read the odd-chip rule an hour ago should not find it + // still hanging open under the one they came back for.
keeps its state, + // so closing the sheet is what forgets it. + const openRulings = () => { + rulingsOverlay.querySelectorAll(".ruling[open]").forEach(r => { r.open = false; }); + rulingsOverlay.hidden = false; }; - document.getElementById("openRulingsBtn").addEventListener("click", () => { - buildRulingsIndex(); - rulingsOverlay.hidden = false; - }); + document.getElementById("openRulingsBtn").addEventListener("click", openRulings); document.getElementById("closeRulingsBtn").addEventListener("click", closeRulings); rulingsOverlay.addEventListener("click", e => { if (e.target === rulingsOverlay) closeRulings(); diff --git a/tests/holdem/theRulings.test.js b/tests/holdem/theRulings.test.js new file mode 100644 index 0000000..6d61211 --- /dev/null +++ b/tests/holdem/theRulings.test.js @@ -0,0 +1,142 @@ +// The sheet a dealer opens mid-argument. +// +// It held twelve rulings with all their prose showing — 2,480px of it on a 390px +// phone, of which 1,754 was below the fold — and a row of twelve chips across the top +// to jump between them. The chips were an index of the twelve headings printed directly +// above the same twelve headings: the same list twice, 172px of navigation before the +// first word, and the dealer who used one landed in the middle of a scroll with eleven +// rulings they hadn't asked for still wrapped around them. +// +// The headings are the index now. NN/g's rule for this shape — a longer list of +// independent topics somebody opens one of — is an accordion, shut by default, and +// half of these headings answer the question without being opened at all: "A short +// all-in doesn't reopen the betting" is the ruling, not the topic. Measured after: +// 29px below the fold on an iPhone with all twelve rows on screen at once, 51px on a +// desktop. Opening the longest of them leaves 244px. +// +// These are layout and markup decisions, so they are checked as text — except the one +// about what the sheet does when it is opened again, which is behaviour and lives in +// main.js. +const test = require("node:test"); +const { describe } = require("node:test"); +const assert = require("node:assert"); +const fs = require("fs"); +const path = require("path"); + +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"); + +const sheet = () => { + const at = PAGE.indexOf('
= 0, "there is no rulings overlay"); + return PAGE.slice(at, PAGE.indexOf('
{ + const at = CSS.indexOf(`\n${selector} {`); + assert.ok(at >= 0, `there is no \`${selector}\` rule`); + return CSS.slice(at, CSS.indexOf("}", at)); +}; + +// Each ruling as its own slice of markup. +const rulings = () => { + const parts = sheet().split('
p.slice(0, p.indexOf("
"))); +}; + +// --------------------------------------------------------------------------- + +describe("twelve rulings, shut", () => { + test("every one of them is a disclosure, and there are still twelve", () => { + assert.equal(rulings().length, 12); + }); + + // NN/g is explicit that opening the first one by default costs the reader the + // glance at the whole list, which on this sheet is the entire point. + test("none of them is open in the markup", () => { + assert.ok(!/
]*\bopen\b/.test(sheet()), + "a ruling starts open, so the sheet no longer opens on its own contents"); + }); + + // Native exclusive accordions close the open one when another is opened, which + // shifts everything below it under the finger that is reaching. That is the + // disorientation the accordion is here to remove, not add. + test("opening one does not close another", () => { + assert.ok(!/
]*\bname=/.test(sheet()), + "the rulings are an exclusive accordion, so content moves when one opens"); + }); + + test("the name of a ruling is the control that opens it", () => { + rulings().forEach((r, i) => { + assert.match(r, /

.+<\/h3><\/summary>/, + `ruling ${i + 1} does not carry its heading in its summary`); + }); + }); +}); + +describe("the index that was above them is gone, not merely hidden", () => { + test("nothing builds a chip row any more", () => { + assert.ok(!/rulingsIndex/.test(PAGE), "the index nav is still in the markup"); + assert.ok(!/buildRulingsIndex|rulingsIndexBtn/.test(MAIN), "main.js still builds the index"); + assert.ok(!/rulingsIndexBtn/.test(CSS), "the index chips still have styles"); + }); + + // The short labels existed only to fit three chips on a line. With the full + // headings doing the work there is nothing left to read them. + test("and the short labels it read are gone with it", () => { + assert.ok(!/data-short=/.test(PAGE)); + }); +}); + +describe("what a summary has to be to work", () => { + // A is announced as a button and not as a heading, so without this the + // twelve names — the whole of this sheet's navigation — are invisible to anybody + // moving through it by heading. Same fix as the sidebar's panel names. + test("the heading survives being a control", () => { + assert.match(ruleFor(".ruling > summary > h3"), /display:\s*inline/); + assert.match(ruleFor(".ruling > summary > h3"), /font:\s*inherit/); + }); + + // A is content-box, so a min-height is *added* to its padding — the trap + // the sidebar's headers were in, one stylesheet section over. + test("a thumb gets its 44px, counted the way the box actually counts", () => { + const rule = ruleFor(".ruling > summary"); + assert.match(rule, /box-sizing:\s*border-box/); + assert.match(rule, /min-height:\s*44px/); + }); + + test("the accent belongs to the ruling being read, not to all twelve", () => { + assert.match(ruleFor(".ruling > summary"), /color:\s*var\(--text\)/); + assert.match(ruleFor(".ruling[open] > summary"), /color:\s*var\(--gold\)/); + }); + + test("and so does the gold bar down the side", () => { + assert.match(ruleFor(".ruling[open]"), /border-left:\s*2px solid/); + assert.ok(!/border-left/.test(ruleFor(".ruling")), + "every ruling wears the open one's marker"); + }); +}); + +describe("reading one of them", () => { + // Five of the twelve are two paragraphs and they ran into each other: the sections + // were spaced from one another, but nothing separated the halves of one ruling. + test("two paragraphs of a ruling are two paragraphs", () => { + assert.match(ruleFor(".ruling p + p"), /margin-top:\s*var\(--sp-\d\)/); + }); + + //
keeps its state, so without this a dealer who read the odd-chip rule an + // hour ago finds it still hanging open under the one they came back for. + test("the sheet opens shut, however it was left", () => { + const at = MAIN.indexOf("const openRulings"); + assert.ok(at >= 0, "nothing owns what the sheet looks like when it opens"); + const body = MAIN.slice(at, MAIN.indexOf("};", at)); + assert.match(body, /\.ruling\[open\]/); + assert.match(body, /open = false/); + }); + + test("and that is what the button calls", () => { + assert.match(MAIN, /getElementById\("openRulingsBtn"\)\.addEventListener\("click", openRulings\)/); + }); +});