collin/mahjong · fba98f79
The corners meet because there is no sum left to get wrong
Collin Richards · 2026-08-21 20:58 UTC · fba98f792458ec509e9725818781f07a39ae3f1e · parent 6e2784f0 · browse files
modifiedREADME.md+8 −4
| ⋯ 572 unchanged lines | |||
| 573 | 573 | back off the element, so the colliders turn with it, and `WallRing` reads it too | |
| 574 | 574 | so the square is built smaller by however much the turn costs it (stood on its | |
| 575 | 575 | corner a square needs its diagonal where it used to need its side). Set it to | |
| 576 | - | zero and everything squares up again. `--wall-lap` is how far each wall runs past its own corner, in wall-depths — | |
| 577 | - | that overlap is the whole of what makes four straight walls a pinwheel, and at 0 | |
| 578 | - | they merely meet. `--wall-room`, `--wall-x` and `--wall-y` put it where you want | |
| 579 | - | it, and `npm run dev` serves **`/wall.html`** for turning | |
| 576 | + | zero and everything squares up again. The lap — how far a wall runs past its own corner, the overlap that makes four | |
| 577 | + | straight walls a pinwheel — is not a number anybody gets to choose. For four | |
| 578 | + | equal walls to close it has to be exactly one wall-depth, and any other value is | |
| 579 | + | either a gap or an overshoot. So it isn't one: `--wall-len` is a side's length, | |
| 580 | + | eighteen stacks exactly, set from `WallRing` alongside `--ws`; the opening is | |
| 581 | + | that less one depth; and each wall is pinned at the corner it is built from. The | |
| 582 | + | corners meet because there is no arithmetic left for them to disagree about. | |
| 583 | + | `--wall-room`, `--wall-x` and `--wall-y` put it where you want it, and `npm run dev` serves **`/wall.html`** for turning | |
| 580 | 584 | all four by hand — the real `WallRing` in a real `.center`, with every collider | |
| 581 | 585 | the physics would be handed drawn over the top, because "it looks right" and "a | |
| 582 | 586 | tile would bounce off it there" are two different claims. | |
| ⋯ 75 unchanged lines | |||
modifiedsrc/game/wall.ts+14 −20
| ⋯ 115 unchanged lines | |||
| 116 | 116 | * `ratio` is the stack's long side over its short one, measured off the element | |
| 117 | 117 | * rather than restated here: it is CSS's business what shape a stack is. | |
| 118 | 118 | */ | |
| 119 | - | export function ringCell( | |
| 120 | - | space: { w: number; h: number }, | |
| 121 | - | ratio: number, | |
| 122 | - | lap = ratio, | |
| 123 | - | ): number { | |
| 124 | - | // Outer footprint: the opening, plus a wall hanging off each side of it. | |
| 125 | - | // opening = n*s - lap*s walls = 2 * ratio*s | |
| 126 | - | // outer = (n + 2*ratio - lap) * s | |
| 127 | - | return Math.max(1, Math.min(space.w, space.h) / (STACKS_PER_SIDE + 2 * ratio - lap)); | |
| 119 | + | export function ringCell(space: { w: number; h: number }, ratio: number): number { | |
| 120 | + | // A side is its eighteen stacks; the opening is that less one wall-depth, and | |
| 121 | + | // a wall hangs off each side of the opening. So across the middle: | |
| 122 | + | // opening = (n - ratio) * s walls = 2 * ratio * s | |
| 123 | + | // outer = (n + ratio) * s | |
| 124 | + | return Math.max(1, Math.min(space.w, space.h) / (STACKS_PER_SIDE + ratio)); | |
| 128 | 125 | } | |
| 129 | 126 | ||
| 130 | 127 | /** | |
| 131 | 128 | * The opening the square leaves in the middle — where the discards go. | |
| 132 | 129 | * | |
| 133 | - | * A side holds its stacks and then runs `lap` past its own corner: that overlap | |
| 134 | - | * is the whole of what makes four straight walls a pinwheel, and it is what the | |
| 135 | - | * opening is short by. One wall-depth is the square everyone knows; less draws | |
| 136 | - | * the arms in, more throws them out. | |
| 130 | + | * A side holds its stacks and then runs one wall-depth past its own corner — | |
| 131 | + | * over the outside of the next wall along, which is the pinwheel — so what the | |
| 132 | + | * opening is short by is exactly that depth. It is not a free number: any other | |
| 133 | + | * value leaves the four walls either short of each other or thrown out past | |
| 134 | + | * each other, and both of those are the gap. | |
| 137 | 135 | */ | |
| 138 | - | export const ringOpening = ( | |
| 139 | - | cap: RingCapacity, | |
| 140 | - | cell: { s: number; d: number }, | |
| 141 | - | lap = cell.d, | |
| 142 | - | ) => ({ | |
| 143 | - | w: Math.max(cell.s, cap.h * cell.s - lap), | |
| 144 | - | h: Math.max(cell.s, cap.v * cell.s - lap), | |
| 136 | + | export const ringOpening = (cap: RingCapacity, cell: { s: number; d: number }) => ({ | |
| 137 | + | w: Math.max(cell.s, cap.h * cell.s - cell.d), | |
| 138 | + | h: Math.max(cell.s, cap.v * cell.s - cell.d), | |
| 145 | 139 | }); | |
| 146 | 140 | ||
| 147 | 141 | /** A stack and which of the 72 it is — the index is what the physics reads. */ | |
| ⋯ 51 unchanged lines | |||
modifiedsrc/styles.css+15 −10
| ⋯ 675 unchanged lines | |||
| 676 | 676 | element, so the colliders turn with it and nothing has to be kept in step | |
| 677 | 677 | by hand. Zero squares it up again. */ | |
| 678 | 678 | --wall-turn: 45deg; | |
| 679 | - | /* How far a wall runs past its own corner, in wall-depths. That overlap is | |
| 680 | - | the whole of what makes four straight walls a pinwheel: at 1 it is the | |
| 681 | - | square everyone knows, at 0 the four of them just meet, and above 1 the | |
| 682 | - | arms are thrown out past each other. */ | |
| 683 | - | --wall-lap: 1; | |
| 684 | - | --lap: calc(var(--wd) * var(--wall-lap)); | |
| 679 | + | /* A side's length, set from ui/WallRing: eighteen stacks, exactly. The lap — | |
| 680 | + | how far a wall runs past its own corner, the overlap that makes four | |
| 681 | + | straight walls a pinwheel — is not a number anybody gets to choose. For | |
| 682 | + | four equal walls to close it has to be one wall-depth, no more and no less, | |
| 683 | + | and any other value is a gap or an overshoot. So it is not set here: the | |
| 684 | + | side is its stacks, the opening is the side less one depth, and the corners | |
| 685 | + | meet because there is no arithmetic left for them to disagree about. */ | |
| 686 | + | --wall-len: 0px; | |
| 685 | 687 | /* How much of the room it has the square actually takes, and where its middle | |
| 686 | 688 | sits relative to the middle of the table. All three are turned by hand in | |
| 687 | 689 | `/wall.html` and pasted back here. */ | |
| ⋯ 28 unchanged lines | |||
| 716 | 718 | display: flex; | |
| 717 | 719 | justify-content: flex-end; | |
| 718 | 720 | } | |
| 721 | + | /* Each wall is pinned at the corner it is built from and runs its own length | |
| 722 | + | from there, which carries it one depth past the far one — over the outside of | |
| 723 | + | the next wall along, which is the pinwheel. Every side the same way round. */ | |
| 719 | 724 | .wall-side.top { | |
| 720 | 725 | top: calc(-1 * var(--wd)); | |
| 721 | 726 | left: 0; | |
| 722 | - | right: calc(-1 * var(--lap)); | |
| 727 | + | width: var(--wall-len); | |
| 723 | 728 | height: var(--wd); | |
| 724 | 729 | } | |
| 725 | 730 | .wall-side.right { | |
| 726 | 731 | right: calc(-1 * var(--wd)); | |
| 727 | 732 | top: 0; | |
| 728 | - | bottom: calc(-1 * var(--lap)); | |
| 733 | + | height: var(--wall-len); | |
| 729 | 734 | width: var(--wd); | |
| 730 | 735 | flex-direction: column; | |
| 731 | 736 | } | |
| 732 | 737 | .wall-side.bottom { | |
| 733 | 738 | bottom: calc(-1 * var(--wd)); | |
| 734 | 739 | right: 0; | |
| 735 | - | left: calc(-1 * var(--lap)); | |
| 740 | + | width: var(--wall-len); | |
| 736 | 741 | height: var(--wd); | |
| 737 | 742 | flex-direction: row-reverse; | |
| 738 | 743 | } | |
| 739 | 744 | .wall-side.left { | |
| 740 | 745 | left: calc(-1 * var(--wd)); | |
| 741 | 746 | bottom: 0; | |
| 742 | - | top: calc(-1 * var(--lap)); | |
| 747 | + | height: var(--wall-len); | |
| 743 | 748 | width: var(--wd); | |
| 744 | 749 | flex-direction: column-reverse; | |
| 745 | 750 | } | |
| ⋯ 1573 unchanged lines | |||
modifiedsrc/tuneWall.tsx+2 −5
| ⋯ 31 unchanged lines | |||
| 32 | 32 | interface Placing { | |
| 33 | 33 | turn: number; | |
| 34 | 34 | room: number; | |
| 35 | - | lap: number; | |
| 36 | 35 | x: number; | |
| 37 | 36 | y: number; | |
| 38 | 37 | } | |
| 39 | 38 | ||
| 40 | - | const START: Placing = { turn: 45, room: 0.95, lap: 1, x: 0, y: 0 }; | |
| 39 | + | const START: Placing = { turn: 45, room: 0.95, x: 0, y: 0 }; | |
| 41 | 40 | ||
| 42 | 41 | const KNOBS: Knob[] = [ | |
| 43 | 42 | { key: 'turn', label: 'Turn', min: 0, max: 90, step: 1, note: '° clockwise', unit: 'deg' }, | |
| 44 | 43 | { key: 'room', label: 'Room it takes', min: 0.3, max: 1.3, step: 0.01, note: 'of the middle', unit: '' }, | |
| 45 | - | { key: 'lap', label: 'Pinwheel lap', min: 0, max: 2, step: 0.05, note: 'wall-depths past the corner', unit: '' }, | |
| 46 | 44 | { key: 'x', label: 'Across', min: -200, max: 200, step: 1, note: 'from the middle', unit: 'px' }, | |
| 47 | 45 | { key: 'y', label: 'Down', min: -200, max: 200, step: 1, note: 'from the middle', unit: 'px' }, | |
| 48 | 46 | ]; | |
| ⋯ 25 unchanged lines | |||
| 74 | 72 | if (!ring) return; | |
| 75 | 73 | ring.style.setProperty('--wall-turn', `${place.turn}deg`); | |
| 76 | 74 | ring.style.setProperty('--wall-room', `${place.room}`); | |
| 77 | - | ring.style.setProperty('--wall-lap', `${place.lap}`); | |
| 78 | 75 | ring.style.setProperty('--wall-x', `${place.x}px`); | |
| 79 | 76 | ring.style.setProperty('--wall-y', `${place.y}px`); | |
| 80 | 77 | }, [place]); | |
| ⋯ 66 unchanged lines | |||
| 147 | 144 | }; | |
| 148 | 145 | }, [place, drawn, showColliders, state]); | |
| 149 | 146 | ||
| 150 | - | const css = `.wall-ring {\n --wall-turn: ${place.turn}deg;\n --wall-room: ${place.room};\n --wall-lap: ${place.lap};\n --wall-x: ${place.x}px;\n --wall-y: ${place.y}px;\n}`; | |
| 147 | + | const css = `.wall-ring {\n --wall-turn: ${place.turn}deg;\n --wall-room: ${place.room};\n --wall-x: ${place.x}px;\n --wall-y: ${place.y}px;\n}`; | |
| 151 | 148 | ||
| 152 | 149 | return ( | |
| 153 | 150 | <div className="tune"> | |
| ⋯ 84 unchanged lines | |||
modifiedsrc/ui/WallRing.tsx+9 −6
| ⋯ 41 unchanged lines | |||
| 42 | 42 | opening | |
| 43 | 43 | ? // `--ws` is set from here rather than from the tile, because the | |
| 44 | 44 | // square is the fixed thing now and the tile is what gives. | |
| 45 | - | ({ width: opening.w, height: opening.h, '--ws': `${cell}px` } as React.CSSProperties) | |
| 45 | + | ({ | |
| 46 | + | width: opening.w, | |
| 47 | + | height: opening.h, | |
| 48 | + | '--ws': `${cell}px`, | |
| 49 | + | // A side is its stacks and nothing else, so there is no sum for | |
| 50 | + | // the corners to come out of by a pixel. | |
| 51 | + | '--wall-len': `${RING.h * cell}px`, | |
| 52 | + | } as React.CSSProperties) | |
| 46 | 53 | : undefined | |
| 47 | 54 | } | |
| 48 | 55 | > | |
| ⋯ 58 unchanged lines | |||
| 107 | 114 | const turn = (parseFloat(style.getPropertyValue('--wall-turn')) || 0) * (Math.PI / 180); | |
| 108 | 115 | const spread = Math.abs(Math.cos(turn)) + Math.abs(Math.sin(turn)); | |
| 109 | 116 | const room = parseFloat(style.getPropertyValue('--wall-room')) || 1; | |
| 110 | - | // How far a wall runs past its corner, as a share of a stack's short side. | |
| 111 | - | const lapOf = parseFloat(style.getPropertyValue('--wall-lap')); | |
| 112 | - | const lap = ratio * (Number.isFinite(lapOf) ? lapOf : 1); | |
| 113 | 117 | // Eighteen stacks and one depth of overlap is `(18 + ratio) * s` across, | |
| 114 | 118 | // and turned that covers `spread` times as much — so the room it has to | |
| 115 | 119 | // be solved in is the middle divided by the turn. | |
| ⋯ 3 unchanged lines | |||
| 119 | 123 | h: (middle.clientHeight * room) / spread, | |
| 120 | 124 | }, | |
| 121 | 125 | ratio, | |
| 122 | - | lap, | |
| 123 | 126 | ); | |
| 124 | 127 | // The square is always eighteen a side; what fits it is the tile. | |
| 125 | 128 | const cell = { s, d: s * ratio }; | |
| 126 | - | const opening = ringOpening(RING, cell, s * lap); | |
| 129 | + | const opening = ringOpening(RING, cell); | |
| 127 | 130 | setSquare((was) => | |
| 128 | 131 | was.cell === s && was.opening?.w === opening.w && was.opening?.h === opening.h | |
| 129 | 132 | ? was | |
| ⋯ 15 unchanged lines | |||