anvilsign in

collin/mahjong

1import type { SeatId } from '../game/types';
2import { BiEm } from './Bi';
3import { useLang, useT } from './lang';
4import { SEAT_ROT, SEAT_SLOT } from './rotation';
5
6interface Section {
7 zh: string;
8 en: string;
9 body: [string, string][];
10}
11
12/** Rules reference, shown inside one player's own strip so only they read it. */
13const SECTIONS: Section[] = [
14 {
15 zh: '目標',
16 en: 'The goal',
17 body: [
18 ['十六張牌,湊成五組加一對眼。', 'Build five sets plus one pair from 16 tiles.'],
19 ['一組=順子(三張同花色連號)或刻子(三張相同)。', 'A set is a run of three in one suit, or three of a kind.'],
20 ['摸進第十七張後打出一張。', 'You draw a 17th tile each turn, then discard one.'],
21 ],
22 },
23 {
24 zh: '牌',
25 en: 'The tiles',
26 body: [
27 ['萬 · 條 · 筒 一到九,各四張。', 'Characters, bamboo and dots, 1–9, four of each.'],
28 ['字牌:東南西北 中發白,各四張。', 'Honours: four winds and three dragons, four of each.'],
29 ['花牌八張,摸到自動補牌,還能算台。', 'Eight flowers: drawn ones replace themselves and score.'],
30 ],
31 },
32 {
33 zh: '輪到你',
34 en: 'Your turn',
35 body: [
36 ['點一張牌抬起,再點一次打出。', 'Tap a tile to lift it, tap again to discard it.'],
37 ['剛摸的牌會分開放在最右邊,標著「摸」。', 'The tile you just drew sits apart on the right, marked 摸 / Drew.'],
38 ['牌可以拖曳重排,或按「理牌」自動排序。', 'Drag tiles to rearrange, or press 理牌 / Sort to sort.'],
39 ],
40 },
41 {
42 zh: '吃碰槓',
43 en: 'Claiming a discard',
44 body: [
45 ['碰:手上兩張相同,可碰任何人打的牌。', 'Pung: two matching tiles, from anyone’s discard.'],
46 ['吃:只能吃上家(左手邊)打的牌。', 'Chow: a run, and only from the player to your left.'],
47 ['優先序:胡 > 碰/槓 > 吃。', 'Priority: win beats pung/kong, which beat chow.'],
48 ['吃碰之後不用摸牌,直接打一張。', 'After claiming you do not draw — just discard.'],
49 [
50 '要碰要快:下家一摸牌就來不及了,桌子不會等你。',
51 'Call fast — the window shuts when the next player draws. Nobody waits.',
52 ],
53 ],
54 },
55 {
56 zh: '槓',
57 en: 'Kongs',
58 body: [
59 ['四張相同:明槓、暗槓、或碰後加槓。', 'Four of a kind, claimed, concealed, or added to a pung.'],
60 ['槓完從牌尾補一張,胡了算「槓上開花」。', 'You draw from the tail; winning on it scores 槓上開花 — a win off a kong.'],
61 ['別人加槓時你剛好能胡,可以搶槓。', 'You may rob an added kong if that tile completes your hand.'],
62 ],
63 },
64 {
65 zh: '胡牌',
66 en: 'Winning',
67 body: [
68 ['自摸:自己摸到,三家各付一份。', 'Self-draw: all three opponents pay you.'],
69 ['放槍:胡別人打的牌,放槍者一家付。', 'Off a discard: the discarder alone pays.'],
70 ['一份 = 底 + 台數 × 台值。', 'One share is the base plus your tai count.'],
71 ['牌牆剩十六張還沒人胡就流局。', 'If 16 tiles remain undrawn, the hand is a draw.'],
72 ],
73 },
74 {
75 zh: '常見台數',
76 en: 'Common tai',
77 body: [
78 ['自摸 1 · 門清 1 · 門清自摸 +1', 'Self-draw 1 · concealed 1 · both together +1'],
79 ['正花每張 1 · 圈風 1 · 門風 1 · 三元牌每組 1', 'Own flower 1 · round wind 1 · seat wind 1 · dragon pung 1'],
80 ['碰碰胡 4 · 混一色 4 · 清一色 8 · 字一色 16', 'All pungs 4 · half flush 4 · full flush 8 · all honours 16'],
81 ['大三元 8 · 小四喜 8 · 大四喜 16', 'Big three dragons 8 · little four winds 8 · big four winds 16'],
82 ['莊家 1 台,連莊每次再加 2 台。', 'Dealer scores 1, plus 2 more for each repeat deal.'],
83 ],
84 },
85 {
86 zh: '房規',
87 en: 'House rules',
88 body: [
89 [
90 '過水:不胡的牌,到自己再摸牌前都不能胡。',
91 'Sacred discard: pass on a winning tile and it is dead to you until your next draw.',
92 ],
93 [
94 '一炮多響:一張放槍牌,幾家胡就付幾家。',
95 'Multiple winners: one discard can pay out to every seat that calls on it.',
96 ],
97 [
98 '包牌:放第三張三元/四喜刻子的人,一人付全部。',
99 'Liability: feed the third dragon or wind pung to a big hand and you pay for it alone.',
100 ],
101 ['以上都可以在設定裡開關。', 'All three are switches on the settings screen.'],
102 ],
103 },
104 {
105 zh: '拿錯了',
106 en: 'Mis-taps',
107 body: [
108 ['桌子中央有「復原」,可以退回上一步。', 'The 復原 / Undo button in the middle takes back the last action.'],
109 ['誰看到誰按,不限於按錯的那個人。', 'Anyone can press it — not just whoever made the mistake.'],
110 ],
111 },
112 {
113 zh: '莊家',
114 en: 'The dealer',
115 body: [
116 ['莊家胡牌或流局就連莊。', 'The dealer keeps the deal on a win or a draw.'],
117 ['否則換下家做莊,四家輪完換一圈風。', 'Otherwise it passes on; the round wind turns every four passes.'],
118 ],
119 },
120];
121
122/**
123 * Rules panel for one seat. Rendered at table level rather than inside the
124 * seat strip so it can be deep enough to read, but still rotated to face only
125 * that player.
126 */
127export function Help({
128 seat,
129 compact,
130 onClose,
131}: {
132 seat: SeatId;
133 compact?: boolean;
134 onClose: () => void;
135}) {
136 const t = useT();
137 const lang = useLang();
138 /* Stacked rather than side by side, and the <em> is the second of the two —
139 so on its own a line is a <span>, never the dimmed one. */
140 const line = (zh: string, en: string) =>
141 lang === 'bi' ? (
142 <>
143 <span lang="zh-TW">{zh}</span>
144 <em lang="en">{en}</em>
145 </>
146 ) : (
147 <span>{lang === 'zh' ? zh : en}</span>
148 );
149
150 return (
151 <div className={`help-slot ${SEAT_SLOT[seat]}`} onClick={onClose}>
152 <div
153 className="help"
154 style={{
155 transform: compact
156 ? 'translate(-50%, -50%)'
157 : `translate(-50%, -50%) rotate(${SEAT_ROT[seat]}deg)`,
158 }}
159 onClick={(e) => e.stopPropagation()}
160 >
161 <div className="help-head">
162 <h3>{t('台灣麻將規則', 'How to play', ' · ')}</h3>
163 <button className="btn ghost" onClick={onClose}>
164 {t('關閉', 'Close')}
165 </button>
166 </div>
167 <div className="help-body">
168 {SECTIONS.map((sec) => (
169 <section key={sec.zh}>
170 <h4>
171 <BiEm zh={sec.zh} en={sec.en} />
172 </h4>
173 {sec.body.map(([zh, en], i) => (
174 <p key={i}>{line(zh, en)}</p>
175 ))}
176 </section>
177 ))}
178 <section className="credit">
179 <h4>
180 <BiEm zh="牌面" en="Tile art" />
181 </h4>
182 <p>
183 {line(
184 'gnome-mahjongg「postmodern」牌組',
185 'From gnome-mahjongg’s postmodern theme, GPL-2.0-or-later.',
186 )}
187 </p>
188 </section>
189 </div>
190 </div>
191 </div>
192 );
193}