diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 6cbf513..d12078a 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -1086,6 +1086,30 @@ button, color: var(--text-muted); } +/* And on a chair with somebody in it, it isn't drawn at all. + + Not a taste call — it had nowhere left to stand. The corner it was using is also where + the blind badge drops off the rim and where a long name reaches, and measured across + 348 seats on 36 tables, 117 of them had this digit printed under something else: all + 66 seats posting a blind had it fully covered, and 51 more had it sitting on the first + letter of the player's own name — worst case the whole glyph, on "Jonah" at 54px. + + Every other corner is spoken for too, and each by something that has to be there. The + button's chip has the right-hand end of the top rim. Bets land on the rim between a + seat and the pot, which is the bottom edge for the seats above centre and the top edge + for the ones below. The middle of the puck is three rows of name, bet and stack, all + of them about this hand. The chair number is the only mark on a puck that isn't, and + it is the one that gave way. + + Nothing is lost that a tap doesn't return: the seat editor's heading and the seat + menu's first line both name the chair now — see HoldemState.seatHeading — a seat with + no name typed in still says "Seat 5" as its name, and an empty chair still wears its + number large and centred, which is the one time the number is the thing being looked + for. */ +.seat:not(.seatEmpty) .seatChairNumber { + display: none; +} + /* ---- The puck is a stack of rows, and every row is the height of a row ---- Nine chairs on a ring, and until this block no two of them were the same size. A puck @@ -1644,7 +1668,7 @@ button, phone, on a table where the seats had dropped to 8% of the felt. Two steps up the scale for the two numbers a dealer reads across the table, one for - the chair number in the corner, which is a reference rather than a reading. Not + the number on an open chair, which is a reference rather than a reading. Not proportional to the width — 96 to 144 is 1.5x and this is 1.25x — because a name is a label on a puck, not a headline, and the puck also has to hold it: 15px is the largest size at which "Mike from work", the longest name the sweep uses, still comes diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js index 2b4e105..08caf8e 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js @@ -2302,7 +2302,9 @@ const Render = { const index = this.selectedSeatIndex; const player = this.state.seats[index]; - title.textContent = this.puckLabel(player, index); + // Named and numbered. The chair number came off the puck's face this round — + // see HoldemState.seatHeading — and this is one of the two places it went. + title.textContent = HoldemState.seatHeading(player, index); body.innerHTML = ""; const template = document.getElementById("seatEditorTemplate"); diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js index d08152d..f8aa2de 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js @@ -243,7 +243,12 @@ const SeatMenu = { const stack = Betting.stackOf(this.state, player); const owed = Betting.toCall(this.state, player); - const bits = []; + // Which chair this is leads the line. It came off the puck's face this round — + // see HoldemState.seatHeading — and this is the other place it went. It goes in + // the subtitle rather than the heading because the heading is a name and names + // are long: this header is stuck to the top of the sheet that scrolls, and a + // second line of h2 is height every action below it pays for. + const bits = [`Seat ${this.seatIndex + 1}`]; if (stack > 0) bits.push(`${Money.format(stack)} behind`); if (owed > 0) bits.push(`${Money.format(owed)} to call`); subtitle.textContent = bits.join(" · "); diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js index 3f702b0..57a324e 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js @@ -476,6 +476,23 @@ const HoldemState = { return player.name && player.name.trim() ? player.name.trim() : `Seat ${seatIndex + 1}`; }, + // The same seat, named and numbered, for the two sheets that are about one seat. + // + // The chair number used to be a grey digit in the corner of the puck, and the corner + // of the puck is where the blind badge lands and where a long name reaches: measured + // across 348 seats on 36 tables, 117 of them had it printed under something else — + // every seat posting a blind, and one in seven with it sitting on the first letter of + // the player's own name. The felt has no free corner to move it to, so it stopped + // being on the felt. These two headings are where a dealer goes when the question is + // about one seat, and they answer it now. + // + // An unnamed seat is already called "Seat 5" by displayName, so it doesn't say it + // twice. + seatHeading(player, seatIndex) { + const name = player && player.name ? player.name.trim() : ""; + return name ? `${name} · Seat ${seatIndex + 1}` : `Seat ${seatIndex + 1}`; + }, + // Contribution the table calculator will treat this player as having put in. // // Chips already pushed into the middle count whether or not the player is still diff --git a/tests/holdem/theChairNumber.test.js b/tests/holdem/theChairNumber.test.js new file mode 100644 index 0000000..be072dd --- /dev/null +++ b/tests/holdem/theChairNumber.test.js @@ -0,0 +1,129 @@ +// The one mark on a puck that isn't about this hand. +// +// A seat's chair number was a grey digit in the top-left corner, and the top-left corner +// is where the blind badge drops off the rim and where a long centred name reaches. +// Measured across 348 seats on 36 tables: 117 of them had the number printed under +// something else. All 66 seats posting a blind had it covered outright — the badge is +// 22px wide over a 6px digit, at z-index 4 — and 51 more had it sitting on the first +// letter of the player's own name, the whole glyph in the worst case, on a 54px puck. +// +// There is nowhere on the felt to move it to. The button's chip owns the right-hand end +// of the top rim. Bets land on the rim between a seat and the pot, which is the bottom +// edge for the seats above centre and the top edge for the ones below. The middle of the +// puck is three rows — name, bet, stack — and all three are about this hand. So the mark +// that isn't gave way, and the two sheets that are about one seat say which seat. +// +// The sweep had never mentioned any of it, because the check that asks whether a puck +// covers itself up only knew about two of the three things a puck wears, and only ever +// tested them against writing. +const test = require("node:test"); +const { describe } = require("node:test"); +const assert = require("node:assert"); +const fs = require("fs"); +const path = require("path"); +const { HoldemState, makeTable } = require("./load"); + +const ROOT = path.join(__dirname, "..", "..", "JoshHeaps.Net"); +const CSS = fs.readFileSync(path.join(ROOT, "wwwroot", "css", "casino", "holdem.css"), "utf8"); +const AUDIT = fs.readFileSync(path.join(__dirname, "uiAudit.js"), "utf8"); +const src = name => fs.readFileSync( + path.join(ROOT, "wwwroot", "js", "casino", "holdem", `${name}.js`), "utf8"); + +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)); +}; + +// --------------------------------------------------------------------------- + +describe("a chair with somebody in it", () => { + test("does not draw its number on the felt at all", () => { + assert.match(ruleFor(".seat:not(.seatEmpty) .seatChairNumber"), /display:\s*none/); + }); + + // Where the number is the thing being looked for rather than a footnote on somebody + // else's name — which chair is open — it is still large and centred and washed. + test("and an open one still wears its number, large and in the middle", () => { + const rule = ruleFor(".seatEmpty .seatChairNumber"); + assert.match(rule, /top:\s*50%/); + assert.match(rule, /left:\s*50%/); + assert.match(rule, /background:/); + }); + + // Removing it from the felt is only defensible because the felt is not the only + // place it appears. A seat nobody has named says "Seat 5" as its name already. + test("a seat with no name typed in still says which seat it is", () => { + const state = makeTable({ seatCount: 6 }); + state.seats[4].name = ""; + assert.equal(HoldemState.displayName(state.seats[4], 4), "Seat 5"); + }); +}); + +describe("the two sheets that are about one seat", () => { + test("name the chair as well as the player", () => { + assert.equal(HoldemState.seatHeading({ name: "Alexandra" }, 2), "Alexandra · Seat 3"); + }); + + test("and don't say it twice when the name is already the number", () => { + assert.equal(HoldemState.seatHeading({ name: "" }, 4), "Seat 5"); + assert.equal(HoldemState.seatHeading({ name: " " }, 4), "Seat 5"); + assert.equal(HoldemState.seatHeading({}, 0), "Seat 1"); + assert.equal(HoldemState.seatHeading(null, 0), "Seat 1"); + }); + + test("a typed name keeps its own spacing off the ends", () => { + assert.equal(HoldemState.seatHeading({ name: " Mike from work " }, 1), "Mike from work · Seat 2"); + }); + + test("the seat editor's heading is the one that says it", () => { + const render = src("render"); + const at = render.indexOf("renderSeatEditor() {"); + assert.ok(at >= 0); + const body = render.slice(at, render.indexOf("\n },", at)); + assert.match(body, /title\.textContent = HoldemState\.seatHeading\(player, index\)/); + }); + + // Not the heading here: this header is stuck to the top of the sheet that scrolls, + // and a name long enough to wrap would cost every action below it a line. + test("the seat menu says it on its first line instead, and says it first", () => { + const menu = src("seatMenu"); + const at = menu.indexOf("const stack = Betting.stackOf(this.state, player);"); + assert.ok(at >= 0); + const body = menu.slice(at, menu.indexOf("subtitle.textContent", at)); + assert.match(body, /const bits = \[`Seat \$\{this\.seatIndex \+ 1\}`\]/); + }); +}); + +describe("the sweep can see it now", () => { + const check = () => { + const at = AUDIT.indexOf(" seatLabelsClear() {"); + assert.ok(at >= 0, "the check that asks whether a puck covers itself up is gone"); + return AUDIT.slice(at, AUDIT.indexOf("\n },", at)); + }; + + // The mark that was sitting on names was neither of the two this check knew about. + test("the chair number counts as something a puck wears", () => { + assert.match(check(), /const MARKS = "\.dealerChip, \.blindBadge, \.seatChairNumber"/); + }); + + // And a badge painted over the number is a puck covering itself up, which is the + // whole subject of this check, and was invisible to it. + test("and the marks are held against each other, not only against the writing", () => { + const body = check(); + assert.match(body, /marks\.forEach\(\(a, i\) => marks\.slice\(i \+ 1\)/); + assert.match(body, /px across/); + }); + + test("a mark that isn't drawn isn't an offender", () => { + const body = check(); + assert.match(body, /style\.display === "none"/); + assert.match(body, /parseFloat\(style\.opacity\) < 0\.05/); + // An empty badge slot has a box and no text; three of them sit on every puck. + assert.match(body, /if \(!\(mark\.textContent \|\| ""\)\.trim\(\)\) return null;/); + }); + + test("and the check says what it now covers", () => { + assert.match(AUDIT, /name: "nothing a seat wears covers its name or another mark"/); + }); +}); diff --git a/tests/holdem/uiAudit.js b/tests/holdem/uiAudit.js index c5517c4..63d0ace 100644 --- a/tests/holdem/uiAudit.js +++ b/tests/holdem/uiAudit.js @@ -1258,36 +1258,67 @@ const LayoutAudit = { // right without any of them being modelled. Line boxes rather than one bounding box, // too: a two-line name that clears the chip on the line beside it is not covered // because its *other* line happens to sit under where the chip is. + // + // Two things a puck wears were missing from this for eleven rounds, and between them + // they are why the chair number could sit on the felt covered up without the sweep + // ever saying so. + // + // The first is the chair number itself. This check has always asked whether the + // button or a blind badge sits on a name, and the mark that was sitting on names was + // neither of those: measured across 348 seats on 36 tables, 51 of them had the number + // printed on the first letter of the player's own name. + // + // The second is that marks were only ever tested against the *writing*, never against + // each other — so a badge painted straight over the chair number, which is what all + // 66 seats posting a blind were doing, was invisible to a check whose whole subject + // is a puck covering itself up. seatLabelsClear() { const offenders = []; + // What a puck wears, as opposed to what it writes. + const MARKS = ".dealerChip, .blindBadge, .seatChairNumber"; + const painted = mark => { + const box = mark.getBoundingClientRect(); + if (!box.width || !box.height) return null; + if (!(mark.textContent || "").trim()) return null; + const style = getComputedStyle(mark); + if (style.display === "none" || style.visibility === "hidden" || + parseFloat(style.opacity) < 0.05) return null; + return box; + }; + const across = (a, b) => { + const wide = Math.min(a.right, b.right) - Math.max(a.left, b.left); + const deep = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); + return wide > 1 && deep > 1 ? wide : 0; + }; + document.querySelectorAll(".seat").forEach(seat => { + const marks = [...seat.querySelectorAll(MARKS)] + .map(mark => ({ mark, box: painted(mark) })) + .filter(entry => entry.box); + const name = seat.querySelector(".seatPuckName"); - if (!name || !name.textContent.trim()) return; - - const range = document.createRange(); - range.selectNodeContents(name); - const lines = [...range.getClientRects()].filter(line => line.width > 1 && line.height > 1); - if (!lines.length) return; - - seat.querySelectorAll(".dealerChip, .blindBadge").forEach(mark => { - const over = mark.getBoundingClientRect(); - if (!over.width || !over.height) return; - const marked = getComputedStyle(mark); - if (marked.display === "none" || marked.visibility === "hidden" || - parseFloat(marked.opacity) < 0.05) return; - - let worst = 0; - lines.forEach(line => { - const across = Math.min(line.right, over.right) - Math.max(line.left, over.left); - const down = Math.min(line.bottom, over.bottom) - Math.max(line.top, over.top); - if (across > 1 && down > 1) worst = Math.max(worst, across); + if (name && name.textContent.trim()) { + const range = document.createRange(); + range.selectNodeContents(name); + const lines = [...range.getClientRects()].filter(line => line.width > 1 && line.height > 1); + marks.forEach(({ mark, box }) => { + const worst = lines.reduce((most, line) => Math.max(most, across(line, box)), 0); + if (worst > 1) { + offenders.push(`${this.describe(name)} :: ${Math.round(worst)}px under ${this.describe(mark)}`); + } }); + } + + // And the marks against each other. The rim holds three of them — the blinds + // at one end, the button at the other — and there is not room for a third. + marks.forEach((a, i) => marks.slice(i + 1).forEach(b => { + const worst = across(a.box, b.box); if (worst > 1) { - offenders.push(`${this.describe(name)} :: ${Math.round(worst)}px under ${this.describe(mark)}`); + offenders.push(`${this.describe(a.mark)} :: ${Math.round(worst)}px across ${this.describe(b.mark)}`); } - }); + })); }); - return { name: "seat names aren't painted under their own badges", offenders }; + return { name: "nothing a seat wears covers its name or another mark", offenders }; }, // A seat's writing, covered by something belonging to a different seat.