From 0f4ae0dca4851727da3e9074bff5feef9b708008 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Tue, 27 Oct 2026 23:11:52 -0400 Subject: [PATCH] A move picked from the chronicle puts the boards back as they stood after it, on the flat boards and the glass alike, with Earlier, Later and Back to now in the status strip; one move on plays itself, any other leap sets the pieces out where they stood, and a tap on the boards brings the game back to now Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx --- src/lib/components/Board.svelte | 64 ++++++++++++++++++++++------- src/lib/components/Chronicle.svelte | 4 +- src/lib/game/chronicle.ts | 4 +- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/lib/components/Board.svelte b/src/lib/components/Board.svelte index 0a98c19..4046c4b 100644 --- a/src/lib/components/Board.svelte +++ b/src/lib/components/Board.svelte @@ -44,29 +44,32 @@ }; const flipped = $derived(mySide === 'scarlet'); const inCheckNow = $derived(!g.over && inCheck(g.cells, g.toMove)); - const checkSquare = $derived(inCheckNow ? kingSquare(g.cells, g.toMove) : -1); // --- Tokens: pieces with a life of their own, so a move slides, a capture fades and a Warrior stands up a Hero --- let tokens = $state([]); - /** How many of the game's moves the tokens have played through. */ + /** How many of the moves the tokens have played through. */ let shown = $state(0); let animating = $state(false); + /** Bumped when the tokens are set out afresh, so a catch-up in flight lets go of them. */ + let epoch = 0; const calm = $derived(reducedMotion()); const showCoords = $derived(prefs.coordinates !== false); const wait = (ms: number) => new Promise((r) => setTimeout(r, calm ? 0 : ms)); function resetTokens() { + epoch += 1; let id = 0; - tokens = g.cells.flatMap((piece, at) => (piece === '' ? [] : [{ id: id++, piece, at, dying: false }])); - shown = g.moves.length; + tokens = v.cells.flatMap((piece, at) => (piece === '' ? [] : [{ id: id++, piece, at, dying: false }])); + shown = v.moves.length; } async function catchUp() { if (animating) return; animating = true; + const mine = epoch; try { - while (shown < g.moves.length) { - const m = g.moves[shown]; + while (shown < v.moves.length) { + const m = v.moves[shown]; const mover = tokens.find((t) => t.at === m.from && !t.dying); if (!mover) { resetTokens(); @@ -80,12 +83,15 @@ if (victim) victim.dying = true; mover.at = m.to; await wait(380); + if (mine !== epoch) return; if (m.promoted) mover.piece = (mover.piece[0] + 'H') as typeof mover.piece; } + if (mine !== epoch) return; if (victim) tokens = tokens.filter((t) => !t.dying); shown += 1; announce(storyOf(shown)); - if (shown < g.moves.length) await wait(260); + if (shown < v.moves.length) await wait(260); + if (mine !== epoch) return; } } finally { animating = false; @@ -93,26 +99,40 @@ } $effect(() => { - const total = g.moves.length; - if (total < shown || (tokens.length === 0 && g.cells.some((p) => p !== ''))) resetTokens(); + const total = v.moves.length; + if (total < shown || (tokens.length === 0 && v.cells.some((p) => p !== ''))) resetTokens(); else if (total > shown) void catchUp(); }); - // --- What the boards show: the last move played, or a move picked from the record --- + // --- What the boards show: the game as it stands, or as it stood after a move picked from the record --- let reviewing = $state(null); $effect(() => { void g.moves.length; reviewing = null; selected = null; }); + /** The state the boards draw: the game's own, or the position after the move under review, with that many moves played. */ + const v = $derived.by((): typeof g => { + if (reviewing === null) return g; + const t = tellings[reviewing - 1]; + return { ...g, cells: t.after, moves: g.moves.slice(0, reviewing), toMove: t.move.by === 'gold' ? 'scarlet' : 'gold' }; + }); + /** The King in check on the boards as drawn: the game's, or the position under review. */ + const checkSquare = $derived(reviewing !== null ? (inCheck(v.cells, v.toMove) ? kingSquare(v.cells, v.toMove) : -1) : inCheckNow ? kingSquare(g.cells, g.toMove) : -1); const shownPly = $derived(reviewing ?? shown); const shownLast = $derived(shownPly > 0 ? g.moves[shownPly - 1] : null); const shownSquares = $derived(shownLast ? (shownLast.afar ? [shownLast.from] : [shownLast.from, shownLast.to]) : []); const shownCaptured = $derived(shownLast?.captured ? [shownLast.to] : []); - /** A move picked from the record lights its squares on the boards as they stand; the pieces stay where they are. */ + /** A move picked from the record puts the boards back as they stood after it, and brings its board into view. */ function review(ply: number | null) { + if (ply !== null && (ply < 1 || ply > g.moves.length)) return; + const step = (ply ?? g.moves.length) - shown; reviewing = ply; + selected = null; + inspected = null; + // One move on plays itself; any other leap sets the pieces out where they stood. + if (step !== 1) resetTokens(); if (ply === null) return; const m = g.moves[ply - 1]; requestAnimationFrame(() => { @@ -209,7 +229,12 @@ async function tap(i: number) { if (room.sending || animating) return; - reviewing = null; + if (reviewing !== null) { + // The boards are showing an earlier position: a tap brings them back to now, and nothing more. + review(null); + announce('Back to the game as it stands.'); + return; + } if (!yourMove) { // Not your move, or no seat: a tap on a piece still opens its panel. inspected = g.cells[i] !== '' ? (inspected === i ? null : i) : null; @@ -304,7 +329,10 @@ {nameOfSide(g.toMove)} is thinking · you are {SIDE_NAMES[mySide!]}{#if inCheckNow} · {nameOfSide(g.toMove)} is in check{/if} {/if} {#if reviewing !== null && !pending} - Move {reviewing} is lit on the boards as they stand now; no piece has moved back. + The boards as they stood after move {reviewing} of {g.moves.length}. + · + · + {/if} {#if shownStory && (shownCaptured.length || reviewing !== null || shownLast?.check || shownLast?.promoted || shownFreeze)} @@ -333,10 +361,10 @@

{#if drake && !g.over} · {/if}

{#if fault}

{fault}

{/if} {#if onGlass && Glass} - + {:else}
- +
{/if}

{liveText}

@@ -428,6 +456,12 @@ color: var(--frost); } + .marked .link:disabled { + color: var(--bone-faint); + text-decoration: none; + cursor: default; + } + .check { color: var(--ember); font-weight: 600; diff --git a/src/lib/components/Chronicle.svelte b/src/lib/components/Chronicle.svelte index 514c4f1..1d159bd 100644 --- a/src/lib/components/Chronicle.svelte +++ b/src/lib/components/Chronicle.svelte @@ -1,7 +1,7 @@