Take the chair number off the corner it was never alone in
A seat's number was a grey digit in the top-left corner of the puck, and the top-left corner is also 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 it printed under something else. All 66 seats posting a blind had it covered outright — a 22px badge at z-index 4 over a 6px digit — 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 has the other 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 name, bet and stack, and all three are about this hand. The chair number is the only mark on a puck that isn't, so it is the one that gave way. Nothing is lost that a tap doesn't return. The seat editor's heading reads "Mike from work · Seat 2" now and the seat menu's first line leads with the chair; a seat nobody has named still calls itself "Seat 5"; and an open chair still wears its number large and centred, which is the one moment the number is the thing being looked for rather than a footnote on somebody else's name. The sweep had never mentioned any of this. The check that asks whether a puck covers itself up knew about two of the three things a puck wears, and only ever held them against writing — so the mark sitting on the names was invisible to it, and so was a badge painted straight over another mark. Both gaps are closed: put the number back and it reports five offenders on a Galaxy S8 ten-handed, naming Christopher, Alexandra, Jonah and the SB. 1148 tests, 418 audits clean.
This commit is contained in:
@@ -1086,6 +1086,30 @@ button,
|
|||||||
color: var(--text-muted);
|
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 ----
|
/* ---- 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
|
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.
|
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
|
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
|
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
|
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
|
largest size at which "Mike from work", the longest name the sweep uses, still comes
|
||||||
|
|||||||
@@ -2302,7 +2302,9 @@ const Render = {
|
|||||||
|
|
||||||
const index = this.selectedSeatIndex;
|
const index = this.selectedSeatIndex;
|
||||||
const player = this.state.seats[index];
|
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 = "";
|
body.innerHTML = "";
|
||||||
const template = document.getElementById("seatEditorTemplate");
|
const template = document.getElementById("seatEditorTemplate");
|
||||||
|
|||||||
@@ -243,7 +243,12 @@ const SeatMenu = {
|
|||||||
|
|
||||||
const stack = Betting.stackOf(this.state, player);
|
const stack = Betting.stackOf(this.state, player);
|
||||||
const owed = Betting.toCall(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 (stack > 0) bits.push(`${Money.format(stack)} behind`);
|
||||||
if (owed > 0) bits.push(`${Money.format(owed)} to call`);
|
if (owed > 0) bits.push(`${Money.format(owed)} to call`);
|
||||||
subtitle.textContent = bits.join(" · ");
|
subtitle.textContent = bits.join(" · ");
|
||||||
|
|||||||
@@ -476,6 +476,23 @@ const HoldemState = {
|
|||||||
return player.name && player.name.trim() ? player.name.trim() : `Seat ${seatIndex + 1}`;
|
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.
|
// 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
|
// Chips already pushed into the middle count whether or not the player is still
|
||||||
|
|||||||
@@ -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"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
+53
-22
@@ -1258,36 +1258,67 @@ const LayoutAudit = {
|
|||||||
// right without any of them being modelled. Line boxes rather than one bounding box,
|
// 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
|
// 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.
|
// 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() {
|
seatLabelsClear() {
|
||||||
const offenders = [];
|
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 => {
|
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");
|
const name = seat.querySelector(".seatPuckName");
|
||||||
if (!name || !name.textContent.trim()) return;
|
if (name && name.textContent.trim()) {
|
||||||
|
const range = document.createRange();
|
||||||
const range = document.createRange();
|
range.selectNodeContents(name);
|
||||||
range.selectNodeContents(name);
|
const lines = [...range.getClientRects()].filter(line => line.width > 1 && line.height > 1);
|
||||||
const lines = [...range.getClientRects()].filter(line => line.width > 1 && line.height > 1);
|
marks.forEach(({ mark, box }) => {
|
||||||
if (!lines.length) return;
|
const worst = lines.reduce((most, line) => Math.max(most, across(line, box)), 0);
|
||||||
|
if (worst > 1) {
|
||||||
seat.querySelectorAll(".dealerChip, .blindBadge").forEach(mark => {
|
offenders.push(`${this.describe(name)} :: ${Math.round(worst)}px under ${this.describe(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);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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.
|
// A seat's writing, covered by something belonging to a different seat.
|
||||||
|
|||||||
Reference in New Issue
Block a user