diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 8e6d4f4..d937472 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -293,7 +293,7 @@ wss.on("connection", (socket) => {
return send(socket, { type: "error", message: "catching up already — one moment" });
}
session.lastCatchUpAt = now;
- const steps = catchUpSteps(room, session.playerId, Number(msg.sinceSeq ?? 0));
+ const steps = catchUpSteps(room, session.playerId, Number(msg.sinceSeq ?? 0), msg.full === true);
if ("error" in steps) return send(socket, { type: "error", message: steps.error });
send(socket, { type: "catchUp", steps });
break;
diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts
index 76661ba..b8ba26c 100644
--- a/packages/server/src/rooms.ts
+++ b/packages/server/src/rooms.ts
@@ -268,11 +268,12 @@ export interface CatchUpStep {
* Rebuild the game and capture a redacted view after each command from
* `sinceSeq` on — the "what happened while you were away" reel.
*/
-export function catchUpSteps(room: Room, playerId: PlayerId, sinceSeq: number): CatchUpStep[] | { error: string } {
+export function catchUpSteps(room: Room, playerId: PlayerId, sinceSeq: number, full = false): CatchUpStep[] | { error: string } {
if (!room.state) return { error: "game not started" };
if (!room.players.includes(playerId)) return { error: "you hold no seat in this room" };
+ if (full && room.state.phase !== "finished") return { error: "full replays wait for the game to finish" };
const MAX_STEPS = 200;
- const from = Math.max(sinceSeq, room.log.length - MAX_STEPS);
+ const from = full ? 0 : Math.max(sinceSeq, room.log.length - MAX_STEPS);
const { state: fresh } = createGame(room.state.config);
let current = fresh;
const steps: CatchUpStep[] = [];
diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte
index 1850b69..e079b5e 100644
--- a/packages/web/src/App.svelte
+++ b/packages/web/src/App.svelte
@@ -1293,7 +1293,12 @@
{#if view.phase === "finished" && view.revealedHands}
-
The hands, face-up
+
+ The hands, face-up
+ {#if !local.active}
+ net.requestFullReplay()}>⟲ Watch the whole game
+ {/if}
+
{#each view.players as p (p.id)}
@@ -1926,6 +1931,10 @@
margin-bottom: 0.6rem;
}
.reveal-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
font-family: "Oswald", sans-serif;
font-size: 0.8rem;
letter-spacing: 0.12em;
diff --git a/packages/web/src/Replay.svelte b/packages/web/src/Replay.svelte
index 6576083..2114863 100644
--- a/packages/web/src/Replay.svelte
+++ b/packages/web/src/Replay.svelte
@@ -13,6 +13,7 @@
let idx = $state(0);
let playing = $state(true);
+ let speed = $state(1);
const step = $derived(steps[Math.min(idx, steps.length - 1)]!);
const lines = $derived(
step.events.map(humanize).filter((l): l is string => l !== null),
@@ -24,7 +25,7 @@
const t = setInterval(() => {
if (idx < steps.length - 1) idx += 1;
else playing = false;
- }, 1500);
+ }, 1500 / speed);
return () => clearInterval(t);
});
@@ -41,7 +42,7 @@
- While you were away
+ {steps[0]?.seq === 0 ? "The whole tale, from the deal" : "While you were away"}
move {idx + 1} of {steps.length}
{atEnd ? "back to the game" : "skip to now"}
@@ -59,6 +60,9 @@
{playing ? "❚❚" : "▶"}
{ playing = false; idx = Math.min(steps.length - 1, idx + 1); }} aria-label="next move">▶
+ {#each [1, 2, 4] as x (x)}
+ (speed = x)}>{x}×
+ {/each}
@@ -129,6 +133,11 @@
line-height: 1.45;
min-height: 3.4rem;
}
+ .replay-controls .speed {
+ font-size: 0.75rem;
+ opacity: 0.7;
+ }
+ .replay-controls .speed.current { opacity: 1; text-decoration: underline; }
.replay-controls {
display: flex;
justify-content: center;
diff --git a/packages/web/src/net.svelte.ts b/packages/web/src/net.svelte.ts
index c74556b..4e505a3 100644
--- a/packages/web/src/net.svelte.ts
+++ b/packages/web/src/net.svelte.ts
@@ -461,6 +461,11 @@ class Net {
}
/** Ask for the reel of everything since we last watched. */
+ /** The whole game from the deal, once it is finished. */
+ requestFullReplay(): void {
+ this.send({ type: "catchUp", sinceSeq: 0, full: true });
+ }
+
requestCatchUp(): void {
if (!this.roomId) return;
this.send({ type: "catchUp", sinceSeq: this.seen[this.roomId] ?? 0 });