anvilsign in

collin/mahjong · c3d1289b

The empty melds row stops being a wall, and a meld is a full tile

Collin Richards · 2026-08-21 18:03 UTC · c3d1289b6539f800067046718eb8a510c9596bd5 · parent feaee21e · browse files

modifiedsrc/styles.css+32 −7
⋯ 7 unchanged lines
88 middle of the table now rather than in a row inside each strip, and the
99 depth that freed up was given to the centre instead of to the tiles. The
1010 wall square and the pile inside it are what wanted the room. */
11- --depth: clamp(202px, 26vh, 276px);
11+ --depth: clamp(226px, 30vh, 307px);
1212 --h-len: max(160px, calc(100vw - 2 * var(--depth)));
1313 --v-len: 100vh;
1414 /* Height the action buttons occupy, held whether or not any are showing.
⋯ 2 unchanged lines
1717 is what keeps everything above it still. Sized for the tallest button: the
1818 ones that name a tile (打出 五萬, 吃 三萬 四萬) carry a third line. */
1919 --bar-h: 52px;
20- /* Largest tile a strip can hold. Edge inwards it has to fit the melds (0.86
21- tiles tall), the button bar, the hand (1.71 + 6px) and the nameplate
22- (28px), plus its own padding, gaps and margins — so 2.57 tiles plus 122px.
20+ /* Largest tile a strip can hold. Edge inwards it has to fit the melds (a
21+ full tile, 1.375 tall, since they are the same tiles as the ones in the
22+ hand), the button bar, the hand (1.71 + 6px) and the nameplate (28px), plus
23+ its own padding, gaps and margins — so 3.085 tiles plus 122px.
24+
25+ The melds used to be drawn at 0.62 of a tile and budgeted at 0.86, which is
26+ where the old 2.57 came from; `--depth` grew by the difference rather than
27+ the tiles shrinking, for the same reason it shrank when the discards moved
28+ out to the middle. The strip is deeper, but the felt is *bigger* than it
29+ was for any seat that has not claimed anything — see geometry.ts.
2330
2431 It used to be 3.45 tiles: the missing 0.88 is the row of discards, which is
2532 in the middle of the table now. `--depth` came down by the same amount
⋯ 3 unchanged lines
2936 Tile size otherwise follows the strip's *length*, which is why a window
3037 with less height than F11 kept full-size tiles and left the strip's
3138 contents to be cut off. */
32- --tile-fit: calc((var(--depth) - 122px) / 2.57);
39+ --tile-fit: calc((var(--depth) - 122px) / 3.085);
3340
3441 /* One size for every tile on the table — in a hand, in the wall, and lying in
3542 the middle. They are the same tiles, so drawing them at different sizes was
⋯ 168 unchanged lines
204211 border-radius: calc(var(--tw) * 0.1);
205212 transition: transform 0.12s ease, filter 0.12s ease;
206213 }
214+/* A melded tile is the same tile it was in the hand it came out of, so it is
215+ the same size. It used to be drawn at 0.62 and read as a different, smaller
216+ set of tiles sitting beside the real ones. */
207217 .tile.t-meld {
208- --tw: calc(var(--tile-w) * 0.62);
218+ --tw: var(--tile-w);
209219 }
210220 .tile.t-small {
211221 --tw: calc(var(--tile-w) * 0.64);
⋯ 108 unchanged lines
320330 pointer-events: none;
321331 }
322332
333+/* No reservation. The strip is packed towards its outer edge and this is the
334+ innermost thing in it, so a meld appearing grows inwards and moves nothing —
335+ the height was only ever holding open a band of empty green between the seat
336+ and the middle, which table/geometry.ts then had to hand back to the felt.
337+ Room for one is still budgeted in `--tile-fit`, so a claim cannot overflow
338+ the strip. */
323339 .melds {
324340 display: flex;
325341 gap: 10px;
326342 flex-wrap: wrap;
327343 justify-content: center;
328- min-height: calc(var(--tile-w) * 0.86);
329344 flex: none;
330345 }
331346 .meld {
⋯ 1236 unchanged lines
15681583 min-height: 0;
15691584 max-width: 100%;
15701585 }
1586+/* Same size as the hand they were laid down from — which on an opponent's card
1587+ is that card's own smaller tile, not the table's. */
1588+.compact .opp .melds .tile {
1589+ --tw: calc(var(--tile-w) * 0.52);
1590+}
15711591 .compact .opp .meld.flowers {
15721592 margin-left: 5px;
15731593 padding-left: 5px;
⋯ 51 unchanged lines
16251645 min-height: 0;
16261646 gap: 6px;
16271647 }
1648+/* Full size, like the hand under it — the discards beside them stay at half,
1649+ which is now the whole of what tells the two rows apart. */
1650+.compact .mine .melds .tile {
1651+ --tw: calc(var(--tile-w) * 0.72);
1652+}
16281653 .compact .mine .discards {
16291654 grid-area: discards;
16301655 /* One line that scrolls, kept pinned to its end by Seat.tsx — wrapping would
⋯ 641 unchanged lines
modifiedsrc/table/geometry.ts+83 −2
⋯ 116 unchanged lines
117117 : launch[seat];
118118 }
119119
120- // The felt is the centre element's own box — the area the four strips leave.
120+ // The felt is the centre element's own box — the area the four strips leave —
121+ // grown into whatever of those strips nobody is using yet.
121122 const centre = table.querySelector<HTMLElement>('.center');
122- const felt = centre ? local(centre.getBoundingClientRect()) : ringRect;
123+ const box = centre ? local(centre.getBoundingClientRect()) : ringRect;
124+ const felt = reaching(box, stripEdges(table, base, local));
123125
124126 return {
125127 size: { w: base.width, h: base.height },
⋯ 28 unchanged lines
154156 return { w, h: w * 1.375 };
155157 }
156158
159+/**
160+ * The inside edge of each strip's contents, where any of them has been read.
161+ * Missing sides are ones there was nothing to measure.
162+ */
163+export interface StripEdges {
164+ left?: number;
165+ right?: number;
166+ top?: number;
167+ bottom?: number;
168+}
169+
170+/**
171+ * The felt, out to where each strip's tiles and buttons actually begin.
172+ *
173+ * `.center` is only what is left over once every strip has been given its full
174+ * `--depth`, and a strip is built from its outer edge inwards: the melds sit at
175+ * the inside end of it, and until somebody 吃s or 碰s there is nothing there. So
176+ * between the middle and the first thing you can see of a seat there is a band
177+ * of plain green — most of a hand, for most seats — and a tile that stopped at
178+ * `.center`'s edge stopped in the middle of it, against nothing. That band is
179+ * table, and this is what hands it back.
180+ *
181+ * Only ever outwards. The middle is never smaller than the box the layout left.
182+ */
183+export function reaching(centre: Rect, edges: StripEdges): Rect {
184+ const right = centre.x + centre.w;
185+ const bottom = centre.y + centre.h;
186+ const x = Math.min(centre.x, edges.left ?? centre.x);
187+ const y = Math.min(centre.y, edges.top ?? centre.y);
188+ return {
189+ x,
190+ y,
191+ w: Math.max(right, edges.right ?? right) - x,
192+ h: Math.max(bottom, edges.bottom ?? bottom) - y,
193+ };
194+}
195+
196+/**
197+ * Read those edges off the four strips. A child holding nothing — the melds row
198+ * before a claim, the button bar with no buttons up — is space nobody is using,
199+ * so what counts is the first child with something in it.
200+ */
201+function stripEdges(table: HTMLElement, base: DOMRect, local: (r: DOMRect) => Rect): StripEdges {
202+ const edges: StripEdges = {};
203+ for (const slot of table.querySelectorAll<HTMLElement>('.slot[data-seat]')) {
204+ const seat = slot.querySelector<HTMLElement>('.seat');
205+ if (!seat) continue;
206+
207+ let used: Rect | null = null;
208+ for (const child of seat.children) {
209+ // The winner's arrow points into the middle from the strip's inside edge
210+ // and is gone again next hand; it is not what a seat takes up.
211+ if (child.classList.contains('win-arrow')) continue;
212+ if (child.childElementCount === 0 && !child.textContent?.trim()) continue;
213+ const r = local(child.getBoundingClientRect());
214+ if (r.w === 0 || r.h === 0) continue;
215+ used = used
216+ ? {
217+ x: Math.min(used.x, r.x),
218+ y: Math.min(used.y, r.y),
219+ w: Math.max(used.x + used.w, r.x + r.w) - Math.min(used.x, r.x),
220+ h: Math.max(used.y + used.h, r.y + r.h) - Math.min(used.y, r.y),
221+ }
222+ : r;
223+ }
224+ if (!used) continue;
225+
226+ // Which edge of the table this strip is against. The left and right ones
227+ // are the only two that touch the sides, so they are what is asked first.
228+ const box = local(slot.getBoundingClientRect());
229+ if (box.x < 1) edges.left = Math.max(edges.left ?? 0, used.x + used.w);
230+ else if (box.x + box.w > base.width - 1) {
231+ edges.right = Math.min(edges.right ?? base.width, used.x);
232+ } else if (box.y < 1) edges.top = Math.max(edges.top ?? 0, used.y + used.h);
233+ else edges.bottom = Math.min(edges.bottom ?? base.height, used.y);
234+ }
235+ return edges;
236+}
237+
157238 export const centreOf = (r: Rect): Point => ({ x: r.x + r.w / 2, y: r.y + r.h / 2 });
158239
159240 /**
⋯ 134 unchanged lines
modifiedsrc/table/physics.test.ts+29 −1
⋯ 24 unchanged lines
2525 type Rect,
2626 type Spawn,
2727 } from './physics';
28-import { canThrow, segmentHitsRect, type TableGeometry } from './geometry';
28+import { canThrow, reaching, segmentHitsRect, type TableGeometry } from './geometry';
2929
3030 const POOL: Rect = { x: 200, y: 200, w: 220, h: 220 };
3131 /** Same aspect as a real tile, roughly pool-tile sized. */
⋯ 470 unchanged lines
502502 });
503503 });
504504
505+describe('the felt reaching into the strips', () => {
506+ // `.center` is what is left once every strip has taken its full depth; the
507+ // inside end of a strip is empty until somebody claims something.
508+ const CENTRE: Rect = { x: 300, y: 300, w: 400, h: 400 };
509+
510+ it('grows out to where a strip that has laid nothing down actually starts', () => {
511+ const felt = reaching(CENTRE, { left: 260, right: 740, top: 250, bottom: 750 });
512+ expect(felt).toEqual({ x: 260, y: 250, w: 480, h: 500 });
513+ });
514+
515+ it('leaves a side alone once that seat has tiles down against the middle', () => {
516+ // Melds out to the centre's own edge: nothing to give back on that side.
517+ const felt = reaching(CENTRE, { left: 300, right: 740 });
518+ expect(felt.x).toBe(300);
519+ expect(felt.x + felt.w).toBe(740);
520+ });
521+
522+ it('never shrinks the middle, whatever a strip claims to reach', () => {
523+ // A strip whose contents overhang the centre would otherwise invert it.
524+ const felt = reaching(CENTRE, { left: 340, top: 360, right: 660, bottom: 640 });
525+ expect(felt).toEqual(CENTRE);
526+ });
527+
528+ it('is the centre itself when there is nothing to measure', () => {
529+ expect(reaching(CENTRE, {})).toEqual(CENTRE);
530+ });
531+});
532+
505533 describe('segmentHitsRect', () => {
506534 const r: Rect = { x: 10, y: 10, w: 20, h: 20 };
507535
⋯ 164 unchanged lines