diff --git a/packages/site/assets/scripts/runtime/2dmahjong.tsx b/packages/site/assets/scripts/runtime/2dmahjong.tsx new file mode 100644 index 0000000..e8d764b --- /dev/null +++ b/packages/site/assets/scripts/runtime/2dmahjong.tsx @@ -0,0 +1,179 @@ +import { jsxAsElement as jsx } from 'web-shared/src/lib/jsx'; + +// prettier-ignore +type Hand = [ + /* Red 5 1 2 3 4 5 6 7 8 9 */ + /* S */ [ T, T, T, T, T, T, T, T, T, T ], + /* P */ [ T, T, T, T, T, T, T, T, T, T ], + /* M */ [ T, T, T, T, T, T, T, T, T, T ], + /* Z */ [ null, T, T, T, T, T, T, T], + /* E S W N W G R */ +]; + +const svgLookup: Hand = [ + [ + '/tiles/Sou5-Dora.svg', + '/tiles/Sou1.svg', + '/tiles/Sou2.svg', + '/tiles/Sou3.svg', + '/tiles/Sou4.svg', + '/tiles/Sou5.svg', + '/tiles/Sou6.svg', + '/tiles/Sou7.svg', + '/tiles/Sou8.svg', + '/tiles/Sou9.svg', + ], + [ + '/tiles/Pin5-Dora.svg', + '/tiles/Pin1.svg', + '/tiles/Pin2.svg', + '/tiles/Pin3.svg', + '/tiles/Pin4.svg', + '/tiles/Pin5.svg', + '/tiles/Pin6.svg', + '/tiles/Pin7.svg', + '/tiles/Pin8.svg', + '/tiles/Pin9.svg', + ], + [ + '/tiles/Man5-Dora.svg', + '/tiles/Man1.svg', + '/tiles/Man2.svg', + '/tiles/Man3.svg', + '/tiles/Man4.svg', + '/tiles/Man5.svg', + '/tiles/Man6.svg', + '/tiles/Man7.svg', + '/tiles/Man8.svg', + '/tiles/Man9.svg', + ], + [ + null, + '/tiles/Ton.svg', + '/tiles/Nan.svg', + '/tiles/Shaa.svg', + '/tiles/Pei.svg', + '/tiles/Haku.svg', + '/tiles/Hatsu.svg', + '/tiles/Chun.svg', + ], +]; + +const buildBaseHand = (count: number = 0): Hand => [ + [count, count, count, count, count, count, count, count, count, count], + [count, count, count, count, count, count, count, count, count, count], + [count, count, count, count, count, count, count, count, count, count], + [null, count, count, count, count, count, count, count], +]; + +const tileRenderOrder = [1, 2, 3, 4, 0, 5, 6, 7, 8, 9]; + +const renderTile = ( + suitIndex: number, + tileIndex: number, + onClick: () => void, +): Node => ( +
+
+ + +
+
+); + +class MahjongGameState { + wall: Hand; + playerHand: Hand; + renderTarget: HTMLDivElement; + drawnTile: [number, number] | undefined; + done: boolean = false; + + constructor(renderTarget: HTMLDivElement) { + this.wall = buildBaseHand(4); + this.playerHand = buildBaseHand(0); + this.renderTarget = renderTarget; + + Array.from({ length: 13 }, () => this.addToHand(this.draw())); + this.drawnTile = this.draw(); + } + + draw(): [suit: number, tile: number] | undefined { + const tiles = this.wall.flatMap((suit, suitIndex) => + suit.flatMap((count, tileIndex) => + Array.from( + { length: count ?? 0 }, + () => [suitIndex, tileIndex] as [number, number], + ), + ), + ); + if (tiles.length === 0) return undefined; + + const [suitIndex, tileIndex] = + tiles[Math.floor(Math.random() * tiles.length)]; + + const wallSuit = this.wall[suitIndex]; + wallSuit[tileIndex] = (wallSuit[tileIndex] ?? 0) - 1; + + return [suitIndex, tileIndex]; + } + + private addToHand(tile?: [number, number]) { + if (!tile) return; + + const [suit, index] = tile; + const handSuit = this.playerHand[suit]; + handSuit[index] = (handSuit[index] ?? 0) + 1; + } + + private drawNext() { + this.drawnTile = this.draw(); + this.done = !this.drawnTile; + this.render(); + } + + private discardFromHand(suitIndex: number, tileIndex: number) { + const handSuit = this.playerHand[suitIndex]; + if (!handSuit[tileIndex]) return; + + handSuit[tileIndex] = (handSuit[tileIndex] ?? 0) - 1; + this.addToHand(this.drawnTile); + this.drawNext(); + } + + private discardDrawn() { + // The drawn tile was never added to the hand, so dropping it is just drawing again. + this.drawNext(); + } + + renderHand(hand: Hand) { + return hand.flatMap((suit, suitIndex) => + tileRenderOrder.flatMap((tileIndex) => { + const tileCount = suit[tileIndex]; + if (!tileCount) return []; // Filtered out by flatMap + + return Array.from({ length: tileCount }, () => + renderTile(suitIndex, tileIndex, () => + this.discardFromHand(suitIndex, tileIndex), + ), + ); + }), + ); + } + + render() { + this.renderTarget.replaceChildren( + ...this.renderHand(this.playerHand), + ...(this.drawnTile + ? [renderTile(...this.drawnTile, () => this.discardDrawn())] + : []), + ); + } +} + +document.addEventListener('DOMContentLoaded', () => { + const game = new MahjongGameState( + document.getElementsByClassName('2d-mahjong')[0] as HTMLDivElement, + ); + + game.render(); +}); diff --git a/packages/site/posts/games/mahjong/mahjong-2d-array.md b/packages/site/posts/games/mahjong/mahjong-2d-array.md new file mode 100644 index 0000000..323049e --- /dev/null +++ b/packages/site/posts/games/mahjong/mahjong-2d-array.md @@ -0,0 +1,27 @@ +--- +title: Mahjong as a 2D Array +summary: Better Data Storage and operations +scripts: runtime/2dmahjong.tsx +--- + +Messing with data structures and ways of storing a Mahjong hand. Bellow is interactive! Click (or tap) to discard :3c + +Ideally I'd also love to render your hand score as well, but that's for another time! + +
+
+ +
+ +Here's the data type used for storing a hand: + +```typescript +type Hand = [ + /* Red 5 1 2 3 4 5 6 7 8 9 */ + /* S */ [ T, T, T, T, T, T, T, T, T, T ], + /* P */ [ T, T, T, T, T, T, T, T, T, T ], + /* M */ [ T, T, T, T, T, T, T, T, T, T ], + /* Z */ [ null, T, T, T, T, T, T, T], + /* E S W N W G R */ +]; +```