anvilsign in

collin/mahjong

RenderedSource

1# 台灣麻將 — four-player hotseat on one touchscreen
2
3Sixteen-tile Taiwanese mahjong for four people sitting around a single laptop.
4All four hands are on screen at once, each rotated to face its own edge — put a
5piece of card or plastic over your strip so the others can't see your tiles.
6Every label is Chinese with English underneath; the tiles themselves stay
7Chinese, because that is what the tiles say.
8
9```
10npm install
11npm run dev # then open the browser full-screen (F11)
12npm test # rules-engine tests, incl. 200 randomly-played hands
13```
14
15## Screen layout
16
17```
18 ┌──── 對家 (rotated 180°) ────┐
19 上家 (rotated 90°) │ wall / discards │ 下家 (rotated −90°)
20 └──── 自家 (upright, near you) ┘
21```
22
23Seat 0 is the bottom edge, and play runs 0 → 1 (right) → 2 (top) → 3 (left),
24which is the normal counter-clockwise 東南西北 order.
25
26Each strip shows, from the player's edge inwards: nameplate (seat wind, 莊
27marker, 聽 badge, chip count) → concealed hand → action buttons → melds and
28flowers → that player's discard row.
29
30## Playing
31
32- **The tile you just drew** is held apart from the sorted hand with a gold ring
33 and a 摸 label, instead of being sorted invisibly into it. It merges into the
34 hand once you discard. 補花 replacements and kong replacements are marked the
35 same way.
36- **The wall** is drawn in the centre as a square: four staggered sides of 18
37 stacks of two, the way it's built on a real table. The gold stack is the break
38 point you draw from, stacks shrink to a single tile and then vanish as they're
39 used, and the dimmed tail is the 16-tile 底牌 that ends the hand — which is
40 also the end kong and flower replacements are taken from, so the square is
41 eaten from both directions at once.
42- **Discard** — tap a tile to lift it, tap again (or the 打出 button) to throw it.
43 Action buttons sit on the right of your own strip, within thumb reach.
44- **Arranging** — drag any tile in your hand to reorder it; 理牌 sorts it back
45 into suit order. Hands are never auto-sorted after the deal, so an arrangement
46 you set up survives draws, claims and kongs.
47- **Rules** — the `?` on your nameplate opens a bilingual rules panel anchored to
48 your own edge of the table and rotated to face you.
49- **Claiming** — after a discard, every seat that *can* claim gets 胡 / 槓 / 碰 /
50 吃 / 過 buttons on their own strip, and they resolve by priority (胡 > 槓/碰 >
51 吃; ties go to the player nearest the discarder in turn order).
52 **The table never blocks on a claim.** The next player gets a 摸牌 button and
53 may draw whenever they like, which shuts the window on anyone who hasn't
54 called — exactly like shouting 碰 before the next player picks up. A claim
55 already declared still stands, so calling in time always wins the tile. If
56 everyone answers first, play advances on its own without the extra tap.
57- **On your turn** — 自摸, 暗槓 and 加槓 appear automatically when legal.
58 加槓 offers everyone else a 搶槓 chance.
59- **Undo** — a 復原 button in the middle of the table takes back the last
60 action, naming what it will undo (`復原 玩家 2 打 五萬`). It sits in the
61 centre rather than on a seat because whoever *spots* the mis-tap should be
62 able to reach it. Twenty actions deep, cleared when the next hand is dealt.
63 Snapshots live outside the saved state, so an undo does not survive a
64 refresh: what you can take back is what happened while everyone was still
65 watching it happen.
66- **Sound** — off by default. A dry clack as a tile goes down, and a distinct
67 two-note chime when a claim window opens, which is how a slow player notices
68 their 碰 is available before the next player draws it shut. Everything is
69 synthesised with a few oscillators (`src/game/sound.ts`) rather than sampled,
70 so there is nothing to load and the cue lands on the same frame as the tile.
71 Toggle it from the 🔊 button next to 復原, or from the settings screen.
72- **Settings** — 設定 from the lobby, or from the game-over panel: player names,
73 底 / 台 / starting chips, the house-rule switches below, and sound. Kept in
74 `localStorage` separately from the save, so they carry over to the next game.
75 Stakes are locked once a game is under way — they'd otherwise rewrite chips
76 already won.
77
78## Saving
79
80The game state is plain data, so a save is just its JSON in `localStorage`,
81rewritten after every move. Close the lid, refresh, or run the battery flat and
82nothing is lost — the lobby offers **繼續對局 Resume** alongside **開新局 New
83game**, showing the round, hand number, chip counts and when it was saved. A
84save is validated before it is offered (four players, 144 tiles accounted for),
85so a truncated or hand-edited one is ignored rather than loaded into a broken
86table. `VERSION` in `save.ts` retires old saves if the state shape changes.
87
88## Rules implemented
89
90- 144 tiles (four of each suit/honour, eight flowers), 16-tile hands, dealer
91 draws the 17th.
92- 補花 at the deal and on every drawn flower, replacements from the back of the
93 wall.
94- 吃 only from 上家; 碰/槓/胡 from anyone. 明槓, 暗槓, 加槓, 搶槓, 槓上開花.
95- 流局 when 16 tiles remain (`Rules.wallReserve`); the dealer keeps the deal on a
96 draw or on a dealer win (連莊), otherwise the deal passes and the round wind
97 advances every four passes. A full 四圈 game is 16 dealer passes.
98
99### House rules (switches in `Rules`, all on the settings screen)
100
101- **過水 `sacredDiscard`** (on) — pass on a tile you could have won with and it
102 is dead to you until your own next draw; the seat shows a 過水 badge listing
103 what it is locked out of, so the missing 胡 button is never a mystery. A draw
104 from either end of the wall lifts it. **`sacredClearedByClaim`** (off) decides
105 whether taking a 吃 / 碰 / 槓 lifts it too — tables genuinely differ.
106- **一炮多響 `multipleWinners`** (off) — one discard pays out to every seat that
107 calls on it, each settled separately against the discarder. With it off the
108 tile goes to the caller nearest the discarder. Either way the table now waits
109 for other seats that *can* win before settling, so the nearest seat wins the
110 tile rather than the quickest hand — and the 摸牌 button still closes the
111 window on anyone dithering.
112- **包牌 `liability`** (on) — feeding the pung that completes a visible 大三元
113 or 大四喜 makes the feeder answer for the whole hand, in place of all three
114 payers. Only the seat that fed the *last* of those sets is on the hook, and
115 only if it came off a discard: a hand that assembled them itself, or closed
116 the set with a 暗槓, has nobody to blame.
117
118### 台 scoring (`src/game/tai.ts`)
119
120自摸 1 · 門清 1 · 門清自摸 +1 · 全求人 2 · 平胡 2 · 五門齊 2 · 正花 1 each ·
121花槓 2 · 八仙過海 8 · 圈風 / 門風 1 each · 三元牌 1 each · 小三元 4 · 大三元 8 ·
122小四喜 8 · 大四喜 16 · 碰碰胡 4 · 混一色 4 · 清一色 8 · 字一色 16 ·
123三暗刻 2 / 四暗刻 5 / 五暗刻 8 · 獨聽 · 單釣 1 · 搶槓 1 · 槓上開花 1 ·
124海底撈月 1 · 河底撈魚 1 · 天胡 16 · 地胡 16 · 人胡 8 · 莊家 1 (連N拉N → 2N+1)
125
126Ambiguous hands are decomposed every legal way and scored at the best reading.
127
128**Payment** (`Game.settle`): one unit is `底 + 台 × 台值`
129(`DEFAULT_RULES` = 底 3, 台 1, 100 chips each).
130放槍一家付 — the discarder alone pays one unit; on 自摸 all three pay one unit
131each. 拉莊 is billed to the dealer alone when the dealer is a payer, and added
132to the whole hand when the dealer wins.
133
134House rules vary a lot; the tai table and payments are plain data/functions in
135`tai.ts` and `types.ts` if yours differ.
136
137## Tile art
138
139`public/tiles/tiles.svg` is the **postmodern** tileset from
140[gnome-mahjongg](https://gitlab.gnome.org/GNOME/gnome-mahjongg), extracted from
141the installed binary's GResource — a 43×2 sprite sheet (second row is the
142highlighted variant, used for a lifted tile). `public/tiles/back.png` is the
143blank tile from its **smooth** theme, tinted jade, since a solitaire game has no
144face-down art of its own.
145
146That art is **GPL-2.0-or-later**. Fine for playing at home; if you ever
147distribute this app, either honour the GPL or swap the two files for art of your
148own — `SPRITE_COL` in `tiles.ts` is the only mapping that would need updating.
149
150Known gaps and house rules that aren't implemented yet are listed in
151[TODO.md](TODO.md).
152
153## Layout
154
155```
156public/tiles/ sprite sheet + tile back
157src/game/tiles.ts tile codes, wall, shuffle, sprite mapping
158src/game/hu.ts hand decomposition, 聽 detection, wait shapes
159src/game/tai.ts 台 scoring
160src/game/engine.ts state machine: deal, turns, claim resolution, settlement
161src/ui/ TileView, Hand (drag), Seat (rotated strip), Center, Help
162```