// The viewer's two modes (docs/games.md §8.4). Replay: kit/bridge.js hands over a replay, which plays move by move
// with step controls. Live (index.html?live): the room page posts the room's messages in batches, and a move made at
// the table goes back through shoalAct. Nothing is stored: the frame is sandboxed, with no storage to rely on.
import { useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { createRoot } from 'react-dom/client';
import './deskpark/deskpark.css';
import './shoal.css';
import { Board } from './Board';
import type { Msg, Replay as ReplayData } from './model';
import { liveView, replayView } from './model';
interface Batch {
room: string;
you?: (string | number)[];
messages: Msg[];
}
declare global {
interface Window {
shoalReplay(start: (text: string, name: string) => void): void;
shoalLive(fn: (batch: Batch) => void): void;
shoalAct(seat: string, version: number, action: string): void;
}
}
function Shell({ sub, right, children }: { sub: string; right?: ReactNode; children?: ReactNode }) {
return (
斗地主固定地主 · 无叫牌{sub}
{right}
{children}
);
}
function ReplayMode() {
const [R, setR] = useState(null);
const [t, setT] = useState(0);
const [playing, setPlaying] = useState(false);
useEffect(
() =>
window.shoalReplay((text) => {
setR(JSON.parse(text));
setT(0);
setPlaying(true);
}),
[],
);
const n = R ? R.moveHistory.length : 0;
useEffect(() => {
if (!R || !playing) return;
if (t >= n) return setPlaying(false);
const id = setTimeout(() => setT(t + 1), t === 0 ? 1200 : 850);
return () => clearTimeout(id);
}, [R, t, n, playing]);
if (!R) return ;
const go = (x: number) => {
setPlaying(false);
setT(Math.max(0, Math.min(n, x)));
};
const controls = (
go(Number(e.target.value))} />
{t} / {n}
);
return (
);
}
function LiveMode() {
const [s, setS] = useState({ room: '', you: [] as string[], msgs: new Map() });
useEffect(
() =>
window.shoalLive((b) =>
setS((old) => {
const msgs = new Map(old.msgs); // batches may overlap: n is the message's identity
for (const m of b.messages ?? []) msgs.set(m.n, m);
return { room: b.room, you: (b.you ?? []).map(String), msgs };
}),
),
[],
);
const { v, reqs } = useMemo(() => liveView([...s.msgs.values()].sort((a, b) => a.n - b.n), s.you), [s]);
const req = reqs[0] ?? null;
const mine = s.you.map(Number);
const me = req ? req.seat : mine.length ? mine[0] : (v.landlord ?? 0);
const status = v.result ? '已结束' : req ? '轮到你出牌' : v.landlord === null ? '等待开局' : '对局中';
return (
{status}}>
req && window.shoalAct(String(req.seat), req.version, a)}
/>
);
}
const live = new URLSearchParams(location.search).has('live');
createRoot(document.getElementById('root')!).render(live ? : );