// Whether two seats can end up wanting the same pixels. // // The felt used to decide this from one number: the straight-line distance between the // closest two *neighbouring* seats, judged against a fraction of a puck's width. Both // halves of that were wrong, and the running page showed it. Measured across nine // viewports and six table sizes: // // seventeen of fifty-four tables had one seat's name or stack painted under another // ten of them had the collision between two seats that are not neighbours // // The second number is the interesting one. On a phone held sideways the felt is 528px // wide and 87 tall, the ring is very nearly a straight line, and the seat that lands on // top of you is the one across the table — seats 1 and 7, overlapping by 68px, while // every neighbouring pair was fine and the check said the table was clean. // // The first is what a distance test can't fix even looking at every pair: distance has // one number in it, and a puck is half again as wide as it is tall. 84px between centres // is generous side by side and nowhere near enough on the diagonal. 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 } = require("./load"); const CSS = fs.readFileSync( path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8"); describe("two boxes on a ring", () => { // A phone held sideways, once the action bar has taken its share: a felt far wider // than it is tall, on which the ring is very nearly a straight line. const felt = { width: 400, height: 70 }; const flat = { rx: 44, ry: 42 }; test("a pair that is not neighbours still counts", () => { // Eight seats, a 48x60 puck. Every neighbouring pair is clear. Seats 1 and 5 are // on top of each other, and so are 2 and 4, and 6 and 8 — the seat that lands on // you here is the one across the table, because the rim doubles back at the ends // of a shape this flat and a long walk apart is no distance at all. assert.equal(Render.seatsCollide(8, felt, flat, 48, 60), true); const neighbours = (count, w, h) => { const points = Render.ringPositions(count, flat.rx, flat.ry, felt.width / felt.height) .map(s => ({ x: (s.left / 100) * felt.width, y: (s.top / 100) * felt.height })); return points.some((point, i) => { const next = points[(i + 1) % points.length]; return Math.abs(point.x - next.x) < w && Math.abs(point.y - next.y) < h; }); }; assert.equal(neighbours(8, 48 + Render.SEAT_CLEARANCE_PX, 60 + Render.SEAT_CLEARANCE_PX), false, "no neighbouring pair overlaps, which is exactly why this had to look at all of them"); }); test("and the answer is the box, not a radius", () => { // Two seats 60px apart across and 35px down, with a 64x40 puck. The distance // between them is 69px, so the test this replaces — "are these two further apart // than a puck is wide?" — called it clear. The boxes overlap by 4px by 5px. const boxes = { width: 200, height: 100 }; const twoSeats = (dx, dy) => { const original = Render.ringPositions; Render.ringPositions = () => [ { left: 50 - (dx / 2 / boxes.width) * 100, top: 50 - (dy / 2 / boxes.height) * 100 }, { left: 50 + (dx / 2 / boxes.width) * 100, top: 50 + (dy / 2 / boxes.height) * 100 }, ]; try { return Render.seatsCollide(2, boxes, flat, 64, 40, 0); } finally { Render.ringPositions = original; } }; assert.ok(Math.hypot(60, 35) > 64, "and a distance test would have passed it"); assert.equal(twoSeats(60, 35), true, "60 across and 35 down, with a 64x40 puck"); assert.equal(twoSeats(60, 45), false, "far enough apart vertically"); assert.equal(twoSeats(66, 10), false, "far enough apart horizontally"); }); test("with daylight, so rounded corners don't read as touching", () => { assert.ok(Render.SEAT_CLEARANCE_PX > 0); }); test("a table nobody is sitting at can't collide with itself", () => { assert.equal(Render.seatsCollide(1, felt, flat, 96, 60), false); assert.equal(Render.seatsCollide(9, null, flat, 96, 60), false); }); }); describe("the widths the felt may choose from", () => { test("run widest first, because the widest that fits is the one wanted", () => { const widths = Render.seatWidthCandidates(); widths.slice(1).forEach((w, i) => assert.ok(w < widths[i], `${widths[i]} then ${w}`)); }); test("and span the whole declared range", () => { const widths = Render.seatWidthCandidates(); assert.equal(widths[0], Render.SEAT_WIDTH_MAX); assert.equal(widths[widths.length - 1], Render.SEAT_WIDTH_MIN); }); test("the floor is still a name, not a smudge", () => { // At 44px the tight layout shows about five characters and an ellipsis. Below // that a puck stops telling the dealer who is sitting there, which is the one // thing on the felt it exists to say. assert.ok(Render.SEAT_WIDTH_MIN >= 44); assert.ok(Render.SEAT_TIGHT_BELOW_PX > Render.SEAT_WIDTH_MIN); assert.ok(Render.SEAT_TIGHT_BELOW_PX < Render.SEAT_WIDTH_MAX); }); test("and there are few enough of them to measure after every tap", () => { // Each one costs a forced layout. Ten is the budget; the cache carries the rest. assert.ok(Render.seatWidthCandidates().length <= 12); }); }); describe("what the stylesheet has to keep holding up", () => { test("the puck's width comes from the property, not from a breakpoint", () => { // Three media queries used to declare it — 78px, 64px and 62px — three guesses // at a number that depends on how many people are sitting down, which no media // query can see. The same 390px phone has room for 90px at four seats and 61 at // nine. const declarations = CSS.replace(/\/\*[\s\S]*?\*\//g, ""); const seatRules = [...declarations.matchAll(/(^|[,}])\s*(\.seat)\s*\{([^}]*)\}/g)] .map(m => m[3]); const hardWidths = seatRules .flatMap(body => body.match(/(? !decl.includes("var(--seatPuckW")); assert.deepEqual(hardWidths, []); }); test("the puck measures what it says it measures", () => { // As content-box a 64px puck came out 74 across once its padding and border were // counted, and the search compares that number against distances on the felt. const rule = CSS.match(/\n\.seat \{[^}]*\}/); assert.ok(rule, ".seat rule not found"); assert.match(rule[0], /box-sizing:\s*border-box/); }); test("and a blind badge never reaches sideways off its own puck", () => { // This is what makes the footprint the puck and nothing more. The badge's // remaining overhang is vertical, and it points away from the table at both ends // — `top` on the upper arc, `bottom` on the lower — so two seats facing each // other across the felt wear their badges back to back. // // It was not always so: at `left: -6px` the small blind hung off the puck's left // corner, and on an iPhone Pro Max at six seats — pucks not touching at all — // "SB" was painted across the next player's $1,137. const badges = [...CSS.matchAll(/\.(sb|bb|str)Badge\s*\{([^}]*)\}/g)]; assert.equal(badges.length, 3, "expected all three blind badges"); badges.forEach(([, which, body]) => { const left = body.match(/(?= 0, `.${which}Badge sits at ${left[1]}px, off the left of its own puck`); }); }); test("and all three sit in the same place, so there is one spot to look at", () => { // Two badges in two positions means the dealer's eye has to find which of them // this seat is using before it can read which blind it is, forty times a night. // A seat only ever wears one: the blinds are different players by construction, // and the straddle chain starts left of the big blind and stops short of the // small one. const lefts = [...CSS.matchAll(/\.(sb|bb|str)Badge\s*\{([^}]*)\}/g)] .map(([, , body]) => (body.match(/(? { // It didn't, for a while, and the reasoning had one hole in it. The badge hangs 9px // off the puck and always points away from the table — `top: -9px` on the upper arc, // `bottom: -9px` on the lower — so two seats facing each other across the felt wear // them back to back and it costs nothing. That much is true. // // What was also claimed is that two seats on the *same* arc "wear them at the same // height, offset by exactly as much as the pucks are". True of a straight rail, // false of a ring: neighbours on a curve sit at different heights, so the badge // pointing away from the table on one of them points at the corner of the other. // Measured on the running page, three tables in the sweep had a player's stack // painted under the next player's SB or BB — and the badge outranks everything else // on the felt, so what the dealer reads in that spot is the wrong number entirely. test("a seat needs room for the badge it might be wearing", () => { const puck = { width: 64, height: 40 }; assert.deepEqual(Render.seatFootprint(puck), { width: 64, height: 40 + Render.SEAT_BADGE_OVERHANG_PX }); }); test("sideways it costs nothing, because the badge doesn't go sideways", () => { // It sits inside the puck's own width — see "all three sit in the same place" // above, which holds the stylesheet to it. assert.equal(Render.seatFootprint({ width: 64, height: 40 }).width, 64); }); test("the number here is the number the stylesheet draws", () => { // Two files have to agree about one distance: holdem.css hangs the badge off the // puck by it, render.js charges every chair in the ring for it. They are wired // together by nothing but this test. Drift either way is silent — too small and // badges land on neighbours again, too large and every puck on a nine-handed // phone gives up a size step for clearance nothing needs. const top = CSS.match(/\.blindBadge \{[^}]*?(? { // seatsCollide is given one box and compares centres against it, so the // separation it asks for is one puck plus one badge. That is exactly right for // two seats on the same arc each wearing one, and generous for the far commoner // case where only one of the two is in the blinds at all. Counting it on both // sides is what made a landscape table unsatisfiable the first time this was // tried, back when that felt was 87px tall. const puck = { width: 64, height: 40 }; const grew = Render.seatFootprint(puck).height - puck.height; assert.equal(grew, Render.SEAT_BADGE_OVERHANG_PX); assert.ok(grew < 2 * Render.SEAT_BADGE_OVERHANG_PX); }); }); describe("with no browser to measure", () => { test("nothing here throws", () => { assert.equal(Render.measuredSeatBox(), null); assert.equal(Render.seatFootprint(null), null); }); });