// 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("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`); } }); }); });