// Where the seats sit on the rim. // // The ring used to space seats by angle, which is only the same as spacing them evenly // when the felt is a circle. On a phone held sideways the felt is 601x126, and equal // angles put four seats down each steep end, 20px apart, with pucks 72px tall — so a // seat's centre belonged to its neighbour and tapping a player got the wrong one. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { Render } = require("./load"); // The seats as a dealer sees them: pixels on a felt of a given size. const ringPixels = (count, width, height, rx = 44, ry = 42) => { const aspect = width / height; return Array.from({ length: count }, (_, i) => { const pos = Render.positionOnRing(i, count, rx, ry, aspect); return { x: (pos.left / 100) * width, y: (pos.top / 100) * height }; }); }; const gaps = (points) => points.map((p, i) => { const next = points[(i + 1) % points.length]; return Math.hypot(next.x - p.x, next.y - p.y); }); // What the ring did before: one turn cut into equal angles. const byAngle = (index, count, rx, ry) => { const radians = ((-90 + (360 / count) * index) * Math.PI) / 180; return { left: 50 + rx * Math.cos(radians), top: 50 + ry * Math.sin(radians) }; }; describe("a round table", () => { test("still spaces seats by angle, because on a circle that is the same thing", () => { for (let seat = 0; seat < 9; seat++) { const now = Render.positionOnRing(seat, 9, 40, 40, 1); const before = byAngle(seat, 9, 40, 40); assert.ok(Math.abs(now.left - before.left) < 0.2, `seat ${seat} left`); assert.ok(Math.abs(now.top - before.top) < 0.2, `seat ${seat} top`); } }); test("seat one is at the top, whatever shape the felt is", () => { [[600, 600], [601, 126], [351, 351], [949, 683]].forEach(([w, h]) => { const first = Render.positionOnRing(0, 10, 44, 42, w / h); assert.ok(Math.abs(first.left - 50) < 0.01, `${w}x${h} left`); assert.ok(first.top < 50, `${w}x${h} is above the middle`); }); }); }); describe("a felt wider than it is tall", () => { // 640x360 landscape: the size the audit was failing at. const wide = () => ringPixels(10, 601, 126); const byAnglePixels = () => Array.from({ length: 10 }, (_, i) => { const pos = byAngle(i, 10, 44, 42); return { x: (pos.left / 100) * 601, y: (pos.top / 100) * 126 }; }); // The two seats at the right-hand end, which is where they crowd. const endPairApart = (seats) => { const rightmost = [...seats].sort((a, b) => b.x - a.x).slice(0, 2); return Math.abs(rightmost[0].y - rightmost[1].y); }; test("pulls the pair at each end apart", () => { // They still share the end — ten seats on a rim this flat have nowhere else to // be — but they stop sitting on top of each other. const now = endPairApart(wide()); const before = endPairApart(byAnglePixels()); assert.ok(now > before * 1.4, `${now.toFixed(1)}px now, ${before.toFixed(1)}px before`); }); test("nearly doubles the room between the closest two neighbours", () => { // Not equal spacing in a straight line — at the ends the rim turns back on // itself, so the same walk along it covers less ground across. Even there the // gap goes from a third of a puck to most of one. const now = Math.min(...gaps(wide())); const before = Math.min(...gaps(byAnglePixels())); assert.ok(now > before * 1.7, `${now.toFixed(1)}px now, ${before.toFixed(1)}px before`); assert.ok(now > 55, `${now.toFixed(1)}px`); }); test("never sits two seats on the same spot", () => { const seats = wide(); seats.forEach((seat, i) => { seats.slice(i + 1).forEach((other, j) => { const apart = Math.hypot(seat.x - other.x, seat.y - other.y); assert.ok(apart > 50, `seats ${i + 1} and ${i + j + 2} are ${apart.toFixed(1)}px apart`); }); }); }); }); describe("how close the closest two seats come", () => { // What decides whether the pucks shrink. Rim length per seat was the first answer // and it is the wrong question: it measures the walk between two seats, not the // gap. On a long oval the rim doubles back at the ends, so two seats a long walk // apart end up stacked one above the other. const gap = (count, width, height) => Render.closestSeatGapPx(count, { width, height }); test("a flat table no longer stacks two seats at each end", () => { // 616x96: the landscape felt on a 640x360 phone once the bar has grown. On an // ellipse that flat the end pair came out 25px apart with 64px pucks. The rim // is a stadium at this shape, so an end holds one seat. assert.ok(gap(6, 616, 96) > 70, `${gap(6, 616, 96).toFixed(0)}px apart`); assert.ok(gap(12, 616, 96) > 40, `${gap(12, 616, 96).toFixed(0)}px apart at twelve seats`); }); test("an ellipse's ends are still closer than its rim suggests, which is why the shape changes", () => { // Same felt, forced through the ellipse the tool uses for rounder tables. const asEllipse = Render.ringAngles(6, 44, 42, 616 / 96).map(angle => ({ x: ((50 + 44 * Math.cos(angle)) / 100) * 616, y: ((50 + 42 * Math.sin(angle)) / 100) * 96, })); const closest = asEllipse.reduce((least, point, i) => { const next = asEllipse[(i + 1) % asEllipse.length]; return Math.min(least, Math.hypot(point.x - next.x, point.y - next.y)); }, Infinity); assert.ok(closest < 70, `${closest.toFixed(0)}px`); assert.ok(gap(6, 616, 96) > closest * 1.3, `stadium ${gap(6, 616, 96).toFixed(0)}px against ellipse ${closest.toFixed(0)}px`); }); test("a round table's neighbours are about as far apart as the rim says", () => { const rim = (Math.PI * 2 * 0.43 * 400) / 9; assert.ok(Math.abs(gap(9, 400, 400) - rim) < rim * 0.15); }); test("more seats, less room, in that order", () => { assert.ok(gap(12, 400, 400) < gap(9, 400, 400)); assert.ok(gap(9, 400, 400) < gap(6, 400, 400)); assert.ok(gap(6, 400, 400) < gap(6, 800, 800), "and a bigger table gives more"); }); test("nothing to measure between fewer than two seats", () => { assert.equal(gap(1, 400, 400), Infinity); assert.equal(gap(9, 0, 0), Infinity); }); }); describe("which rim a table gets", () => { // An ellipse until the table is much wider than tall, then a stadium: two rails // and a half-round cap at each end, which is how a real poker table is built. const shape = (width, height) => { const aspect = width / height; return 44 * aspect > 42 * Render.STADIUM_RATIO ? "stadium" : "ellipse"; }; test("every layout the tool had keeps the ellipse", () => { assert.equal(shape(844, 608), "ellipse", "desktop"); assert.equal(shape(600, 600), "ellipse", "tablet"); assert.equal(shape(278, 314), "ellipse", "phone upright"); assert.equal(shape(336, 240), "ellipse", "short phone upright"); }); test("a phone on its side gets the stadium", () => { assert.equal(shape(528, 73), "stadium"); assert.equal(shape(780, 113), "stadium"); }); test("the cap holds one seat instead of stacking two", () => { // Six seats on a 616x96 rim: the walk between neighbours has to be longer than // the cap, or two of them end up on the same half-circle. const halfWidth = 44 * (616 / 96), halfHeight = 42; const perimeter = 4 * (halfWidth - halfHeight) + 2 * Math.PI * halfHeight; assert.ok(perimeter / 6 > Math.PI * halfHeight, "one seat per cap at six-handed"); }); test("seat one is still at the top centre, and the ring still mirrors", () => { const seats = Array.from({ length: 8 }, (_, i) => Render.positionOnRing(i, 8, 44, 42, 616 / 96)); assert.ok(Math.abs(seats[0].left - 50) < 0.01); assert.ok(seats[0].top < 50); for (let seat = 1; seat < 4; seat++) { const mirrored = seats[8 - seat]; assert.ok(Math.abs((seats[seat].left - 50) + (mirrored.left - 50)) < 0.01, `seat ${seat} across`); assert.ok(Math.abs(seats[seat].top - mirrored.top) < 0.01, `seat ${seat} down`); } }); test("nobody is drawn off the felt", () => { Array.from({ length: 12 }, (_, i) => Render.positionOnRing(i, 12, 44, 42, 616 / 96)).forEach((seat, i) => { assert.ok(seat.left >= 5 && seat.left <= 95, `seat ${i + 1} across at ${seat.left.toFixed(1)}%`); assert.ok(seat.top >= 7 && seat.top <= 93, `seat ${i + 1} down at ${seat.top.toFixed(1)}%`); }); }); }); describe("the shape of the ring", () => { test("is a mirror image left to right", () => { const seats = ringPixels(10, 601, 126); for (let seat = 1; seat < 5; seat++) { const mirrored = seats[10 - seat]; assert.ok(Math.abs((seats[seat].x - 300.5) + (mirrored.x - 300.5)) < 0.5, `seat ${seat} across`); assert.ok(Math.abs(seats[seat].y - mirrored.y) < 0.5, `seat ${seat} down`); } }); test("holds for the smallest and largest tables the seat count allows", () => { [2, 3, 4, 9, 12, 24].forEach(count => { const seats = ringPixels(count, 601, 126); assert.equal(seats.length, count); seats.forEach((seat, i) => { assert.ok(Number.isFinite(seat.x) && Number.isFinite(seat.y), `${count}-handed seat ${i + 1}`); assert.ok(seat.x >= 0 && seat.x <= 601, `${count}-handed seat ${i + 1} is on the felt`); }); assert.ok(Math.min(...gaps(seats)) > 0, `${count}-handed seats are distinct`); }); }); test("shares the rim out equally on a round table at any size", () => { [3, 6, 9, 10, 24].forEach(count => { const spacing = gaps(ringPixels(count, 500, 500, 40, 40)); const shortest = Math.min(...spacing); const longest = Math.max(...spacing); assert.ok(longest - shortest < 0.5, `${count}-handed gaps ran ${shortest.toFixed(1)}-${longest.toFixed(1)}px`); }); }); test("asked for no seats, offers no positions", () => { assert.deepEqual(Render.ringAngles(0, 44, 42, 1), []); }); test("puts each bet between its own player and the pot", () => { // Where a dealer pushes the chips: on that player's line to the middle, short // of the seat. const bearing = (point) => Math.atan2(point.top - 50, point.left - 50); const outFromMiddle = (point) => Math.hypot(point.left - 50, point.top - 50); [[601, 126], [810, 583], [351, 351]].forEach(([w, h]) => { for (let seat = 0; seat < 9; seat++) { const player = Render.seatPosition(seat, 9, w / h); const bet = Render.betPosition(seat, 9, w / h); const offBy = Math.abs(bearing(bet) - bearing(player)) * (180 / Math.PI); assert.ok(offBy < 1, `${w}x${h} seat ${seat + 1} is ${offBy.toFixed(1)}° off its spoke`); assert.ok(outFromMiddle(bet) < outFromMiddle(player), `${w}x${h} seat ${seat + 1} is short of the seat`); } }); }); });