diff --git a/docs/compose-game.ts b/docs/compose-game.ts new file mode 100644 index 0000000..66c5be4 --- /dev/null +++ b/docs/compose-game.ts @@ -0,0 +1,36 @@ +// Composes the told game (src/lib/game/told.ts): its moves are played at +// their plies and the drake plays any ply beyond them, so a longer game can +// be sketched and steered. Prints the chronicle, and with a ply and a +// square, the legal moves of the piece there at that ply. +// npx tsx docs/compose-game.ts [plies] [ply square] +import { createGame, applyMove, validateMove } from '../src/lib/game/index.ts'; +import { chooseMove } from '../src/lib/game/bot.ts'; +import { narrate } from '../src/lib/game/chronicle.ts'; +import { square, squareName, legalMovesFrom, type Side } from '../src/lib/game/board.ts'; +import { TOLD_MOVES } from '../src/lib/game/told.ts'; +const FORCED: Record = Object.fromEntries(TOLD_MOVES.map((m, n) => [n + 1, m])); + +const plies = Number(process.argv[2] ?? 40); +const askPly = process.argv[3] ? Number(process.argv[3]) : 0; +const askSquare = process.argv[4]; +let s = createGame({ A: 'Gold', B: 'Scarlet' }); +for (let ply = 1; ply <= plies && !s.over; ply++) { + const seat = s.toMove === 'gold' ? 'A' : 'B'; + if (ply === askPly && askSquare) { + console.log(`ply ${ply}, ${s.toMove} to move, ${askSquare} may go to: ${legalMovesFrom(s.cells, square(askSquare), 'printed').map(squareName).join(' ')}`); + process.exit(0); + } + const forced = FORCED[ply]; + let move: { from: number; to: number }; + if (forced) { + move = { from: square(forced[0]), to: square(forced[1]) }; + const why = validateMove(s, seat, move); + if (why) { + console.log(`ply ${ply}: forced ${forced[0]}-${forced[1]} refused: ${why}`); + process.exit(1); + } + } else move = chooseMove(s.cells, s.toMove as Side, 'printed', 40000)!; + s = applyMove(s, move.from, move.to); +} +for (const t of narrate(s)) console.log(`${t.ply}${FORCED[t.ply] ? '*' : ' '} ${t.lines.map((l) => l.text).join(' ')}`); +console.log(s.over ? `over: ${s.over.reason}` : `${s.toMove} to move`); diff --git a/src/lib/game/chronicle.ts b/src/lib/game/chronicle.ts index f043586..3fdbd12 100644 --- a/src/lib/game/chronicle.ts +++ b/src/lib/game/chronicle.ts @@ -100,7 +100,8 @@ function tellOne(before: Cell[], move: Move, ply: number, name: (side: Side) => 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; + // 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.` }); } diff --git a/src/lib/game/lessons.test.ts b/src/lib/game/lessons.test.ts index adc401d..22eefad 100644 --- a/src/lib/game/lessons.test.ts +++ b/src/lib/game/lessons.test.ts @@ -61,3 +61,22 @@ describe('the lessons', () => { void other; }); }); + +describe('the game told', () => { + it('plays through the engine from the opening to a checkmate, and every callout sits on a move', async () => { + const { toldStates, TOLD_MOVES, CALLOUTS } = await import('./told'); + const states = toldStates(); + expect(states).toHaveLength(TOLD_MOVES.length + 1); + const last = states[states.length - 1]; + expect(last.end).toBe('mate'); + expect(last.over?.winner).toBe('A'); + for (const n of Object.keys(CALLOUTS).map(Number)) expect(n).toBeLessThanOrEqual(TOLD_MOVES.length); + // The things the callouts say happened. + expect(last.moves[8].captured).toBe('R'); + expect(last.moves[16].kind).toBe('B'); + expect(last.moves[20].captured).toBe('H'); + expect(last.moves[23].captured).toBe('H'); + expect(last.moves[26].captured).toBe('S'); + expect(last.moves[26].check).toBe(true); + }); +}); diff --git a/src/lib/game/told.ts b/src/lib/game/told.ts new file mode 100644 index 0000000..bd46267 --- /dev/null +++ b/src/lib/game/told.ts @@ -0,0 +1,53 @@ +// A game told: twenty-seven moves written to show what a first game should +// see, with a callout where there is something to look at. The moves are +// replayed through the engine, so the game can never drift from the rules, +// and a test says so. + +import { applyMove, createGame, validateMove, type State } from './index'; +import { square } from './board'; + +/** The moves, from and to, in order. */ +export const TOLD_MOVES: [string, string][] = [ + ['2f2', '2f3'], ['2g7', '2g6'], ['2b1', '2c3'], ['2k8', '2j6'], ['2c1', '2e3'], ['3g8', '3c4'], ['2c3', '2e4'], ['3c4', '3d4'], ['2e3', '3d4'], + ['2c8', '2e6'], ['3d4', '2e5'], ['2h7', '2h6'], ['1c1', '1c2'], ['2e6', '2c4'], ['1c2', '1c3'], ['2i8', '2h7'], ['1c3', '1c4'], ['2d7', '2d6'], + ['2b2', '2b3'], ['2j6', '2h5'], ['2b3', '2c4'], ['3c8', '3f6'], ['2e2', '2e3'], ['3f6', '2e5'], ['3g1', '3d4'], ['2a7', '2a6'], ['3d4', '3g7'] +]; + +/** What to look at, by the ply just played; 0 is before the first move. */ +export const CALLOUTS: Record = { + 0: 'Both armies stand as Gygax drew them, and Gold moves first. Step through the game a move at a time. Where there is something to see, a word on it appears here.', + 1: 'A Warrior steps one square, never two, so the opening is slower than chess. This one clears the f file for the Mage behind it.', + 3: 'The Unicorn is the first piece out, because it is the one piece of the land that can leap the wall of Warriors.', + 5: 'The Hero leaps two squares along a diagonal, over its own Warrior at 2d2. That leap is how a Hero gets out early.', + 6: 'Scarlet’s Dragon leaves its roost, which Gygax warned against. Look at what it threatens now: the five land squares beneath 3c4, and Gold’s Unicorn at 2c3 among them. Then look at what it has left behind. There is no roof over Scarlet’s King.', + 7: 'Gold steps the Unicorn out of the Dragon’s shadow.', + 8: 'The Dragon follows, hungry, to 3d4, where it threatens the Unicorn again. But it has stepped onto a square a Hero can reach. From 2e3 a Hero rises onto any square diagonally beside 3e3, and 3d4 is one of them.', + 9: 'The Hero rises and the Dragon falls. Only five kinds of land piece can touch the air at all, and this is how one of them does it. The Hero is in the air now, where it can do nothing but come down.', + 10: 'Scarlet brings a Hero out in answer, leaping its own Warrior at 2d7.', + 11: 'Gold’s Hero comes home to the land, onto a square diagonally beside the one beneath it. Off the land, that was its only kind of move.', + 13: 'Gold’s Basilisk sets out on a slow walk, one square a move. It wants to be beneath whatever Scarlet sends deep into Gold’s side. Watch where Scarlet’s Hero goes.', + 14: 'The Hero digs into Gold’s side at 2c4, two leaps from home. A piece that comes this deep should look beneath its feet.', + 17: 'The gaze falls. The Hero at 2c4 is frozen: it cannot move, cannot take, and threatens nothing, until the Basilisk moves or dies. Gold is in no hurry.', + 19: 'A Warrior comes to stand diagonally behind the frozen Hero.', + 21: 'Taken, in Gygax’s words, more or less at your leisure. The Warrior is safe above Gold’s own Basilisk, since a Basilisk freezes only enemies.', + 22: 'Scarlet’s Griffon leaps to 3f6, two squares one way and three the other, over everything. Now look at the four land squares diagonally beside 2f6. One of them is 2e5, where Gold’s Hero stands.', + 23: 'Gold does not look up.', + 24: 'The swoop. The Griffon drops onto the Hero and takes it. On the land it has lost its wings, and creeps one square at a time until it climbs back.', + 25: 'Gold’s Dragon leaves its roost, and this time it is safe to do so: Scarlet’s Dragon is dead, so nothing threatens Gold’s King from the air. Look along the diagonal from 3d4 through e5 and f6 to 3g7, where a Scarlet Sylph hovers above Scarlet’s King.', + 26: 'Scarlet does not look up either.', + 27: 'The Dragon takes the Sylph at 3g7 and strikes down at every square around the King: 2g7 beneath it, and 2g8, 2f7 and 2h7 beside that. The King cannot step up, because the Dragon stands beside 3g8. It cannot step down, because its own Elemental is at 1g8. Its neighbours on the land are its own pieces or struck squares, and nothing of Scarlet’s can reach 3g7. Checkmate.' +}; + +/** The game at every ply, from the opening to the end. The engine refuses a move that is not legal. */ +export function toldStates(): State[] { + let s = createGame({ A: 'Gold', B: 'Scarlet' }); + const out = [s]; + TOLD_MOVES.forEach(([from, to], n) => { + const seat = s.toMove === 'gold' ? 'A' : 'B'; + const why = validateMove(s, seat, { from: square(from), to: square(to) }); + if (why) throw new Error(`told game, ply ${n + 1}, ${from} to ${to}: ${why}`); + s = applyMove(s, square(from), square(to)); + out.push(s); + }); + return out; +} diff --git a/src/routes/guide/+page.svelte b/src/routes/guide/+page.svelte index ddf95dc..a8b8e02 100644 --- a/src/routes/guide/+page.svelte +++ b/src/routes/guide/+page.svelte @@ -65,7 +65,7 @@

A first game

-

Before that, learn the pieces: fifteen short scenes, one a piece, each a small position with a move for you to make and a word on what came of it.

+

Before that, learn the pieces, fifteen short scenes, one a piece, each a small position with a move for you to make and a word on what came of it, and then a game told, twenty-seven moves stepped through with a callout wherever there is something to see.

Play a drake first. It plays a fair game and never minds how long you take. Begin as you would at chess, with Warriors stepping forward and the knights of the land, the Unicorns and Heroes, coming out behind them. Gygax's own advice is to hold the Mage and the Paladin back until the lesser pieces are developed, and to keep your Dragon home. It starts directly above your King for a reason. A Dragon that flies off to raid leaves the King open to the other Dragon, which strikes the land from the air without ever coming down.

Watch the Basilisks. Anything you leave on the land directly above an enemy Basilisk is frozen on the spot, and a frozen piece may as well be off the board. The Cleric is stronger than it looks, since it is the only piece that plays the same on all three boards, and the Dwarves, if you do not hurry them forward, make good guards late in the game.

The full rules, piece by piece, are on the rules page, with Gygax's article beneath them. If a rule here reads wrong to you, or something misbehaves, send word, or use Report in the masthead at the table, which pins what you saw to the move it happened on. The hall's about this game lists the other ways to reach the keeper.

diff --git a/src/routes/learn/+page.svelte b/src/routes/learn/+page.svelte index aeccf77..a4ea592 100644 --- a/src/routes/learn/+page.svelte +++ b/src/routes/learn/+page.svelte @@ -56,7 +56,7 @@
{#if room}Back to your game{' · '}{/if}Back to the hall · How to play · The rules

Learn the pieces

-

Fifteen pieces, one scene each, and a checkmate to finish. Every scene is a small position with something to see and one move to make. Tap any piece to read about it, as at the table.

+

Fifteen pieces, one scene each, and a checkmate to finish. Every scene is a small position with something to see and one move to make. Tap any piece to read about it, as at the table. When the scenes are done, a game told shows the pieces working together.