anvilsign in

collin/mahjong

1import type { TableCtl } from '../game/ctl';
2import type { Prefs } from '../game/prefs';
3import type { SeatId } from '../game/types';
4import { labelOf } from '../game/tiles';
5import { Bi } from './Bi';
6import { FullscreenToggle } from './FullscreenToggle';
7import { SoundToggle } from './SoundToggle';
8import { TileView } from './TileView';
9import { SEAT_ROT } from './rotation';
10import { WallRing } from './WallRing';
11
12interface Props {
13 game: TableCtl;
14 prefs: Prefs;
15 /** Phone-sized table: the centre becomes a single bar between the seats. */
16 compact?: boolean;
17 /** The seat drawn at the bottom of this screen — 0 except online, where the
18 * table is turned so it is your own. */
19 viewSeat?: SeatId;
20 /** Online: the one person reading this screen, whatever the bots are doing. */
21 preferSeat?: SeatId | null;
22 onPrefs: (p: Prefs) => void;
23 onSettings: () => void;
24}
25
26export function Center({ game, prefs, compact, viewSeat = 0, preferSeat = null, onPrefs, onSettings }: Props) {
27 const s = game.state;
28 const r = s.result;
29 // Point the live readout at whoever can move the game along.
30 const pending = (Object.keys(s.options) as unknown as number[])
31 .map(Number)
32 .filter((k) => s.responses[k as SeatId] === undefined) as SeatId[];
33 // Against the computer there is only one person reading this, and they are
34 // sitting at the bottom of the screen.
35 const human = s.bots.indexOf(false);
36 const facing =
37 preferSeat ?? (human >= 0 && game.hasBots ? (human as SeatId) : game.nextDrawer() ?? s.turn);
38 // Deliberately not per-seat: whoever spots the mis-tap should be able to
39 // reach it, not just the player who made it.
40 const undoable = s.phase === 'gameEnd' ? null : game.undoable();
41
42 return (
43 <div className="center">
44 {/* The wall square wants the whole middle of the table, which a phone does
45 not have to give — there it shrinks to the one number it was telling
46 you anyway. */}
47 {s.phase !== 'gameEnd' &&
48 (compact ? (
49 <div className="wall-pill">
50 <b>{game.wallLeft}</b>
51 <em>剩牌 left</em>
52 </div>
53 ) : (
54 <WallRing state={s} />
55 ))}
56
57 <div
58 className="facing"
59 style={
60 compact
61 ? undefined
62 : { transform: `rotate(${SEAT_ROT[(((facing - viewSeat + 4) % 4) as SeatId)]}deg)` }
63 }
64 >
65 {/* The tile just thrown is out there in the middle, lit, where everyone
66 can see it — so saying it again here would only cover the pile. The
67 phone has no pile, and still needs telling. */}
68 {compact && s.lastDiscard && s.phase !== 'handEnd' && (
69 <div className="last-discard">
70 <div className="ld-label">{s.players[s.lastDiscard.from].name} 打 discarded</div>
71 <TileView tile={s.lastDiscard.tile} size="hand" highlight />
72 </div>
73 )}
74
75 {s.phase === 'claims' && (
76 <div className="prompt">
77 {pending.length} 家可吃碰 · {pending.length} may claim
78 </div>
79 )}
80 {s.phase === 'robkong' && <div className="prompt warn">搶槓機會 · Rob the kong?</div>}
81 </div>
82
83 {r && s.phase !== 'gameEnd' && (
84 <div className="result">
85 {r.drawGame ? (
86 <>
87 <h2>流局 · Draw</h2>
88 {/* Nobody won, so the thing everyone wants to see is who was
89 there and on what — the same reveal as turning the hands over
90 at a real table. */}
91 <div className="ready-list">
92 {s.players.map((p, seat) => {
93 const waiting = game.tenpai(seat as SeatId);
94 return (
95 <div className={`ready-row ${waiting.length ? '' : 'none'}`} key={seat}>
96 <span className="ready-name">{p.name}</span>
97 {waiting.length > 0 ? (
98 <>
99 <span className="tag ting">聽 Ready</span>
100 <span className="ready-tiles">
101 {waiting.map((t) => (
102 <TileView key={t} tile={t} size="small" />
103 ))}
104 </span>
105 </>
106 ) : (
107 <span className="ready-no">未聽 · not ready</span>
108 )}
109 </div>
110 );
111 })}
112 </div>
113 </>
114 ) : (
115 <>
116 {/* One block per winner — more than one only under 一炮多響. */}
117 {r.winners.map((w) => (
118 <div className="win" key={w.seat}>
119 <h2>
120 {s.players[w.seat].name} {w.selfDraw ? '自摸' : '胡牌'} · {w.tai} 台
121 <span className="wt"> {labelOf(w.winTile)}</span>
122 </h2>
123 {w.liable !== null && (
124 <div className="liable">
125 包牌 · {s.players[w.liable].name} pays the whole hand
126 </div>
127 )}
128 <div className="tai-list">
129 {w.items.map((it, i) => (
130 <div key={i} className="tai-item">
131 <span className="tai-name">
132 {it.name}
133 <em>{it.en}</em>
134 </span>
135 <span className="tai-val">{it.tai}</span>
136 </div>
137 ))}
138 </div>
139 </div>
140 ))}
141 {r.winners.length > 1 && (
142 <div className="multi">一炮多響 · one discard, {r.winners.length} winners</div>
143 )}
144 <div className="deltas">
145 {r.deltas.map((d, i) => (
146 <span key={i} className={d > 0 ? 'up' : d < 0 ? 'down' : ''}>
147 {s.players[i].name} {d > 0 ? `+${d}` : d}
148 </span>
149 ))}
150 </div>
151 </>
152 )}
153 <button className="btn big-btn" onClick={() => game.nextHand()}>
154 <Bi zh="下一局" en="Next hand" />
155 </button>
156 </div>
157 )}
158
159 {s.phase === 'gameEnd' && (
160 <div className="result">
161 <h2>一將終了 · Game over</h2>
162 <div className="deltas">
163 {[...s.players]
164 .map((p, i) => ({ p, i }))
165 .sort((a, b) => b.p.score - a.p.score)
166 .map(({ p, i }) => (
167 <span key={i} className={p.score > s.rules.startingScore ? 'up' : p.score < s.rules.startingScore ? 'down' : ''}>
168 {p.name} {p.score}
169 </span>
170 ))}
171 </div>
172 <div className="over-buttons">
173 <button className="btn big-btn" onClick={() => game.newGame(s.bots)}>
174 <Bi zh="再來一將" en="New game" />
175 </button>
176 {/* The moment anyone is going to want the names or the stakes changed. */}
177 <button className="btn ghost big-btn" onClick={onSettings}>
178 <Bi zh="設定" en="Settings" />
179 </button>
180 </div>
181 </div>
182 )}
183
184 <div className="table-controls">
185 {undoable && (
186 <button className="btn ghost undo" onClick={() => game.undo()}>
187 <span className="undo-arrow" aria-hidden>
188 ↺
189 </span>
190 <span className="undo-what">
191 <span className="zh">復原 {undoable.zh}</span>
192 <span className="en">Undo · {undoable.en}</span>
193 </span>
194 </button>
195 )}
196 {/* Only on the layout the address bar actually crowds. */}
197 {compact && <FullscreenToggle prefs={prefs} onPrefs={onPrefs} compact />}
198 <SoundToggle prefs={prefs} onPrefs={onPrefs} compact />
199 </div>
200 </div>
201 );
202}