From dc5db16ffd091276307d179512dc35447a394e91 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Wed, 2 Sep 2026 04:33:25 -0600 Subject: [PATCH] Let the phone table use the width it has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last round capped the felt so the ring would clear the action bar, and capped the wrong dimension. The wide layout sizes the felt from its width, so capping the width is how you shorten it there — on a phone the felt is already the full width of the column, and capping that threw away 96px of a 360px screen: a 240x240 table inside a 336px space, with nine pucks crowding it. A max-height instead leaves the width alone and lets the aspect ratio give way, which turns a short screen into a wide oval — the shape a poker table is. At 360x640: 336x240 rather than 240x240, same clearance over the bar, and the twelve-seat ring that was crowded at 240 comes out clean, which closes the one combination left failing last round. Where the room isn't the binding constraint, 390x844 and up, nothing changes and the table stays round. The landscape branch now says max-height: none, because it already spends the same measured room on its own height and applying it twice took the felt to the floor value. The measurement that decides whether pucks shrink was also wrong, in a way that only showed up once it was asked a hard question. Rim length per seat measures the walk between two seats, not the gap between them, and on a long oval the rim doubles back at the ends: twelve seats at 640x360 had 193px of rim each and sat 60px apart. It now measures the closest two neighbours directly, off the ring itself — and off the seat layer rather than the felt, because the layer is inset by half a puck on a phone and the felt's box put the answer out by 30px. That makes the rule geometric rather than about screen size, so it is no longer scoped to a phone media query: a wide landscape felt crowds at its ends while its rim claims there is room, and now it is told so. Pucks still touch at nine and twelve seats on a small screen. Tried 46px against 58px: on a roomy phone 46 cleared every overlap where 58 left two, but in a hand with a side pot, where the bar has grown and the felt has shrunk to suit, 46 overlapped four times and 58 twice. That is the felt being small, not the puck being wide, and paying for it with an ellipsis on every long name buys nothing. What has to hold is that each seat keeps its own centre, and that is measured at every size. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 77 ++++++++++++------- .../wwwroot/js/casino/holdem/render.js | 60 +++++++++------ tests/holdem/seatRing.test.js | 30 ++++++++ 3 files changed, 118 insertions(+), 49 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index baf7970..017b86e 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -820,6 +820,38 @@ button, outline-offset: 1px; } +/* Seats close enough together that full-size pucks would overlap. The class is set + from a measurement render.js takes off the ring itself — the closest two neighbours, + straight line — because no media query can see it: the same 390px phone is roomy at + six seats and crowded at nine, and a wide landscape felt crowds at its ends while + its rim says there is space. Not scoped to a screen size for the same reason. */ +.seatsTight .seat { + /* 58 was chosen by measurement and 46 was tried against it. On a roomy phone 46 + cleared every overlap at nine seats where 58 left two — but in the state that + actually matters, a hand with a side pot where the bar has grown and the felt has + shrunk to suit, 46 overlapped four times and 58 twice. Pucks touching at that + density is the felt being small, not the puck being wide, and paying for it with + an ellipsis on every long name buys nothing. What has to hold is that each seat + keeps its own centre, so a tap lands on the player it was aimed at, and that is + measured at every size. */ + width: 58px; + min-height: 38px; + padding: 3px 4px; +} + +.seatsTight .seatPuckName { + margin-top: 0.35rem; + font-size: 0.62rem; +} + +.seatsTight .seatPuckStack { + font-size: 0.55rem; +} + +.seatsTight .seatPuckCards { + font-size: 0.58rem; +} + /* ---------- Seat editor panel (sidebar form for the selected seat) ---------- */ .seatEditorPanel { @@ -2921,34 +2953,20 @@ body.hasActionBar #toastHost { That trade was real then and isn't now: seats are spaced by distance around the rim rather than by angle, so a shorter felt no longer piles them up. Re-measured at 360x640 with the bar grown by a side pot, where the bottom seat sat 38px - inside it: capped, the felt is 240px and the ring clears the bar by 50, with the - audit clean at 6, 9 and 10 seats. Scroll room is not an answer for a seat that is - covered rather than merely below the fold. */ + inside it: the ring now clears the bar by 50. Scroll room is not an answer for a + seat that is covered rather than merely below the fold. + + The room is spent on height, not on both. Capping the *width* by it — the wide + layout's way, because there the felt is sized from its width — kept the table + square and threw away 96px of a 360px screen: 240x240 inside a 336px column. A + max-height leaves the width alone and lets the aspect ratio give way, so a short + screen gets a wide oval, which is the shape a poker table is anyway. Measured at + 360x640: 336x240 rather than 240x240, same clearance, and the twelve-seat ring + that was crowded at 240 comes out clean. Where the room isn't the binding + constraint — 390x844 and up — nothing changes and the table stays round. */ #pokerTableWrap { - max-width: calc(var(--feltRoomPx, 100vh) / var(--feltAspect)); - } - - /* A felt small enough that the rim can't give each seat its own puck width. Set - from the measured rim in render.js rather than from the window, because the - question is how much table there is per player, which no media query can see: - nine seats on a 240px felt get 72px of rim each and the puck is 76 across. */ - .seatsTight .seat { - width: 58px; - min-height: 38px; - padding: 3px 4px; - } - - .seatsTight .seatPuckName { - margin-top: 0.35rem; - font-size: 0.62rem; - } - - .seatsTight .seatPuckStack { - font-size: 0.55rem; - } - - .seatsTight .seatPuckCards { - font-size: 0.58rem; + max-width: none; + max-height: var(--feltRoomPx, 100vh); } /* Stacked, with the action that ends the hand furthest from a stray thumb. */ @@ -3312,6 +3330,11 @@ body.hasActionBar #toastHost { a table that is already as short as the screen allows. */ aspect-ratio: auto; max-width: none; + /* This branch already spends the measured room on its own height. The phone + block's max-height is the same number arriving twice, and applying it here + took the felt down to the floor value and crowded the ring — measured at + 640x360, two seats lost their centres to their neighbours. */ + max-height: none; } /* The page's own top padding is 32px of nothing, on the screen that has the least diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js index bde0ef3..e5a76ad 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js @@ -91,12 +91,21 @@ const Render = { // circle when there's nothing to measure — the same answer the ring gave before it // asked. feltAspect() { - if (typeof document === "undefined") return 1; - const wrap = document.getElementById("pokerTableWrap"); - if (!wrap) return 1; - const box = wrap.getBoundingClientRect(); - if (!box.width || !box.height) return 1; - return box.width / box.height; + const box = this.ringBox(); + return box ? box.width / box.height : 1; + }, + + // The box a seat's left/top percentages actually resolve against. Not the felt: on + // a phone the seat layer is inset by half a puck so the outermost seats don't hang + // off the screen, and measuring the felt instead put the ring's shape out by that + // inset — twelve seats at 640x360 were computed 90px apart and drawn 60px apart, + // which is the difference between "fine" and "overlapping". + ringBox() { + if (typeof document === "undefined") return null; + const layer = document.getElementById("seatsLayer") || document.getElementById("pokerTableWrap"); + if (!layer) return null; + const box = layer.getBoundingClientRect(); + return box.width && box.height ? box : null; }, // Bets sit on the felt between their player and the pot, the way chips actually @@ -137,23 +146,30 @@ const Render = { RING_STEPS: 720, ringCache: {}, - // How much rim each seat gets, in pixels. Ramanujan's approximation for an - // ellipse's perimeter, which is well inside a pixel at these sizes. - rimPerSeat(count) { - const wrap = document.getElementById("pokerTableWrap"); - if (!wrap || !count) return Infinity; - const box = wrap.getBoundingClientRect(); - const a = (box.width * 44) / 100; - const b = (box.height * 42) / 100; - if (!a || !b) return Infinity; - const perimeter = Math.PI * (3 * (a + b) - Math.sqrt((3 * a + b) * (a + 3 * b))); - return perimeter / count; + // The closest two neighbouring seats come to each other, straight line, in pixels. + // + // Rim length divided by seat count was the first answer and it is the wrong one: + // it measures the walk between two seats rather than the gap between them, and on + // a flat ellipse those are nothing alike. At 640x360 six seats had 193px of rim + // each and sat 56px apart, because at the ends of a long oval the rim doubles back + // and two seats a long walk apart are stacked one above the other. + closestSeatGapPx(count, box = this.ringBox()) { + if (count < 2 || !box || !box.width || !box.height) return Infinity; + + const points = this.ringAngles(count, 44, 42, box.width / box.height).map(angle => ({ + x: ((50 + 44 * Math.cos(angle)) / 100) * box.width, + y: ((50 + 42 * Math.sin(angle)) / 100) * box.height, + })); + return points.reduce((closest, point, i) => { + const next = points[(i + 1) % points.length]; + return Math.min(closest, Math.hypot(point.x - next.x, point.y - next.y)); + }, Infinity); }, - // A puck is 76px across. Under about that much rim each, neighbours start covering - // each other's middles, and a tap meant for one player lands on the next — so the - // pucks come down a size rather than the table pretending it has room it hasn't. - SEAT_ROOM_PX: 88, + // A puck is 76px across, so neighbours whose centres are closer than that overlap + // by definition; a few pixels more keeps them from touching. Under it the pucks + // come down a size rather than the table pretending it has room it hasn't. + SEAT_ROOM_PX: 84, // Called on every render and again whenever the felt is resized — the action bar // growing mid-hand shortens the table, and the seats have to answer that without @@ -162,7 +178,7 @@ const Render = { const layer = document.getElementById("seatsLayer"); if (!layer || !this.state) return; layer.classList.toggle("seatsTight", - this.rimPerSeat(this.state.seatCount) < this.SEAT_ROOM_PX); + this.closestSeatGapPx(this.state.seatCount) < this.SEAT_ROOM_PX); }, // The angles that cut the rim into `count` equal lengths, starting at the top. diff --git a/tests/holdem/seatRing.test.js b/tests/holdem/seatRing.test.js index 7ed6901..72f5f94 100644 --- a/tests/holdem/seatRing.test.js +++ b/tests/holdem/seatRing.test.js @@ -92,6 +92,36 @@ describe("a felt wider than it is tall", () => { }); }); +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's ends are far closer than its rim suggests", () => { + // 616x96: the landscape felt on a 640x360 phone once the bar has grown. + const rim = Math.PI * (3 * (271 + 40) - Math.sqrt((3 * 271 + 40) * (271 + 3 * 40))) / 6; + assert.ok(gap(6, 616, 96) < rim / 3, `${gap(6, 616, 96).toFixed(0)}px apart, ${rim.toFixed(0)}px of rim each`); + }); + + 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("the shape of the ring", () => { test("is a mirror image left to right", () => { const seats = ringPixels(10, 601, 126);