// 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 ? : );