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 | |||
| 14 | 14 | import { | |
| 15 | 15 | addBody, | |
| 16 | 16 | createWorld, | |
| 17 | + | reshape, | |
| 17 | 18 | faceOn, | |
| 18 | 19 | FIXED_DT, | |
| 19 | 20 | liftFor, | |
| ⋯ 448 unchanged lines | |||
| 468 | 469 | const nearSide = (): Rect[] => | |
| 469 | 470 | Array.from({ length: 18 }, (_, i) => ({ x: 180 + i * 14, y: 440, w: 14, h: 36 })); | |
| 470 | 471 | ||
| 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 | + | ||
| 471 | 498 | describe('the throw gate', () => { | |
| 472 | 499 | it('is shut while the wall in front of the seat is standing', () => { | |
| 473 | 500 | expect(canThrow(geometry(nearSide()), 0)).toBe(false); | |
| ⋯ 251 unchanged lines | |||
modifiedsrc/table/physics.ts+20 −0
| ⋯ 619 unchanged lines | |||
| 620 | 620 | clampInto(bounds, b); | |
| 621 | 621 | if (b.x !== x || b.y !== y) wake(b); | |
| 622 | 622 | } | |
| 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 | + | } | |
| 623 | 643 | } | |
| 624 | 644 | ||
| 625 | 645 | function damp(b: Body, dt: number) { | |
| ⋯ 372 unchanged lines | |||