Compare commits

..
2 Commits
6 changed files with 157 additions and 24 deletions

No files matched your search

+120 -16
View File
@@ -44,29 +44,32 @@
}; };
const flipped = $derived(mySide === 'scarlet'); const flipped = $derived(mySide === 'scarlet');
const inCheckNow = $derived(!g.over && inCheck(g.cells, g.toMove)); 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 --- // --- 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<Token[]>([]); let tokens = $state<Token[]>([]);
/** 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 shown = $state(0);
let animating = $state(false); 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 calm = $derived(reducedMotion());
const showCoords = $derived(prefs.coordinates !== false); const showCoords = $derived(prefs.coordinates !== false);
const wait = (ms: number) => new Promise((r) => setTimeout(r, calm ? 0 : ms)); const wait = (ms: number) => new Promise((r) => setTimeout(r, calm ? 0 : ms));
function resetTokens() { function resetTokens() {
epoch += 1;
let id = 0; let id = 0;
tokens = g.cells.flatMap((piece, at) => (piece === '' ? [] : [{ id: id++, piece, at, dying: false }])); tokens = v.cells.flatMap((piece, at) => (piece === '' ? [] : [{ id: id++, piece, at, dying: false }]));
shown = g.moves.length; shown = v.moves.length;
} }
async function catchUp() { async function catchUp() {
if (animating) return; if (animating) return;
animating = true; animating = true;
const mine = epoch;
try { try {
while (shown < g.moves.length) { while (shown < v.moves.length) {
const m = g.moves[shown]; const m = v.moves[shown];
const mover = tokens.find((t) => t.at === m.from && !t.dying); const mover = tokens.find((t) => t.at === m.from && !t.dying);
if (!mover) { if (!mover) {
resetTokens(); resetTokens();
@@ -80,12 +83,15 @@
if (victim) victim.dying = true; if (victim) victim.dying = true;
mover.at = m.to; mover.at = m.to;
await wait(380); await wait(380);
if (mine !== epoch) return;
if (m.promoted) mover.piece = (mover.piece[0] + 'H') as typeof mover.piece; 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); if (victim) tokens = tokens.filter((t) => !t.dying);
shown += 1; shown += 1;
announce(storyOf(shown)); announce(storyOf(shown));
if (shown < g.moves.length) await wait(260); if (shown < v.moves.length) await wait(260);
if (mine !== epoch) return;
} }
} finally { } finally {
animating = false; animating = false;
@@ -93,34 +99,88 @@
} }
$effect(() => { $effect(() => {
const total = g.moves.length; const total = v.moves.length;
if (total < shown || (tokens.length === 0 && g.cells.some((p) => p !== ''))) resetTokens(); if (total < shown || (tokens.length === 0 && v.cells.some((p) => p !== ''))) resetTokens();
else if (total > shown) void catchUp(); 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<number | null>(null); let reviewing = $state<number | null>(null);
$effect(() => { $effect(() => {
void g.moves.length; void g.moves.length;
reviewing = null; reviewing = null;
selected = 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 shownPly = $derived(reviewing ?? shown);
const shownLast = $derived<Move | null>(shownPly > 0 ? g.moves[shownPly - 1] : null); const shownLast = $derived<Move | null>(shownPly > 0 ? g.moves[shownPly - 1] : null);
const shownSquares = $derived(shownLast ? (shownLast.afar ? [shownLast.from] : [shownLast.from, shownLast.to]) : []); const shownSquares = $derived(shownLast ? (shownLast.afar ? [shownLast.from] : [shownLast.from, shownLast.to]) : []);
const shownCaptured = $derived(shownLast?.captured ? [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) { function review(ply: number | null) {
if (ply !== null && (ply < 1 || ply > g.moves.length)) return;
const step = (ply ?? g.moves.length) - shown;
reviewing = ply; 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; if (ply === null) return;
const m = g.moves[ply - 1]; const m = g.moves[ply - 1];
if (oneBoard && !onGlass) {
level = levelOf(m.to);
return;
}
requestAnimationFrame(() => { requestAnimationFrame(() => {
const target = onGlass ? document.querySelector('.play canvas') : document.querySelector(`.play g.world[data-level="${levelOf(m.to)}"]`); const target = onGlass ? document.querySelector('.play canvas') : document.querySelector(`.play g.world[data-level="${levelOf(m.to)}"]`);
target?.scrollIntoView({ block: matchMedia('(max-width: 860px)').matches ? 'center' : 'nearest', behavior: calm ? 'auto' : 'smooth' }); target?.scrollIntoView({ block: phone ? 'center' : 'nearest', behavior: calm ? 'auto' : 'smooth' });
}); });
} }
// --- Phones: the status strip carries a jump to each board, and a phone on its side shows one board at a time ---
let phone = $state(false);
let oneBoard = $state(false);
$effect(() => {
const narrow = matchMedia('(max-width: 860px)');
const short = matchMedia('(max-width: 1000px) and (max-height: 540px)');
const read = () => {
phone = narrow.matches;
oneBoard = short.matches;
};
read();
narrow.addEventListener('change', read);
short.addEventListener('change', read);
return () => {
narrow.removeEventListener('change', read);
short.removeEventListener('change', read);
};
});
/** The board shown when there is room for one at a time; a move made on another brings that one forward. */
let level = $state(2);
$effect(() => {
const m = g.moves.at(-1);
if (m) level = levelOf(m.to);
});
function goLevel(lv: number) {
if (oneBoard) {
level = lv;
return;
}
const world = document.querySelector(`.play g.world[data-level="${lv}"]`);
const strip = document.querySelector('.play .status');
if (!world) return;
const top = world.getBoundingClientRect().top + scrollY - ((strip?.getBoundingClientRect().height ?? 0) + 14);
scrollTo({ top, behavior: calm ? 'auto' : 'smooth' });
}
/** The game told move by move, replayed from the ledger's moves. */ /** The game told move by move, replayed from the ledger's moves. */
const tellings = $derived(narrate(g)); const tellings = $derived(narrate(g));
function storyOf(ply: number): string { function storyOf(ply: number): string {
@@ -209,7 +269,12 @@
async function tap(i: number) { async function tap(i: number) {
if (room.sending || animating) return; 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) { if (!yourMove) {
// Not your move, or no seat: a tap on a piece still opens its panel. // Not your move, or no seat: a tap on a piece still opens its panel.
inspected = g.cells[i] !== '' ? (inspected === i ? null : i) : null; inspected = g.cells[i] !== '' ? (inspected === i ? null : i) : null;
@@ -303,8 +368,14 @@
{:else} {:else}
<strong>{nameOfSide(g.toMove)} is thinking</strong> · you are {SIDE_NAMES[mySide!]}{#if inCheckNow}&nbsp;· <span class="check">{nameOfSide(g.toMove)} is in check</span>{/if} <strong>{nameOfSide(g.toMove)} is thinking</strong> · you are {SIDE_NAMES[mySide!]}{#if inCheckNow}&nbsp;· <span class="check">{nameOfSide(g.toMove)} is in check</span>{/if}
{/if} {/if}
{#if phone && !onGlass && !pending}
<span class="jump">{#each [[3, 'Air'], [2, 'Land'], [1, 'Underworld']] as [lv, label] (lv)}<button type="button" class="link plain" class:current={oneBoard && level === lv} class:lit={targets.some((t) => levelOf(t) === lv)} onclick={() => goLevel(lv as number)}>{label}</button>{/each}</span>
{/if}
{#if reviewing !== null && !pending} {#if reviewing !== null && !pending}
<span class="marked">Move {reviewing} is lit on the boards as they stand now; no piece has moved back. <button type="button" class="link" onclick={() => review(null)}>Clear it</button></span> <span class="marked">The boards as they stood after move {reviewing} of {g.moves.length}.
<button type="button" class="link" disabled={reviewing <= 1} onclick={() => review(reviewing! - 1)}>Earlier</button> ·
<button type="button" class="link" disabled={reviewing >= g.moves.length} onclick={() => review(reviewing! + 1)}>Later</button> ·
<button type="button" class="link" onclick={() => review(null)}>Back to now</button></span>
{/if} {/if}
</div> </div>
{#if shownStory && (shownCaptured.length || reviewing !== null || shownLast?.check || shownLast?.promoted || shownFreeze)} {#if shownStory && (shownCaptured.length || reviewing !== null || shownLast?.check || shownLast?.promoted || shownFreeze)}
@@ -333,10 +404,10 @@
<p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button>{#if drake && !g.over}&nbsp;· <button type="button" class="link plain" disabled={starting} onclick={again}>Start over against a fresh drake</button>{/if}</p> <p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button>{#if drake && !g.over}&nbsp;· <button type="button" class="link plain" disabled={starting} onclick={again}>Start over against a fresh drake</button>{/if}</p>
{#if fault}<p class="warning">{fault}</p>{/if} {#if fault}<p class="warning">{fault}</p>{/if}
{#if onGlass && Glass} {#if onGlass && Glass}
<Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} /> <Glass g={v} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} />
{:else} {:else}
<div class="frame"> <div class="frame">
<BoardArtwork {g} {tokens} {showCoords} {flipped} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} {escape} /> <BoardArtwork g={v} {tokens} levels={oneBoard ? [level] : [3, 2, 1]} climb={(lv) => (level = lv)} {showCoords} {flipped} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} {escape} />
</div> </div>
{/if} {/if}
<p class="visually-hidden" aria-live="polite">{liveText}</p> <p class="visually-hidden" aria-live="polite">{liveText}</p>
@@ -428,6 +499,32 @@
color: var(--frost); color: var(--frost);
} }
/* The jumps: one a board, and a mark on any board the chosen piece can reach. */
.jump {
display: flex;
gap: 1rem;
margin-top: 0.25rem;
font-size: 0.85rem;
}
.jump .current {
color: var(--ember);
}
.jump .lit::after {
content: ' ●';
font-size: 0.6em;
vertical-align: middle;
color: var(--frost);
}
.marked .link:disabled {
color: var(--bone-faint);
text-decoration: none;
cursor: default;
}
.check { .check {
color: var(--ember); color: var(--ember);
font-weight: 600; font-weight: 600;
@@ -454,6 +551,13 @@
margin: 0 auto; margin: 0 auto;
} }
/* A phone on its side has no height for three boards: one shows at a time, as wide as the screen allows. */
@media (max-width: 1000px) and (max-height: 540px) {
.frame {
max-width: none;
}
}
.glass-way { .glass-way {
max-width: 640px; max-width: 640px;
margin: 0 auto 0.5rem; margin: 0 auto 0.5rem;
+16 -3
View File
@@ -15,10 +15,11 @@
// underworld, each twelve by eight in the colours Gygax suggested, with // underworld, each twelve by eight in the colours Gygax suggested, with
// the pieces as discs bearing his letters. Shared by the table, the // the pieces as discs bearing his letters. Shared by the table, the
// lessons and the hall's still of the opening. // lessons and the hall's still of the opening.
import { tick } from 'svelte';
import { FILES, RANKS, PER_LEVEL, fileOf, rankOf, levelOf, idx, isLight, kindOf, sideOf, frozen, squareName, LETTERS, NAMES, SIDE_NAMES, type State } from '$lib/game'; import { FILES, RANKS, PER_LEVEL, fileOf, rankOf, levelOf, idx, isLight, kindOf, sideOf, frozen, squareName, LETTERS, NAMES, SIDE_NAMES, type State } from '$lib/game';
let { let {
g, tokens, levels = [3, 2, 1], showCoords = false, flipped = false, selected = null, targets = [], reach = [], shownSquares = [], shownCaptured = [], checkSquare = -1, g, tokens, levels = [3, 2, 1], showCoords = false, flipped = false, selected = null, targets = [], reach = [], shownSquares = [], shownCaptured = [], checkSquare = -1,
calm = true, decorative = false, tap = () => {}, escape = () => {} calm = true, decorative = false, tap = () => {}, escape = () => {}, climb
}: { }: {
g: State; g: State;
tokens?: Token[]; tokens?: Token[];
@@ -40,9 +41,12 @@
tap?: (i: number) => void | Promise<void>; tap?: (i: number) => void | Promise<void>;
/** Escape pressed on a square: the caller lets go of whatever it was doing. */ /** Escape pressed on a square: the caller lets go of whatever it was doing. */
escape?: () => void; escape?: () => void;
/** Page Up or Page Down towards a board that is not drawn: the caller may bring it out. */
climb?: (level: number) => void;
} = $props(); } = $props();
let svg: SVGSVGElement | undefined = $state(); let svg: SVGSVGElement | undefined = $state();
const focusSquare = (i: number) => svg?.querySelector<SVGRectElement>(`rect.square[data-i="${i}"]`)?.focus();
/** The one square Tab reaches: the grid is entered once, at the square last visited, and the keys walk it from there. */ /** The one square Tab reaches: the grid is entered once, at the square last visited, and the keys walk it from there. */
let visited = $state<number | null>(null); let visited = $state<number | null>(null);
/** Arrows walk a board the way it is drawn, Page Up and Page Down climb and descend, Enter picks up and sets down. */ /** Arrows walk a board the way it is drawn, Page Up and Page Down climb and descend, Enter picks up and sets down. */
@@ -57,7 +61,13 @@
const f = fileOf(i) + df; const f = fileOf(i) + df;
const r = rankOf(i) + dr; const r = rankOf(i) + dr;
const lv = levelOf(i) + dl; const lv = levelOf(i) + dl;
if (f >= 0 && f < FILES && r >= 0 && r < RANKS && levels.includes(lv)) svg?.querySelector<SVGRectElement>(`rect.square[data-i="${idx(lv, f, r)}"]`)?.focus(); if (f < 0 || f >= FILES || r < 0 || r >= RANKS || lv < 1 || lv > 3) return;
const to = idx(lv, f, r);
if (levels.includes(lv)) focusSquare(to);
else if (climb) {
climb(lv);
void tick().then(() => focusSquare(to));
}
} else if (e.key === 'Enter' || e.key === ' ') { } else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
void tap(i); void tap(i);
@@ -88,7 +98,10 @@
return { x: PAD_L + f * CELL, y: slot * (boardHeight + GAP) + PAD_T + r * CELL }; return { x: PAD_L + f * CELL, y: slot * (boardHeight + GAP) + PAD_T + r * CELL };
} }
const drawn = $derived(levels.flatMap((lv) => Array.from({ length: PER_LEVEL }, (_, n) => (lv - 1) * PER_LEVEL + n))); const drawn = $derived(levels.flatMap((lv) => Array.from({ length: PER_LEVEL }, (_, n) => (lv - 1) * PER_LEVEL + n)));
const entry = $derived(visited ?? selected ?? (levels.includes(2) ? idx(2, flipped ? FILES - 1 : 0, flipped ? RANKS - 1 : 0) : drawn[0])); const entry = $derived.by(() => {
for (const c of [visited, selected]) if (c !== null && drawn.includes(c)) return c;
return levels.includes(2) ? idx(2, flipped ? FILES - 1 : 0, flipped ? RANKS - 1 : 0) : drawn[0];
});
const fileLetter = (n: number) => 'abcdefghijkl'[flipped ? FILES - 1 - n : n]; const fileLetter = (n: number) => 'abcdefghijkl'[flipped ? FILES - 1 - n : n];
const rankNumber = (n: number) => (flipped ? n + 1 : RANKS - n); const rankNumber = (n: number) => (flipped ? n + 1 : RANKS - n);
const describe = (i: number) => { const describe = (i: number) => {
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
// The game told top to bottom, oldest first, each move with what came of // The game told top to bottom, oldest first, each move with what came of
// it, and the newest scrolled into view. A move's heading is a button that // it, and the newest scrolled into view. A move's heading is a button that
// shows that move on the boards. // puts the boards back as they stood after it.
import { tick } from 'svelte'; import { tick } from 'svelte';
import type { Telling } from '$lib/game/chronicle'; import type { Telling } from '$lib/game/chronicle';
import { notation, type Side } from '$lib/game'; import { notation, type Side } from '$lib/game';
@@ -25,7 +25,7 @@
{#each tellings as t (t.ply)} {#each tellings as t (t.ply)}
<article class:mine={mySide === t.move.by} class:reviewed={reviewing === t.ply}> <article class:mine={mySide === t.move.by} class:reviewed={reviewing === t.ply}>
<h3> <h3>
<button type="button" class="link plain" title="Light this move's squares on the boards as they stand" onclick={() => onReview(reviewing === t.ply ? null : t.ply)}> <button type="button" class="link plain" title="Show the boards as they stood after this move" onclick={() => onReview(reviewing === t.ply ? null : t.ply)}>
Move {t.ply} <span class="notation">{notation(t.move)}</span> Move {t.ply} <span class="notation">{notation(t.move)}</span>
</button> </button>
</h3> </h3>
+3 -1
View File
@@ -37,7 +37,7 @@
/** The boards' colours, Gygax's: light blue and white, light green and amber, red and brown. */ /** The boards' colours, Gygax's: light blue and white, light green and amber, red and brown. */
const SQUARES: Record<number, [string, string]> = { 3: ['#e4ecf2', '#9fb8cc'], 2: ['#e6d7a3', '#8fa66a'], 1: ['#c9937a', '#7a4a3a'] }; const SQUARES: Record<number, [string, string]> = { 3: ['#e4ecf2', '#9fb8cc'], 2: ['#e6d7a3', '#8fa66a'], 1: ['#c9937a', '#7a4a3a'] };
const LIVERY: Record<Side, string> = { gold: '#d8a63e', scarlet: '#a4302a' }; const LIVERY: Record<Side, string> = { gold: '#d8a63e', scarlet: '#a4302a' };
const MARK = { selected: '#8cc8e0', target: '#8cc8e0', reach: '#8cc8e0', last: '#f0a53a', fallen: '#e0655c', check: '#e0655c' }; const MARK = { selected: '#8cc8e0', target: '#8cc8e0', reach: '#8cc8e0', last: '#f0a53a', fallen: '#e0655c', check: '#e0655c', frozen: '#b9c9d2' };
/** Air between the sheets, in squares: enough for a Dragon's wings. */ /** Air between the sheets, in squares: enough for a Dragon's wings. */
const GAP = 4.2; const GAP = 4.2;
const CELL = 1; const CELL = 1;
@@ -447,6 +447,8 @@
}); });
$effect(() => { $effect(() => {
marks.clear(); marks.clear();
// A figure held by a Basilisk stands in a pale ring, since its dimmed livery is easy to miss among forty.
for (let i = 0; i < g.cells.length; i++) if (g.cells[i] !== '' && frozen(g.cells, i) && i !== selected) mark(i, 'ring', MARK.frozen, 0.8);
for (const i of reach) mark(i, g.cells[i] === '' ? 'dot' : 'ring', MARK.reach, 0.85); for (const i of reach) mark(i, g.cells[i] === '' ? 'dot' : 'ring', MARK.reach, 0.85);
for (const i of shownSquares) mark(i, 'halo', MARK.last); for (const i of shownSquares) mark(i, 'halo', MARK.last);
for (const i of shownCaptured) mark(i, 'halo', MARK.fallen); for (const i of shownCaptured) mark(i, 'halo', MARK.fallen);
+13 -1
View File
@@ -24,6 +24,8 @@
Object.fromEntries((game.options ?? []).map((o) => [o.key, o.choices.some((c) => c.value === prefs[`table.${o.key}`]) ? String(prefs[`table.${o.key}`]) : o.default])) Object.fromEntries((game.options ?? []).map((o) => [o.key, o.choices.some((c) => c.value === prefs[`table.${o.key}`]) ? String(prefs[`table.${o.key}`]) : o.default]))
); );
let prefsOpen = $state(false); let prefsOpen = $state(false);
/** The side and the Sylphs' setup wait behind one line until asked for. */
let setupOpen = $state(false);
/** The opening as the host's choices lay it, drawn beside the buttons. */ /** The opening as the host's choices lay it, drawn beside the buttons. */
const opening = $derived(createGame({ A: 'Gold', B: 'Scarlet' }, 0, undefined, options)); const opening = $derived(createGame({ A: 'Gold', B: 'Scarlet' }, 0, undefined, options));
let busy = $state(false); let busy = $state(false);
@@ -218,7 +220,9 @@
</label> </label>
{#if !ready}<p class="option-note muted small">A name first, and the buttons wake. The drake is the computer, playing for the house.</p>{/if} {#if !ready}<p class="option-note muted small">A name first, and the buttons wake. The drake is the computer, playing for the house.</p>{/if}
{#if game.options?.length} {#if game.options?.length}
<div class="options"> {@const chosenLabels = game.options.map((o) => `${o.label} ${o.choices.find((c) => c.value === options[o.key])?.label.replace(/^(?!Gold|Scarlet)./, (ch) => ch.toLowerCase()) ?? ''}.`)}
<p class="setup muted small">{chosenLabels.join(' ')} <button type="button" class="link plain" aria-expanded={setupOpen} onclick={() => (setupOpen = !setupOpen)}>{setupOpen ? 'Done' : 'Change'}</button></p>
<div class="options" hidden={!setupOpen}>
{#each game.options as o (o.key)} {#each game.options as o (o.key)}
{@const chosen = o.choices.find((c) => c.value === options[o.key])} {@const chosen = o.choices.find((c) => c.value === options[o.key])}
<div> <div>
@@ -660,6 +664,14 @@
margin: 0.2rem 0 0.6rem; margin: 0.2rem 0 0.6rem;
} }
.options[hidden] {
display: none;
}
.setup {
margin: 0.2rem 0 0.6rem;
}
.option { .option {
display: flex; display: flex;
align-items: center; align-items: center;
+3 -1
View File
@@ -20,6 +20,8 @@ export interface Telling {
ply: number; ply: number;
move: Move; move: Move;
lines: Line[]; lines: Line[];
/** The boards as they stood once the move was made. */
after: Cell[];
} }
const WORLD: Record<number, string> = { 3: 'the air', 2: 'the land', 1: 'the underworld' }; const WORLD: Record<number, string> = { 3: 'the air', 2: 'the land', 1: 'the underworld' };
@@ -127,7 +129,7 @@ export function narrate(state: State, start?: Cell[]): Telling[] {
state.moves.forEach((move, n) => { state.moves.forEach((move, n) => {
const { lines, after } = tellOne(cells, move, owner); const { lines, after } = tellOne(cells, move, owner);
cells = after; cells = after;
out.push({ ply: n + 1, move, lines }); out.push({ ply: n + 1, move, lines, after });
}); });
if (state.over && out.length) out[out.length - 1].lines.push({ kind: 'end', text: state.over.reason }); if (state.over && out.length) out[out.length - 1].lines.push({ kind: 'end', text: state.over.reason });
return out; return out;