Files
wyrm/src/lib/game/chronicle.ts
T

142 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, PER_LEVEL, NAMES, frozen, kindOf, levelOf, opening, other, playOn, rankOf, sideOf, squareName } from './board';
import { playerName, type Move, type 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[];
/** 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' };
/** 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 '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}`;
}
}
/** 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;
}
/** Whose a piece is, as a possessive: "Aud’s", "Scarlet’s", or "Your" at a lesson. */
export type Owner = (side: Side) => string;
/** Tell one move from the position it was made in. */
function tellOne(before: Cell[], move: Move, owner: Owner): { lines: Line[]; after: Cell[] } {
const who = owner(move.by);
const foe = owner(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 || 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) {
// The same piece, still there and no longer held; a piece that has just arrived is another story.
if (heldAfter.has(i) || after[i] === '' || after[i] !== before[i] || i === move.from || i === move.to) 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.` });
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 owner: Owner = (side) => `${playerName(state, side)}’s`;
let cells = start ?? opening(state.setup);
const out: Telling[] = [];
state.moves.forEach((move, n) => {
const { lines, after } = tellOne(cells, move, owner);
cells = after;
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 });
return out;
}
/** One move told on its own, from the position it was made in. */
export function tell(before: Cell[], move: Move, owner: Owner): string {
return tellOne(before, move, owner).lines.map((l) => l.text).join(' ');
}