Files
wyrm/src/lib/game/lessons.test.ts
T
Eric WagonerandClaude Fable 5.1 0be89a1dfe Credibility pass: the code reads as one hand's work. Dead plumbing and copies removed, four claims the source does not support corrected, and inherited kit text re-read for this game
Engine: the facade re-exports the board in one block; describe() and the
dead letters preference, void ply, the King-taken line, the identity maps
and the unused imports are gone; SIDE_NAMES is the one spelling of a side;
the drake's hunter comment no longer claims only four kinds reach the air.
Prose: no shatranj and no sitting duck, which Gygax never wrote; the Thief
statistic and the Basilisk's "one trick" are gone; lessons say what the
board shows instead of repeating the panel; a Thief's leap counts four.
Browser: one keyboard walk, in BoardArtwork; the lesson board and page share
one game and one inspected piece; the chronicle's possessive is told, not
patched by a regex; the chronicle colours and the reading layout are global
rules instead of three copies; the guide and the about panel say what the
code and the article actually do.
Server and docs: the room server states that it is the kit's, kept whole;
the skills, the deploy README and the share card name files that exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
2026-10-26 22:38:00 -04:00

91 lines
4.3 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.
import { describe, expect, it } from 'vitest';
import { SCENES, sceneCells } from './lessons';
import { frozen, hasLegalMove, inCheck, kindOf, legalMovesFrom, movesFrom, playOn, square } from './board';
import { CALLOUTS, TOLD_MOVES, toldStates } from './told';
describe('the lessons', () => {
it('every scene’s move is legal for the side to move, and only the asked-for moves count', () => {
for (const scene of SCENES) {
const cells = sceneCells(scene);
for (const [from, to] of scene.moves) {
const legal = legalMovesFrom(cells, square(from), 'printed');
expect(legal, `${scene.id}: ${from} to ${to}`).toContain(square(to));
}
// The piece asked for is the player's.
for (const [from] of scene.moves) expect(cells[square(from)][0], scene.id).toBe(scene.side === 'gold' ? 'g' : 's');
// The scene draws every board its move touches.
for (const [from, to] of scene.moves) {
expect(scene.levels, scene.id).toContain(Number(from[0]));
expect(scene.levels, scene.id).toContain(Number(to[0]));
}
}
});
it('the scenes do what their words say', () => {
const at = (id: string) => SCENES.find((s) => s.id === id)!;
const play = (id: string) => {
const s = at(id);
const cells = sceneCells(s);
const [from, to] = s.moves[0];
return { cells, after: playOn(cells, square(from), square(to)), s };
};
// The Warrior's step is blocked and its capture is not.
expect(movesFrom(sceneCells(at('warrior')), square('2c2'))).toEqual([square('2d3')]);
// The Hero leaps the Warrior and takes the Mage.
expect(play('hero').after.captured).toBe('M');
// The Mage comes down two boards onto the Basilisk.
expect(play('mage').after.captured).toBe('B');
// The Paladin vaults over the Dwarf at 1c5.
expect(play('paladin').after.captured).toBe('D');
// The Sylph's dive and the Dragon's strike take what they say.
expect(play('sylph').after.captured).toBe('O');
const dragon = play('dragon');
expect(dragon.after.afar).toBe(true);
expect(dragon.after.captured).toBe('M');
expect(dragon.after.cells[square('3c4')]).toBe('gR');
// The Basilisk's arrival freezes the Mage above.
const basilisk = play('basilisk');
expect(frozen(basilisk.after.cells, square('2e4'))).toBe(true);
expect(movesFrom(basilisk.after.cells, square('2e4'))).toEqual([]);
// The Elemental climbs onto the Thief.
expect(play('elemental').after.captured).toBe('T');
// The King is in check before it moves, and out of it after either move; up or down leaves it off the land.
const king = sceneCells(at('king'));
expect(inCheck(king, 'gold')).toBe(true);
expect(legalMovesFrom(king, square('2g1'), 'printed').sort()).toEqual([square('1g1'), square('2f1'), square('2h1'), square('3g1')].sort());
// The last scene is mate in one by either listed move, and by no other.
const mate = play('mate');
for (const [from, to] of mate.s.moves) {
const after = playOn(mate.cells, square(from), square(to)).cells;
expect(inCheck(after, 'scarlet'), `${from} to ${to}`).toBe(true);
expect(hasLegalMove(after, 'scarlet', 'printed'), `${from} to ${to}`).toBe(false);
}
const listed = new Set(mate.s.moves.map(([a, b]) => `${square(a)}-${square(b)}`));
for (const to of legalMovesFrom(mate.cells, square('2a1'), 'printed')) {
if (listed.has(`${square('2a1')}-${to}`)) continue;
const after = playOn(mate.cells, square('2a1'), to).cells;
expect(inCheck(after, 'scarlet') && !hasLegalMove(after, 'scarlet', 'printed'), `unlisted mate by the Oliphant to ${to}`).toBe(false);
}
expect(kindOf(mate.cells[square('2d4')])).toBe('K');
expect(frozen(mate.cells, square('2d4'))).toBe(true);
});
});
describe('the game told', () => {
it('plays through the engine from the opening to a checkmate, and every callout sits on a move', () => {
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);
});
});