The chronicle tells each move as the table saw it, oldest first: the Griffon leaps, the Dwarf trudges, a gaze falls, a King steps off the land, and who lost what
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
4212bf9419
commit
04831ed2aa
6 files changed
+310
-66
No files matched your search
@@ -5,9 +5,11 @@
|
||||
// from being shown on the boards, and the talk.
|
||||
import TableTalk from './TableTalk.svelte';
|
||||
import PiecePanel from './PiecePanel.svelte';
|
||||
import Chronicle from './Chronicle.svelte';
|
||||
import { narrate } from '$lib/game/chronicle';
|
||||
import BoardArtwork, { type Token } from './BoardArtwork.svelte';
|
||||
import {
|
||||
describe, legalMovesFrom, movesFrom, squareName, sideOf, kindOf, kingSquare, inCheck, levelOf, fileOf, rankOf, idx, frozen, notation, NAMES, FILES, RANKS, LEVELS,
|
||||
legalMovesFrom, movesFrom, squareName, sideOf, kindOf, kingSquare, inCheck, levelOf, fileOf, rankOf, idx, frozen, NAMES, FILES, RANKS, LEVELS,
|
||||
type Move, type Side
|
||||
} from '$lib/game';
|
||||
import type { Room } from '$lib/net/room.svelte';
|
||||
@@ -92,9 +94,11 @@
|
||||
const shownSquares = $derived(shownLast ? (shownLast.afar ? [shownLast.from] : [shownLast.from, shownLast.to]) : []);
|
||||
const shownCaptured = $derived(shownLast?.captured ? [shownLast.to] : []);
|
||||
|
||||
/** The game told move by move, replayed from the ledger's moves. */
|
||||
const tellings = $derived(narrate(g));
|
||||
function storyOf(ply: number): string {
|
||||
const m = g.moves[ply - 1];
|
||||
return m ? describe(g, m) : '';
|
||||
const t = tellings[ply - 1];
|
||||
return t ? t.lines.map((l) => l.text).join(' ') : '';
|
||||
}
|
||||
const shownStory = $derived(shownPly > 0 ? storyOf(shownPly) : '');
|
||||
|
||||
@@ -256,20 +260,9 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<p class="muted small">Move {g.moves.length + 1}.{#if g.moves.length} Click a move to see it on the boards.{/if}</p>
|
||||
{#if g.moves.length}
|
||||
<ol class="record muted small" reversed start={g.moves.length}>
|
||||
{#each g.moves.slice(-10).reverse() as m, i (g.moves.length - i)}
|
||||
{@const ply = g.moves.length - i}
|
||||
<li class:mine={mySide === m.by} class:reviewed={reviewing === ply}>
|
||||
<button type="button" class="link plain" onclick={() => (reviewing = reviewing === ply ? null : ply)}>
|
||||
<span class="who">{nameOfSide(m.by)}</span> <span class="notation">{notation(m)}</span>{#if m.captured}, <strong>takes the {NAMES[m.captured]}</strong>{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
<p class="muted small">Move {g.moves.length + 1}.</p>
|
||||
</section>
|
||||
<Chronicle {tellings} {mySide} {reviewing} onReview={(ply) => (reviewing = ply)} />
|
||||
<TableTalk {room} />
|
||||
</aside>
|
||||
</main>
|
||||
@@ -435,34 +428,4 @@
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.record {
|
||||
margin: 0.3rem 0 0;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.record li.mine {
|
||||
color: var(--bone);
|
||||
}
|
||||
|
||||
.record .plain {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.record .plain:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.record li.reviewed .link {
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
.record .notation {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.record strong {
|
||||
color: var(--ember);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
// 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
|
||||
// shows that move on the boards.
|
||||
import { tick } from 'svelte';
|
||||
import type { Telling } from '$lib/game/chronicle';
|
||||
import { notation, type Side } from '$lib/game';
|
||||
|
||||
let { tellings, mySide, reviewing, onReview }: { tellings: Telling[]; mySide: Side | null; reviewing: number | null; onReview: (ply: number | null) => void } = $props();
|
||||
|
||||
let box = $state<HTMLElement | null>(null);
|
||||
$effect(() => {
|
||||
void tellings.length;
|
||||
void tick().then(() => {
|
||||
if (box && reviewing === null) box.scrollTop = box.scrollHeight;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="chronicle" bind:this={box}>
|
||||
<h2>Chronicle</h2>
|
||||
{#if tellings.length === 0}
|
||||
<p class="muted">Nothing has happened yet. The boards wait on Gold.</p>
|
||||
{/if}
|
||||
{#each tellings as t (t.ply)}
|
||||
<article class:mine={mySide === t.move.by} class:reviewed={reviewing === t.ply}>
|
||||
<h3>
|
||||
<button type="button" class="link plain" title="Show this move on the boards" onclick={() => onReview(reviewing === t.ply ? null : t.ply)}>
|
||||
Move {t.ply} <span class="notation">{notation(t.move)}</span>
|
||||
</button>
|
||||
</h3>
|
||||
<ul>
|
||||
{#each t.lines as l, i (i)}
|
||||
<li class={l.kind}>{l.text}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.chronicle {
|
||||
padding: 0.75rem 0;
|
||||
max-height: 44vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
font-style: italic;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 0.8rem;
|
||||
color: var(--bone-faint);
|
||||
margin: 0.6rem 0 0.2rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h3 .plain {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
h3 .plain:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notation {
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-left: 0.3rem;
|
||||
}
|
||||
|
||||
article.reviewed h3 .plain {
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
li.move {
|
||||
color: var(--bone-dim);
|
||||
}
|
||||
|
||||
article.mine li.move {
|
||||
color: var(--bone);
|
||||
}
|
||||
|
||||
li.capture {
|
||||
color: var(--ember);
|
||||
}
|
||||
|
||||
li.check,
|
||||
li.freeze,
|
||||
li.promotion {
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
li.note {
|
||||
color: var(--bone-dim);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
li.end {
|
||||
color: var(--blood);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
// The chronicle: every move told as the table saw it, with what followed
|
||||
// from it. A Griffon leaps and a Dwarf trudges, a capture names who lost
|
||||
// what, a Basilisk's gaze falls on whatever stops above it, and a King that
|
||||
// steps off the land is told what it has done. The lines are read off a
|
||||
// replay of the moves, so the chronicle is a function of the game and
|
||||
// never part of the ledger: rewording it changes no rules.
|
||||
|
||||
import {
|
||||
type Cell, type Kind, type Side, CELLS, PER_LEVEL, NAMES, frozen, kindOf, levelOf, opening, other, playOn, rankOf, sideOf, squareName
|
||||
} from './board';
|
||||
import type { Move, State } from './index';
|
||||
|
||||
export type LineKind = 'move' | 'capture' | 'check' | 'freeze' | 'promotion' | 'end' | 'note';
|
||||
|
||||
export interface Line {
|
||||
kind: LineKind;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface Telling {
|
||||
/** The move's number, from one. */
|
||||
ply: number;
|
||||
move: Move;
|
||||
lines: Line[];
|
||||
}
|
||||
|
||||
const WORLD: Record<number, string> = { 3: 'the air', 2: 'the land', 1: 'the underworld' };
|
||||
|
||||
/** How each piece goes when it simply moves on its own board. */
|
||||
const GOES: Record<Kind, string> = {
|
||||
S: 'drifts', G: 'leaps', R: 'glides', W: 'steps', O: 'lumbers', U: 'springs', T: 'slips', H: 'bounds',
|
||||
C: 'walks', M: 'sweeps', K: 'steps', P: 'rides', D: 'trudges', B: 'slithers', E: 'grinds'
|
||||
};
|
||||
|
||||
/** The word for a piece that changes boards, going up or down. */
|
||||
function crossing(kind: Kind, from: number, to: number): string {
|
||||
const up = levelOf(to) > levelOf(from);
|
||||
if (kind === 'G') return up ? 'takes wing' : 'swoops down';
|
||||
if (kind === 'P') return up ? 'vaults up' : 'vaults down';
|
||||
if (kind === 'H') return up ? 'springs up' : 'springs down';
|
||||
if (kind === 'M') return up ? 'ascends' : 'descends';
|
||||
if (kind === 'D') return up ? 'bursts up' : 'drops back';
|
||||
if (kind === 'E') return up ? 'heaves itself up' : 'sinks';
|
||||
if (kind === 'S') return up ? 'rises' : 'dives';
|
||||
return up ? 'climbs' : 'drops';
|
||||
}
|
||||
|
||||
/** How a victim is told. */
|
||||
function falls(victim: Kind, owner: string, where: string): string {
|
||||
switch (victim) {
|
||||
case 'K': return `${owner} King is taken`;
|
||||
case 'R': return `${owner} Dragon falls from the sky at ${where}`;
|
||||
case 'B': return `${owner} Basilisk is crushed at ${where}, and its gaze is broken`;
|
||||
case 'S': return `${owner} Sylph is scattered at ${where}`;
|
||||
case 'E': return `${owner} Elemental is shattered at ${where}`;
|
||||
default: return `${owner} ${NAMES[victim]} falls at ${where}`;
|
||||
}
|
||||
}
|
||||
|
||||
const possessive = (name: string) => `${name}’s`;
|
||||
|
||||
/** The pieces on the land held frozen, by square. */
|
||||
function frozenSquares(cells: Cell[]): Set<number> {
|
||||
const held = new Set<number>();
|
||||
for (let i = PER_LEVEL; i < PER_LEVEL * 2; i++) if (cells[i] !== '' && frozen(cells, i)) held.add(i);
|
||||
return held;
|
||||
}
|
||||
|
||||
/** Tell one move from the position it was made in. */
|
||||
function tellOne(before: Cell[], move: Move, ply: number, name: (side: Side) => string): { lines: Line[]; after: Cell[] } {
|
||||
const who = possessive(name(move.by));
|
||||
const foe = possessive(name(other(move.by)));
|
||||
const piece = NAMES[move.kind];
|
||||
const from = squareName(move.from);
|
||||
const to = squareName(move.to);
|
||||
const played = playOn(before, move.from, move.to);
|
||||
const after = played.cells;
|
||||
const lines: Line[] = [];
|
||||
|
||||
if (move.afar) {
|
||||
lines.push({ kind: 'capture', text: `${who} Dragon strikes down from ${from} without leaving the air, and ${falls(move.captured!, foe, to)}.` });
|
||||
} else if (levelOf(move.from) !== levelOf(move.to)) {
|
||||
const verb = crossing(move.kind, move.from, move.to);
|
||||
if (move.captured) lines.push({ kind: 'capture', text: `${who} ${piece} ${verb} from ${from} onto ${to}, and ${falls(move.captured, foe, to)}.` });
|
||||
else lines.push({ kind: 'move', text: `${who} ${piece} ${verb} from ${from} to ${to}, into ${WORLD[levelOf(move.to)]}.` });
|
||||
} else if (move.captured) {
|
||||
lines.push({ kind: 'capture', text: `${who} ${piece} ${GOES[move.kind]} from ${from} to ${to}, and ${falls(move.captured, foe, to)}.` });
|
||||
} else {
|
||||
lines.push({ kind: 'move', text: `${who} ${piece} ${GOES[move.kind]} from ${from} to ${to}.` });
|
||||
}
|
||||
|
||||
if (move.promoted) lines.push({ kind: 'promotion', text: `On the far rank the Warrior stands up a Hero.` });
|
||||
|
||||
// What the move did to the land beneath and above: a gaze that falls, a gaze that lifts.
|
||||
const heldBefore = frozenSquares(before);
|
||||
const heldAfter = frozenSquares(after);
|
||||
for (const i of heldAfter) {
|
||||
if (heldBefore.has(i) && !(move.afar === false && i === move.to)) continue;
|
||||
const owner = sideOf(after[i]) === move.by ? who : foe;
|
||||
lines.push({ kind: 'freeze', text: `The Basilisk’s gaze falls on ${owner} ${NAMES[kindOf(after[i])]} at ${squareName(i)}, which cannot stir.` });
|
||||
}
|
||||
for (const i of heldBefore) {
|
||||
if (heldAfter.has(i) || after[i] === '' || i === move.from) continue;
|
||||
const owner = sideOf(after[i]) === move.by ? who : foe;
|
||||
lines.push({ kind: 'note', text: `${owner} ${NAMES[kindOf(after[i])]} at ${squareName(i)} is free to move again.` });
|
||||
}
|
||||
|
||||
// A piece that has put itself somewhere it can do little.
|
||||
const landed = after[move.to];
|
||||
if (landed !== '' && !move.afar) {
|
||||
const k = kindOf(landed);
|
||||
const lv = levelOf(move.to);
|
||||
if (k === 'K' && lv !== 2) lines.push({ kind: 'note', text: `Off the land the King can do nothing but come back.` });
|
||||
if (k === 'S' && lv === 2) lines.push({ kind: 'note', text: `Grounded, the Sylph can only rise again.` });
|
||||
if (k === 'E' && lv === 2) lines.push({ kind: 'note', text: `On the land the Elemental is a rock until it sinks again.` });
|
||||
if (k === 'W' && !move.promoted && rankOf(move.to) === (move.by === 'gold' ? 6 : 1)) lines.push({ kind: 'note', text: `One more step and it is a Hero.` });
|
||||
}
|
||||
|
||||
if (move.check) lines.push({ kind: 'check', text: `${foe} King is in check.` });
|
||||
void ply;
|
||||
return { lines, after };
|
||||
}
|
||||
|
||||
/** The whole game told, one entry a move, from a replay of the moves. A game begun from the opening needs no `start`. */
|
||||
export function narrate(state: State, start?: Cell[]): Telling[] {
|
||||
const name = (side: Side) => {
|
||||
const seat = state.seats.find((id) => state.players[id].side === side);
|
||||
return seat ? state.players[seat].name : side === 'gold' ? 'Gold' : 'Scarlet';
|
||||
};
|
||||
let cells = start ?? opening(state.setup);
|
||||
const out: Telling[] = [];
|
||||
state.moves.forEach((move, n) => {
|
||||
const { lines, after } = tellOne(cells, move, n + 1, name);
|
||||
cells = after;
|
||||
out.push({ ply: n + 1, move, lines });
|
||||
});
|
||||
if (state.over && out.length) out[out.length - 1].lines.push({ kind: 'end', text: state.over.reason });
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One move told on its own, from the position it was made in. */
|
||||
export function tell(before: Cell[], move: Move, name: (side: Side) => string): string {
|
||||
return tellOne(before, move, 0, name).lines.map((l) => l.text).join(' ');
|
||||
}
|
||||
|
||||
export { CELLS };
|
||||
@@ -301,17 +301,17 @@ describe('check and the ends', () => {
|
||||
const after = resolveRound(s, { A: { from: square('2c2'), to: square('2c3') } });
|
||||
expect(after.toMove).toBe('scarlet');
|
||||
expect(after.moves).toHaveLength(1);
|
||||
expect(tell(after, after.moves[0])).toBe('Aud’s Warrior goes from 2c2 to 2c3.');
|
||||
expect(tell(after, after.moves[0])).toBe('Aud’s Warrior steps from 2c2 to 2c3.');
|
||||
expect(notation(after.moves[0])).toBe('W/2c2-2c3');
|
||||
});
|
||||
|
||||
it('tells a capture, a climb and a strike from afar', () => {
|
||||
const s = position({ '3c4': 'gR', '2c4': 'sW', '2e4': 'gH' });
|
||||
const afar = applyMove(s, square('3c4'), square('2c4'));
|
||||
expect(tell(afar, afar.moves[0])).toBe('Aud’s Dragon strikes down from 3c4 and takes the Warrior at 2c4.');
|
||||
expect(tell(afar, afar.moves[0], s.cells)).toBe('Aud’s Dragon strikes down from 3c4 without leaving the air, and Bjorn\u2019s Warrior falls at 2c4.');
|
||||
expect(notation(afar.moves[0])).toBe('Drx2c4');
|
||||
const climb = applyMove(s, square('2e4'), square('3d5'));
|
||||
expect(tell(climb, climb.moves[0])).toBe('Aud’s Hero climbs from 2e4 to 3d5, in the air.');
|
||||
expect(tell(climb, climb.moves[0], s.cells)).toBe('Aud’s Hero springs up from 2e4 to 3d5, into the air.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -349,3 +349,30 @@ describe('the drake', () => {
|
||||
expect(s.over).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the chronicle', () => {
|
||||
it('tells a Basilisk’s gaze falling and lifting, a Warrior standing up a Hero, and a King leaving the land', async () => {
|
||||
const { narrate } = await import('./chronicle');
|
||||
const s = position({ '1c3': 'gB', '2d4': 'sT', '2a7': 'gW', '2g1': 'gK' });
|
||||
s.cells[square('2l1')] = '';
|
||||
const frozen = applyMove(s, square('1c3'), square('1d4'));
|
||||
expect(narrate(frozen, s.cells)[0].lines.map((l) => l.text)).toEqual([
|
||||
'Aud’s Basilisk slithers from 1c3 to 1d4.',
|
||||
'The Basilisk’s gaze falls on Bjorn’s Thief at 2d4, which cannot stir.'
|
||||
]);
|
||||
const freed = applyMove(applyMove(frozen, square('2a8'), square('2b7')), square('1d4'), square('1d5'));
|
||||
expect(narrate(freed, s.cells)[2].lines.map((l) => l.text)).toEqual([
|
||||
'Aud’s Basilisk slithers from 1d4 to 1d5.',
|
||||
'Bjorn’s Thief at 2d4 is free to move again.'
|
||||
]);
|
||||
const hero = applyMove(applyMove(freed, square('2b7'), square('2a8')), square('2a7'), square('2a8'));
|
||||
const told = narrate(hero, s.cells)[4].lines;
|
||||
expect(told[0].text).toBe('Aud’s Warrior steps from 2a7 to 2a8, and Bjorn’s King is taken.');
|
||||
expect(told.some((l) => l.kind === 'promotion')).toBe(true);
|
||||
const up = applyMove(s, square('2g1'), square('3g1'));
|
||||
expect(narrate(up, s.cells)[0].lines.map((l) => l.text)).toEqual([
|
||||
'Aud’s King climbs from 2g1 to 3g1, into the air.',
|
||||
'Off the land the King can do nothing but come back.'
|
||||
]);
|
||||
});
|
||||
});
|
||||
+6
-16
@@ -11,6 +11,7 @@ import {
|
||||
} from './board';
|
||||
export { FILES, RANKS, LEVELS, PER_LEVEL, fileOf, rankOf, levelOf, idx, isLight, kindOf, sideOf, frozen, kingSquare, inCheck, other, square } from './board';
|
||||
import { chooseMove } from './bot';
|
||||
import { narrate } from './chronicle';
|
||||
|
||||
export const CURRENT_RULES = 1;
|
||||
|
||||
@@ -154,22 +155,11 @@ export function resolveRound(previous: State, inputs: Record<SeatId, Input>): St
|
||||
return applyMove(previous, input.from, input.to);
|
||||
}
|
||||
|
||||
/** The move told as the chronicle tells it. */
|
||||
export function describe(state: State, move: Move): string {
|
||||
const who = playerName(state, move.by);
|
||||
const piece = NAMES[move.kind];
|
||||
const level = (i: number) => (levelOf(i) === 3 ? 'in the air' : levelOf(i) === 1 ? 'underground' : 'on the land');
|
||||
let text: string;
|
||||
if (move.afar) text = `${who}’s Dragon strikes down from ${squareName(move.from)} and takes the ${NAMES[move.captured!]} at ${squareName(move.to)}.`;
|
||||
else if (move.captured) {
|
||||
const crossing = levelOf(move.from) !== levelOf(move.to);
|
||||
text = `${who}’s ${piece} ${crossing ? (levelOf(move.to) > levelOf(move.from) ? 'climbs' : 'drops') : 'goes'} from ${squareName(move.from)} to ${squareName(move.to)} and takes the ${NAMES[move.captured]}.`;
|
||||
} else if (levelOf(move.from) !== levelOf(move.to)) {
|
||||
text = `${who}’s ${piece} ${levelOf(move.to) > levelOf(move.from) ? 'climbs' : 'drops'} from ${squareName(move.from)} to ${squareName(move.to)}, ${level(move.to)}.`;
|
||||
} else text = `${who}’s ${piece} goes from ${squareName(move.from)} to ${squareName(move.to)}.`;
|
||||
if (move.promoted) text += ` It reaches the far rank and stands up a Hero.`;
|
||||
if (move.check) text += ' Check.';
|
||||
return text;
|
||||
/** The move told as the chronicle tells it, with what came of it. */
|
||||
export function describe(state: State, move: Move, start?: Cell[]): string {
|
||||
const n = state.moves.indexOf(move);
|
||||
const telling = narrate(state, start)[n];
|
||||
return telling ? telling.lines.map((l) => l.text).join(' ') : '';
|
||||
}
|
||||
|
||||
/** The move in Gygax's notation: piece, from, x, to, with (H), "afar" and ch as he wrote them. */
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<p>The three boards stand one above the other, the air at the top, the land in the middle and the underworld at the bottom, in the colours Gygax suggested. Your side is nearest you on every board. Each piece is a disc bearing the initial Gygax gave it, with Dr for the Dragon so it is not mistaken for a Dwarf. The square names along the edges are his too, and you can hide them under <em>preferences</em>.</p>
|
||||
<figure><img src="/guide/panel.png" alt="The panel for Scarlet's Dragon: where it stands and what it can take, what it is, how it moves, why it is in the game, and how to play it" loading="lazy" /></figure>
|
||||
<p>Tap any piece, yours or the other side's, and a panel opens beside the boards, or rises from the foot of the screen on a phone, saying what the piece is, where it stands and what it can reach from there, how it moves, why Gygax put it in the game, and how to play it. Dotted squares show where a piece that is not yours to move could go. Tap the piece again, or the cross, to close it.</p>
|
||||
<p>The strip above the boards says whose move it is and warns when a King is in check. The record beside the boards lists the moves played in Gygax's notation. Click one to see it on the boards, and click again, or make your move, to come back to now.</p>
|
||||
<p>The strip above the boards says whose move it is and warns when a King is in check. The chronicle beside the boards tells each move and what came of it, who took what, whose gaze fell on whom, and when a King is in check, with Gygax's notation beside each move. Click a move to see it on the boards, and click again, or make your move, to come back to now.</p>
|
||||
</section>
|
||||
|
||||
<section id="moving">
|
||||
|
||||
Reference in new issue
Block a user