feat(engine): ghost derivation from cross-board piece comparison
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DuplicateGame } from './game';
|
||||
import { playSymmetric } from './test-helpers';
|
||||
import { ghosts } from './ghosts';
|
||||
|
||||
describe('ghosts', () => {
|
||||
it('reports no ghosts at the start', () => {
|
||||
expect(ghosts(new DuplicateGame())).toEqual([]);
|
||||
});
|
||||
|
||||
it('forms a ghost when a piece is captured on one board but not its twin', () => {
|
||||
const g = new DuplicateGame();
|
||||
// Symmetric opening so all four boards stay identical...
|
||||
playSymmetric(g, [
|
||||
[{ from: 'e2', to: 'e4' }, { from: 'e7', to: 'e5' }],
|
||||
[{ from: 'g1', to: 'f3' }, { from: 'b8', to: 'c6' }],
|
||||
]);
|
||||
// ...then North & South each play Nxe5 (capturing the e5 pawn),
|
||||
// and East captures that knight only on its boards (NE, SE).
|
||||
g.applyMove({ from: 'f3', to: 'e5' }); // N: Nf3xe5 on NW, NE
|
||||
g.applyMove({ from: 'f3', to: 'e5' }); // S: Nf3xe5 on SW, SE
|
||||
g.applyMove({ from: 'c6', to: 'e5' }); // E: Nc6xe5 on NE, SE — captures the white knight
|
||||
// North's knight on e5 survives on NW but was captured on NE -> NW/e5 is a ghost.
|
||||
// South's knight on e5 survives on SW but was captured on SE -> SW/e5 is a ghost.
|
||||
const result = ghosts(g).sort((x, y) => (x.board < y.board ? -1 : 1));
|
||||
expect(result).toEqual([
|
||||
{ board: 'NW', square: 'e5' },
|
||||
{ board: 'SW', square: 'e5' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { DuplicateGame } from './game';
|
||||
import type { GhostMarker, BoardId, Color, Square } from './types';
|
||||
import { PLAYERS, PLAYER_BOARDS, PLAYER_COLOR } from './boards';
|
||||
|
||||
/** Squares occupied by a piece of `color` on `board`. */
|
||||
function colorSquares(game: DuplicateGame, board: BoardId, color: Color): Set<Square> {
|
||||
const set = new Set<Square>();
|
||||
for (const row of game.boards[board].board()) {
|
||||
for (const cell of row) {
|
||||
if (cell && cell.color === color) set.add(cell.square);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ghosts across all four players. A player's non-ghost pieces always occupy
|
||||
* identical squares on both their boards (they move in lockstep), so a piece is
|
||||
* a ghost iff the player's other board has no same-colour piece on that square.
|
||||
*/
|
||||
export function ghosts(game: DuplicateGame): GhostMarker[] {
|
||||
const markers: GhostMarker[] = [];
|
||||
for (const player of PLAYERS) {
|
||||
const [a, b] = PLAYER_BOARDS[player];
|
||||
const color = PLAYER_COLOR[player];
|
||||
const sqA = colorSquares(game, a, color);
|
||||
const sqB = colorSquares(game, b, color);
|
||||
for (const sq of sqA) if (!sqB.has(sq)) markers.push({ board: a, square: sq });
|
||||
for (const sq of sqB) if (!sqA.has(sq)) markers.push({ board: b, square: sq });
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
Reference in New Issue
Block a user