| 1 | import type { SeatId } from '../game/types'; |
| 2 | import { BiEm } from './Bi'; |
| 3 | import { useLang, useT } from './lang'; |
| 4 | import { SEAT_ROT, SEAT_SLOT } from './rotation'; |
| 5 | |
| 6 | interface 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. */ |
| 13 | const 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 | */ |
| 127 | export 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 | } |