// The three boards and how every piece moves on them, on a bare array of // cells so the engine, the bot and the board drawing share one source. The // rules are docs/source.md, Gygax's text, read as its "Decisions" section // says: board 3 is the air, 2 the land, 1 the underworld. export type Side = 'gold' | 'scarlet'; /** One letter a piece: Gygax's initials, with R for the Dragon (he wrote Dr). */ export type Kind = 'S' | 'G' | 'R' | 'W' | 'O' | 'U' | 'H' | 'T' | 'C' | 'M' | 'K' | 'P' | 'D' | 'B' | 'E'; /** A cell holds '' or a side letter and a kind: 'gK' is Gold's King, 'sR' Scarlet's Dragon. */ export type Cell = string; export type Setup = 'printed' | 'shifted'; export const FILES = 12; export const RANKS = 8; export const LEVELS = 3; export const PER_LEVEL = FILES * RANKS; export const CELLS = PER_LEVEL * LEVELS; export const KINDS: Kind[] = ['S', 'G', 'R', 'W', 'O', 'U', 'H', 'T', 'C', 'M', 'K', 'P', 'D', 'B', 'E']; export const NAMES: Record = { S: 'Sylph', G: 'Griffon', R: 'Dragon', W: 'Warrior', O: 'Oliphant', U: 'Unicorn', H: 'Hero', T: 'Thief', C: 'Cleric', M: 'Mage', K: 'King', P: 'Paladin', D: 'Dwarf', B: 'Basilisk', E: 'Elemental' }; /** The letters the pieces are written with: Gygax's initials, and Dr for the Dragon. */ export const LETTERS: Record = { S: 'S', G: 'G', R: 'Dr', W: 'W', O: 'O', U: 'U', H: 'H', T: 'T', C: 'C', M: 'M', K: 'K', P: 'P', D: 'D', B: 'B', E: 'E' }; /** Gygax's values. He gives the King none: 0 here, and the panel says "beyond price". */ export const VALUES: Record = { K: 0, M: 11, P: 10, C: 9, R: 8, G: 5, O: 5, H: 4.5, T: 4, E: 4, B: 3, U: 2.5, D: 2, S: 1, W: 1 }; export const SIDES: Side[] = ['gold', 'scarlet']; export const SIDE_NAMES: Record = { gold: 'Gold', scarlet: 'Scarlet' }; export const other = (side: Side): Side => (side === 'gold' ? 'scarlet' : 'gold'); /** A cell index from level 1 to 3, file 0 to 11 and rank 0 to 7. */ export const idx = (level: number, file: number, rank: number): number => (level - 1) * PER_LEVEL + rank * FILES + file; export const levelOf = (i: number): number => Math.floor(i / PER_LEVEL) + 1; export const fileOf = (i: number): number => i % FILES; export const rankOf = (i: number): number => Math.floor((i % PER_LEVEL) / FILES); const inBounds = (file: number, rank: number): boolean => file >= 0 && file < FILES && rank >= 0 && rank < RANKS; /** The square written as Gygax writes it: 3c4 is the air, file c, rank 4. */ export function squareName(i: number): string { return `${levelOf(i)}${'abcdefghijkl'[fileOf(i)]}${rankOf(i) + 1}`; } /** The index of a square written as Gygax writes it. */ export function square(name: string): number { const level = Number(name[0]); const file = 'abcdefghijkl'.indexOf(name[1]); const rank = Number(name.slice(2)) - 1; if (!(level >= 1 && level <= 3) || file < 0 || !(rank >= 0 && rank < RANKS)) throw new Error(`no square ${name}`); return idx(level, file, rank); } /** Light squares: a light square sits in Gold's near right corner, l1. */ export const isLight = (i: number): boolean => (fileOf(i) + rankOf(i)) % 2 === 1; export const sideOf = (cell: Cell): Side | null => (cell === '' ? null : cell[0] === 'g' ? 'gold' : 'scarlet'); export const kindOf = (cell: Cell): Kind => cell[1] as Kind; export const cellOf = (side: Side, kind: Kind): Cell => (side === 'gold' ? 'g' : 's') + kind; /** Which way is forward: Gold climbs the ranks, Scarlet descends them. */ export const forward = (side: Side): number => (side === 'gold' ? 1 : -1); /** The home files of the Sylphs: as printed, or shifted onto the Dwarves' files. */ export const sylphFiles = (setup: Setup): number[] => (setup === 'shifted' ? [1, 3, 5, 7, 9, 11] : [0, 2, 4, 6, 8, 10]); /** The opening position. */ export function opening(setup: Setup = 'printed'): Cell[] { const cells: Cell[] = new Array(CELLS).fill(''); const backRank: Kind[] = ['O', 'U', 'H', 'T', 'C', 'M', 'K', 'P', 'T', 'H', 'U', 'O']; for (const side of SIDES) { const home = side === 'gold' ? 0 : RANKS - 1; const second = side === 'gold' ? 1 : RANKS - 2; for (let f = 0; f < FILES; f++) { cells[idx(2, f, home)] = cellOf(side, backRank[f]); cells[idx(2, f, second)] = cellOf(side, 'W'); } for (const f of sylphFiles(setup)) cells[idx(3, f, second)] = cellOf(side, 'S'); cells[idx(3, 2, home)] = cellOf(side, 'G'); cells[idx(3, 10, home)] = cellOf(side, 'G'); cells[idx(3, 6, home)] = cellOf(side, 'R'); for (const f of [1, 3, 5, 7, 9, 11]) cells[idx(1, f, second)] = cellOf(side, 'D'); cells[idx(1, 2, home)] = cellOf(side, 'B'); cells[idx(1, 10, home)] = cellOf(side, 'B'); cells[idx(1, 6, home)] = cellOf(side, 'E'); } return cells; } /** A piece on the middle board with an enemy Basilisk beneath it cannot move, capture or give check. */ export function frozen(cells: Cell[], i: number): boolean { if (levelOf(i) !== 2) return false; const below = cells[i - PER_LEVEL]; return below !== '' && kindOf(below) === 'B' && sideOf(below) !== sideOf(cells[i]); } const KNIGHT = [[1, 2], [2, 1], [-1, 2], [-2, 1], [1, -2], [2, -1], [-1, -2], [-2, -1]]; const GRIFFON = [[2, 3], [3, 2], [-2, 3], [-3, 2], [2, -3], [3, -2], [-2, -3], [-3, -2]]; const ORTHOGONAL = [[1, 0], [-1, 0], [0, 1], [0, -1]]; const DIAGONAL = [[1, 1], [1, -1], [-1, 1], [-1, -1]]; const AROUND = [...ORTHOGONAL, ...DIAGONAL]; /** * Every square the piece on `from` may move to or capture on, by the rules * alone: its own King may be left in check. The Dragon's capture from afar * is a move to the middle-board square it empties. A frozen piece has none. */ export function movesFrom(cells: Cell[], from: number, setup: Setup = 'printed'): number[] { const cell = cells[from]; if (cell === '' || frozen(cells, from)) return []; const side = sideOf(cell)!; const kind = kindOf(cell); const level = levelOf(from); const f = fileOf(from); const r = rankOf(from); const fw = forward(side); const out: number[] = []; const empty = (i: number) => cells[i] === ''; const enemy = (i: number) => cells[i] !== '' && sideOf(cells[i]) !== side; const open = (i: number) => cells[i] === '' || sideOf(cells[i]) !== side; /** Add the square if it is on the board and passes the test. */ const add = (lv: number, file: number, rank: number, ok: (i: number) => boolean): void => { if (lv < 1 || lv > LEVELS || !inBounds(file, rank)) return; const i = idx(lv, file, rank); if (ok(i)) out.push(i); }; /** Slide until something is in the way; a piece of the other side may be taken. */ const slide = (dirs: number[][]) => { for (const [df, dr] of dirs) { let file = f + df; let rank = r + dr; while (inBounds(file, rank)) { const i = idx(level, file, rank); if (cells[i] === '') out.push(i); else { if (sideOf(cells[i]) !== side) out.push(i); break; } file += df; rank += dr; } } }; const step = (dirs: number[][], lv = level, ok = open) => { for (const [df, dr] of dirs) add(lv, f + df, r + dr, ok); }; const lastRank = side === 'gold' ? RANKS - 1 : 0; switch (kind) { case 'S': if (level === 3) { add(3, f - 1, r + fw, empty); add(3, f + 1, r + fw, empty); add(3, f, r + fw, enemy); add(2, f, r, enemy); } else if (level === 2) { add(3, f, r, empty); const homeRank = side === 'gold' ? 1 : RANKS - 2; for (const file of sylphFiles(setup)) add(3, file, homeRank, empty); } break; case 'G': if (level === 3) { step(GRIFFON); step(DIAGONAL, 2); } else if (level === 2) { step(DIAGONAL); step(DIAGONAL, 3); } break; case 'R': if (level === 3) { slide(DIAGONAL); step(ORTHOGONAL); add(2, f, r, enemy); step(ORTHOGONAL, 2, enemy); } break; case 'W': if (level === 2) { add(2, f, r + fw, empty); add(2, f - 1, r + fw, enemy); add(2, f + 1, r + fw, enemy); } break; case 'O': if (level === 2) slide(ORTHOGONAL); break; case 'U': if (level === 2) step(KNIGHT); break; case 'T': if (level === 2) slide(DIAGONAL); break; case 'H': if (level === 2) { step(DIAGONAL); step(DIAGONAL.map(([df, dr]) => [df * 2, dr * 2])); step(DIAGONAL, 1); step(DIAGONAL, 3); } else step(DIAGONAL, 2); break; case 'C': step(AROUND); add(level + 1, f, r, open); add(level - 1, f, r, open); break; case 'M': if (level === 2) { slide(ORTHOGONAL); slide(DIAGONAL); add(3, f, r, open); add(1, f, r, open); } else { step(ORTHOGONAL); add(2, f, r, open); if (cells[idx(2, f, r)] === '') add(level === 3 ? 1 : 3, f, r, open); } break; case 'K': if (level === 2) { step(AROUND); add(3, f, r, open); add(1, f, r, open); } else add(2, f, r, open); break; case 'P': step(AROUND); if (level === 2) { step(KNIGHT); for (const lv of [1, 3]) step(ORTHOGONAL.map(([df, dr]) => [df * 2, dr * 2]), lv); } else { step(ORTHOGONAL.map(([df, dr]) => [df * 2, dr * 2]), 2); step(ORTHOGONAL, level === 3 ? 1 : 3); } break; case 'D': if (level === 1 || level === 2) { if (r !== lastRank) { add(level, f, r + fw, empty); add(level, f - 1, r + fw, enemy); add(level, f + 1, r + fw, enemy); } add(level, f - 1, r, empty); add(level, f + 1, r, empty); if (level === 1) add(2, f, r, enemy); else add(1, f, r, empty); } break; case 'B': if (level === 1) { add(1, f, r + fw, open); add(1, f - 1, r + fw, open); add(1, f + 1, r + fw, open); add(1, f, r - fw, empty); } break; case 'E': if (level === 1) { for (const [df, dr] of ORTHOGONAL) { if (!inBounds(f + df, r + dr)) continue; const near = idx(1, f + df, r + dr); if (open(near)) out.push(near); if (cells[near] === '') { add(1, f + df * 2, r + dr * 2, open); add(2, f + df, r + dr, enemy); } } step(DIAGONAL, 1, empty); } else if (level === 2 && cells[idx(1, f, r)] === '') step(ORTHOGONAL, 1); break; } return out; } /** Whether `side` attacks square `i` in this position: one of its pieces could capture there now. */ export function attacked(cells: Cell[], i: number, by: Side): boolean { for (let from = 0; from < CELLS; from++) { const cell = cells[from]; if (cell === '' || sideOf(cell) !== by) continue; if (movesFrom(cells, from).includes(i)) return true; } return false; } export function kingSquare(cells: Cell[], side: Side): number { const king = cellOf(side, 'K'); for (let i = 0; i < CELLS; i++) if (cells[i] === king) return i; return -1; } export function inCheck(cells: Cell[], side: Side): boolean { const k = kingSquare(cells, side); return k >= 0 && attacked(cells, k, other(side)); } export interface Played { cells: Cell[]; captured: Kind | null; promoted: boolean; /** The Dragon's capture from afar: the Dragon stays and the square it struck is emptied. */ afar: boolean; } /** The position after the move, on a copy. The move is taken to be one `movesFrom` offered. */ export function playOn(cells: Cell[], from: number, to: number): Played { const next = cells.slice(); const piece = cells[from]; const side = sideOf(piece)!; const kind = kindOf(piece); const captured = cells[to] === '' ? null : kindOf(cells[to]); if (kind === 'R' && levelOf(to) === 2) { next[to] = ''; return { cells: next, captured, promoted: false, afar: true }; } next[from] = ''; const promoted = kind === 'W' && rankOf(to) === (side === 'gold' ? RANKS - 1 : 0); next[to] = promoted ? cellOf(side, 'H') : piece; return { cells: next, captured, promoted, afar: false }; } /** The moves of the piece on `from` that leave its own King out of check. */ export function legalMovesFrom(cells: Cell[], from: number, setup: Setup = 'printed'): number[] { const side = sideOf(cells[from]); if (!side) return []; return movesFrom(cells, from, setup).filter((to) => !inCheck(playOn(cells, from, to).cells, side)); } /** Every legal move for a side, as [from, to] pairs. */ export function legalMoves(cells: Cell[], side: Side, setup: Setup = 'printed'): [number, number][] { const out: [number, number][] = []; for (let from = 0; from < CELLS; from++) { if (cells[from] === '' || sideOf(cells[from]) !== side) continue; for (const to of legalMovesFrom(cells, from, setup)) out.push([from, to]); } return out; } /** Whether a side has any legal move at all, found without listing them. */ export function hasLegalMove(cells: Cell[], side: Side, setup: Setup = 'printed'): boolean { for (let from = 0; from < CELLS; from++) { if (cells[from] === '' || sideOf(cells[from]) !== side) continue; for (const to of movesFrom(cells, from, setup)) if (!inCheck(playOn(cells, from, to).cells, side)) return true; } return false; } /** The pieces a side has left, by kind. */ export function census(cells: Cell[], side: Side): Partial> { const count: Partial> = {}; for (const cell of cells) if (cell !== '' && sideOf(cell) === side) count[kindOf(cell)] = (count[kindOf(cell)] ?? 0) + 1; return count; }