feat(engine): synchronized-move intersection and selection highlight
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DuplicateGame } from './game';
|
||||
import { legalSyncedMoves, selectionHighlight } from './legality';
|
||||
|
||||
describe('legalSyncedMoves', () => {
|
||||
it('returns all 20 white opening moves when a player\'s two boards are identical', () => {
|
||||
const g = new DuplicateGame();
|
||||
expect(legalSyncedMoves(g)).toHaveLength(20);
|
||||
});
|
||||
|
||||
it('excludes a move legal on only one of the player\'s boards', () => {
|
||||
// N e2e4, S e2e4, W e7e5, E d7d5 -> NW black has pe5, NE black has pd5.
|
||||
const g = new DuplicateGame([
|
||||
{ player: 'N', from: 'e2', to: 'e4' },
|
||||
{ player: 'S', from: 'e2', to: 'e4' },
|
||||
{ player: 'E', from: 'd7', to: 'd5' },
|
||||
{ player: 'W', from: 'e7', to: 'e5' },
|
||||
]);
|
||||
expect(g.currentPlayer).toBe('N');
|
||||
const keys = legalSyncedMoves(g).map((m) => `${m.from}${m.to}`);
|
||||
// e4-d5 is a capture on NE but illegal on NW (d5 empty there): not synced.
|
||||
expect(keys).not.toContain('e4d5');
|
||||
// e4-e5 is legal on NE but blocked on NW (black pawn on e5): not synced.
|
||||
expect(keys).not.toContain('e4e5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectionHighlight', () => {
|
||||
it('marks every destination playable when the two boards agree', () => {
|
||||
const g = new DuplicateGame();
|
||||
const h = selectionHighlight(g, 'e2');
|
||||
expect(h.playable.sort()).toEqual(['e3', 'e4']);
|
||||
expect(h.onlyA).toEqual([]);
|
||||
expect(h.onlyB).toEqual([]);
|
||||
});
|
||||
|
||||
it('splits destinations into playable vs board-local-only on divergence', () => {
|
||||
const g = new DuplicateGame([
|
||||
{ player: 'N', from: 'e2', to: 'e4' },
|
||||
{ player: 'S', from: 'e2', to: 'e4' },
|
||||
{ player: 'E', from: 'd7', to: 'd5' },
|
||||
{ player: 'W', from: 'e7', to: 'e5' },
|
||||
]);
|
||||
const h = selectionHighlight(g, 'e4'); // North's e4 pawn
|
||||
expect(h.boardA).toBe('NW');
|
||||
expect(h.boardB).toBe('NE');
|
||||
expect(h.playable).toEqual([]); // nothing legal on both
|
||||
expect(h.onlyA).toEqual([]); // e4 is blocked on NW
|
||||
expect(h.onlyB.sort()).toEqual(['d5', 'e5']); // capture + advance on NE only
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { DuplicateGame } from './game';
|
||||
import type { SyncMove, Square, BoardId, PromotionPiece } from './types';
|
||||
import { PLAYER_BOARDS } from './boards';
|
||||
|
||||
function key(m: { from: string; to: string; promotion?: string }): string {
|
||||
return `${m.from}${m.to}${m.promotion ?? ''}`;
|
||||
}
|
||||
|
||||
/** Every synchronized-legal move for the player to move (the intersection). */
|
||||
export function legalSyncedMoves(game: DuplicateGame): SyncMove[] {
|
||||
const [a, b] = PLAYER_BOARDS[game.currentPlayer];
|
||||
const movesA = game.boards[a].moves({ verbose: true });
|
||||
const keysB = new Set(game.boards[b].moves({ verbose: true }).map(key));
|
||||
const seen = new Set<string>();
|
||||
const result: SyncMove[] = [];
|
||||
for (const m of movesA) {
|
||||
const k = key(m);
|
||||
if (keysB.has(k) && !seen.has(k)) {
|
||||
seen.add(k);
|
||||
result.push({
|
||||
from: m.from,
|
||||
to: m.to,
|
||||
promotion: (m.promotion as PromotionPiece) || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface SelectionHighlight {
|
||||
/** The current player's first board. */
|
||||
boardA: BoardId;
|
||||
/** The current player's second board. */
|
||||
boardB: BoardId;
|
||||
/** Destinations legal on BOTH boards (actually playable). */
|
||||
playable: Square[];
|
||||
/** Destinations legal on board A only. */
|
||||
onlyA: Square[];
|
||||
/** Destinations legal on board B only. */
|
||||
onlyB: Square[];
|
||||
}
|
||||
|
||||
/** Triple-highlight data for the current player's piece grabbed at `from`. */
|
||||
export function selectionHighlight(
|
||||
game: DuplicateGame,
|
||||
from: Square,
|
||||
): SelectionHighlight {
|
||||
const [a, b] = PLAYER_BOARDS[game.currentPlayer];
|
||||
const destA = new Set(
|
||||
game.boards[a].moves({ verbose: true }).filter((m) => m.from === from).map((m) => m.to),
|
||||
);
|
||||
const destB = new Set(
|
||||
game.boards[b].moves({ verbose: true }).filter((m) => m.from === from).map((m) => m.to),
|
||||
);
|
||||
const playable: Square[] = [];
|
||||
const onlyA: Square[] = [];
|
||||
const onlyB: Square[] = [];
|
||||
for (const sq of destA) (destB.has(sq) ? playable : onlyA).push(sq);
|
||||
for (const sq of destB) if (!destA.has(sq)) onlyB.push(sq);
|
||||
return { boardA: a, boardB: b, playable, onlyA, onlyB };
|
||||
}
|
||||
Reference in New Issue
Block a user