anvilsign in

collin/mahjong · 9ebe1624

A wall pushed across the table takes the discards with it

Collin Richards · 2026-08-21 22:01 UTC · 9ebe162415dabb1218910bace4c3aee707d35807 · parent 2f7fb415 · browse files

modifiedsrc/table/physics.test.ts+27 −0
⋯ 13 unchanged lines
1414 import {
1515 addBody,
1616 createWorld,
17+ reshape,
1718 faceOn,
1819 FIXED_DT,
1920 liftFor,
⋯ 448 unchanged lines
468469 const nearSide = (): Rect[] =>
469470 Array.from({ length: 18 }, (_, i) => ({ x: 180 + i * 14, y: 440, w: 14, h: 36 }));
470471
472+describe('a wall pushed across the table', () => {
473+ // Its own patch of table, well clear of the pool the other tests share.
474+ const BOUNDS: Rect = { x: 0, y: 0, w: 800, h: 800 };
475+
476+ it('shoves the tiles lying in front of it, even asleep ones', () => {
477+ const wall = { rect: { x: 100, y: 380, w: 300, h: 40 } };
478+ const world = createWorld(BOUNDS, [wall]);
479+ const b = addBody(world, {
480+ tile: 0, seat: 0, x: 300, y: 460, vx: 0, vy: 0, z: 0, vz: 0,
481+ angle: 0, spin: 0, w: TILE.w, h: TILE.h, atRest: true,
482+ });
483+ // Let it go properly to sleep first — that is the case that used to fail.
484+ let n = 0;
485+ while (!settled(world) && n < 2000) { step(world); n++; }
486+ expect(b.resting).toBe(true);
487+ const before = b.y;
488+
489+ // Now push the wall down the table, over where it is lying.
490+ reshape(world, BOUNDS, [{ rect: { x: 100, y: 430, w: 300, h: 40 } }]);
491+ for (let i = 0; i < 400 && !settled(world); i++) step(world);
492+
493+ // Shoved along in front of it, not left standing inside it.
494+ expect(b.y).toBeGreaterThan(before + 20);
495+ });
496+});
497+
471498 describe('the throw gate', () => {
472499 it('is shut while the wall in front of the seat is standing', () => {
473500 expect(canThrow(geometry(nearSide()), 0)).toBe(false);
⋯ 251 unchanged lines
modifiedsrc/table/physics.ts+20 −0
⋯ 619 unchanged lines
620620 clampInto(bounds, b);
621621 if (b.x !== x || b.y !== y) wake(b);
622622 }
623+
624+ // A wall that has moved may be standing where a tile is lying. Anything it
625+ // has come to overlap is woken, and the solver shoves it clear over the next
626+ // few steps — which is a wall being pushed across a table taking the discards
627+ // along in front of it, and is the whole reason you would push one.
628+ //
629+ // Only what is awake gets looked at each step, so without this a tile that
630+ // had gone to sleep would sit inside the wall and stay there.
631+ for (const b of world.bodies) {
632+ if (b.z >= WALL_HEIGHT || b.carried) continue;
633+ for (const w of world.walls) {
634+ // Your own tiles are not solid to you until you are clear of them.
635+ if (w.seat === b.seat && !b.leftSeat) continue;
636+ const hit = overlapOf(boxOf(w.rect, w.angle), b);
637+ if (hit && hit.depth > CONTACT_SLOP) {
638+ wake(b);
639+ break;
640+ }
641+ }
642+ }
623643 }
624644
625645 function damp(b: Body, dt: number) {
⋯ 372 unchanged lines