Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,303 changes: 1,211 additions & 5,092 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 4 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,18 @@
"@badrap/result": "^0.3"
},
"devDependencies": {
"@jest/globals": "^30",
"@types/node": "^25",
"@typescript-eslint/eslint-plugin": "^8",
"@typescript-eslint/parser": "^8",
"dprint": "^0.54",
"eslint": "^10",
"jest": "^30",
"ts-jest": "^29",
"tsx": "^4.22.4",
"typedoc": "^0.28",
"typescript": "^5"
},
"scripts": {
"prepare": "tsc --declarationDir dist/types && tsc --outDir dist/cjs --module commonjs --declaration false",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test": "node --import tsx/esm --test src/*.test.ts",
"doc": "typedoc src/types.ts src/attacks.ts src/util.ts src/squareSet.ts src/board.ts src/setup.ts src/chess.ts src/compat.ts src/debug.ts src/fen.ts src/san.ts src/transform.ts src/variant.ts src/pgn.ts",
"lint": "eslint",
"format": "dprint fmt",
Expand All @@ -64,22 +63,5 @@
"/src",
"/dist",
"!/**/*.test.*"
],
"jest": {
"testRegex": ".*\\.test\\.ts$",
"transform": {
"\\.ts$": [
"ts-jest",
{
"useESM": true
}
]
},
"extensionsToTreatAsEsm": [
".ts"
],
"moduleNameMapper": {
"^(.*)\\.js$": "$1"
}
}
]
}
17 changes: 9 additions & 8 deletions src/attacks.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { expect, test } from '@jest/globals';
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { between, ray, rookAttacks } from './attacks.js';
import { SquareSet } from './squareSet.js';

test('rook attacks', () => {
const d6 = 43;
expect(rookAttacks(d6, new SquareSet(0x2826f5b9, 0x3f7f2880))).toEqual(new SquareSet(0x8000000, 0x83708));
expect(rookAttacks(d6, SquareSet.empty())).toEqual(SquareSet.fromFile(3).xor(SquareSet.fromRank(5)));
assert.deepStrictEqual(rookAttacks(d6, new SquareSet(0x2826f5b9, 0x3f7f2880)), new SquareSet(0x8000000, 0x83708));
assert.deepStrictEqual(rookAttacks(d6, SquareSet.empty()), SquareSet.fromFile(3).xor(SquareSet.fromRank(5)));
});

test('ray', () => {
expect(ray(0, 8)).toEqual(SquareSet.fromFile(0));
assert.deepStrictEqual(ray(0, 8), SquareSet.fromFile(0));
});

test('between', () => {
expect(between(42, 42)).toEqual(SquareSet.empty());
expect(Array.from(between(0, 3))).toEqual([1, 2]);
assert.deepStrictEqual(between(42, 42), SquareSet.empty());
assert.deepStrictEqual(Array.from(between(0, 3)), [1, 2]);

expect(Array.from(between(61, 47))).toEqual([54]);
expect(Array.from(between(47, 61))).toEqual([54]);
assert.deepStrictEqual(Array.from(between(61, 47)), [54]);
assert.deepStrictEqual(Array.from(between(47, 61)), [54]);
});
27 changes: 14 additions & 13 deletions src/board.test.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { expect, test } from '@jest/globals';
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { Board, boardEquals } from './board.js';
import { Piece } from './types.js';

test('set and get', () => {
const emptyBoard = Board.empty();
expect(emptyBoard.getColor(0)).toBeUndefined();
expect(emptyBoard.getRole(0)).toBeUndefined();
expect(emptyBoard.has(0)).toBe(false);
expect(emptyBoard.get(0)).toBeUndefined();
expect(boardEquals(emptyBoard, emptyBoard.clone())).toBe(true);
assert.strictEqual(emptyBoard.getColor(0), undefined);
assert.strictEqual(emptyBoard.getRole(0), undefined);
assert.strictEqual(emptyBoard.has(0), false);
assert.strictEqual(emptyBoard.get(0), undefined);
assert.ok(boardEquals(emptyBoard, emptyBoard.clone()));

const board = emptyBoard.clone();
const piece: Piece = { role: 'knight', color: 'black', promoted: false };
expect(board.set(0, piece)).toBeUndefined();
expect(board.getColor(0)).toBe('black');
expect(board.getRole(0)).toBe('knight');
expect(board.has(0)).toBe(true);
expect(board.get(0)).toEqual(piece);
expect(boardEquals(board, board.clone())).toBe(true);
expect(boardEquals(emptyBoard, board)).toBe(false);
assert.strictEqual(board.set(0, piece), undefined);
assert.strictEqual(board.getColor(0), 'black');
assert.strictEqual(board.getRole(0), 'knight');
assert.strictEqual(board.has(0), true);
assert.deepStrictEqual(board.get(0), piece);
assert.ok(boardEquals(board, board.clone()));
assert.strictEqual(boardEquals(emptyBoard, board), false);
});
126 changes: 68 additions & 58 deletions src/chess.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { expect, test } from '@jest/globals';
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { Castles, castlingSide, Chess, isImpossibleCheck, normalizeMove } from './chess.js';
import { perft } from './debug.js';
import { INITIAL_FEN, makeFen, parseFen } from './fen.js';
import { SquareSet } from './squareSet.js';
import { parseUci } from './util.js';
import { NormalMove } from './types.js';

const tricky: [string, string, number, number, number, number?, number?][] = [
['pos-2', 'r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -', 48, 2039, 97862], // Kiwipete by Peter McKenzie
Expand Down Expand Up @@ -67,89 +69,93 @@ test('castles from setup', () => {
const setup = parseFen(INITIAL_FEN).unwrap();
const castles = Castles.fromSetup(setup);

expect(castles.castlingRights).toEqual(SquareSet.corners());
assert.deepStrictEqual(castles.castlingRights, SquareSet.corners());

expect(castles.rook.white.a).toBe(0);
expect(castles.rook.white.h).toBe(7);
expect(castles.rook.black.a).toBe(56);
expect(castles.rook.black.h).toBe(63);
assert.strictEqual(castles.rook.white.a, 0);
assert.strictEqual(castles.rook.white.h, 7);
assert.strictEqual(castles.rook.black.a, 56);
assert.strictEqual(castles.rook.black.h, 63);

expect(Array.from(castles.path.white.a)).toEqual([1, 2, 3]);
expect(Array.from(castles.path.white.h)).toEqual([5, 6]);
expect(Array.from(castles.path.black.a)).toEqual([57, 58, 59]);
expect(Array.from(castles.path.black.h)).toEqual([61, 62]);
assert.deepStrictEqual(Array.from(castles.path.white.a), [1, 2, 3]);
assert.deepStrictEqual(Array.from(castles.path.white.h), [5, 6]);
assert.deepStrictEqual(Array.from(castles.path.black.a), [57, 58, 59]);
assert.deepStrictEqual(Array.from(castles.path.black.h), [61, 62]);
});

test('play move', () => {
const pos = Chess.fromSetup(parseFen('8/8/8/5k2/3p4/8/4P3/4K3 w - -').unwrap()).unwrap();

const kd1 = pos.clone();
kd1.play({ from: 4, to: 3 });
expect(makeFen(kd1.toSetup())).toBe('8/8/8/5k2/3p4/8/4P3/3K4 b - - 1 1');
assert.strictEqual(makeFen(kd1.toSetup()), '8/8/8/5k2/3p4/8/4P3/3K4 b - - 1 1');

const e4 = pos.clone();
e4.play({ from: 12, to: 28 });
expect(makeFen(e4.toSetup())).toBe('8/8/8/5k2/3pP3/8/8/4K3 b - e3 0 1');
assert.strictEqual(makeFen(e4.toSetup()), '8/8/8/5k2/3pP3/8/8/4K3 b - e3 0 1');
});

test('castling moves', () => {
let pos = Chess.fromSetup(parseFen('2r5/8/8/8/8/8/6PP/k2KR3 w K -').unwrap()).unwrap();
let move = { from: 3, to: 4 };
expect(pos.isLegal(move)).toBe(true);
assert.strictEqual(pos.isLegal(move), true);
pos.play(move);
expect(makeFen(pos.toSetup())).toBe('2r5/8/8/8/8/8/6PP/k4RK1 b - - 1 1');
assert.strictEqual(makeFen(pos.toSetup()), '2r5/8/8/8/8/8/6PP/k4RK1 b - - 1 1');

pos = Chess.fromSetup(
parseFen('r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1').unwrap(),
).unwrap();
move = { from: 4, to: 0 };
expect(pos.isLegal(move)).toBe(true);
assert.strictEqual(pos.isLegal(move), true);
pos.play(move);
expect(makeFen(pos.toSetup())).toBe('r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/2KR3R b kq - 1 1');
assert.strictEqual(makeFen(pos.toSetup()), 'r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/2KR3R b kq - 1 1');

pos = Chess.fromSetup(
parseFen('1r2k2r/p1b1n1pp/1q3p2/1p2pPQ1/4P3/2P4P/1B2B1P1/R3K2R w KQk - 0 20').unwrap(),
).unwrap();
const queenSide = { from: 4, to: 0 };
const altQueenSide = { from: 4, to: 2 };
expect(castlingSide(pos, queenSide)).toBe('a');
expect(normalizeMove(pos, queenSide)).toEqual(queenSide);
expect(pos.isLegal(queenSide)).toBe(true);
expect(castlingSide(pos, altQueenSide)).toBe('a');
expect(normalizeMove(pos, altQueenSide)).toEqual(queenSide);
expect(pos.isLegal(altQueenSide)).toBe(true);
assert.strictEqual(castlingSide(pos, queenSide), 'a');
assert.deepStrictEqual(normalizeMove(pos, queenSide), queenSide);
assert.strictEqual(pos.isLegal(queenSide), true);
assert.strictEqual(castlingSide(pos, altQueenSide), 'a');
assert.deepStrictEqual(normalizeMove(pos, altQueenSide), queenSide);
assert.strictEqual(pos.isLegal(altQueenSide), true);
pos.play(altQueenSide);
expect(makeFen(pos.toSetup())).toBe('1r2k2r/p1b1n1pp/1q3p2/1p2pPQ1/4P3/2P4P/1B2B1P1/2KR3R b k - 1 20');
assert.strictEqual(makeFen(pos.toSetup()), '1r2k2r/p1b1n1pp/1q3p2/1p2pPQ1/4P3/2P4P/1B2B1P1/2KR3R b k - 1 20');
});

test('test illegal promotion', () => {
const pos = Chess.default();
expect(pos.isLegal({ from: 12, to: 20, promotion: 'queen' })).toBe(false);
assert.strictEqual(pos.isLegal({ from: 12, to: 20, promotion: 'queen' }), false);
});

test('starting perft', () => {
const pos = Chess.default();
expect(perft(pos, 0, false)).toBe(1);
expect(perft(pos, 1, false)).toBe(20);
expect(perft(pos, 2, false)).toBe(400);
expect(perft(pos, 3, false)).toBe(8902);
assert.strictEqual(perft(pos, 0, false), 1);
assert.strictEqual(perft(pos, 1, false), 20);
assert.strictEqual(perft(pos, 2, false), 400);
assert.strictEqual(perft(pos, 3, false), 8902);
});

test.each(tricky)('tricky perft: %s: %s', (_, fen, d1, d2, d3) => {
const pos = Chess.fromSetup(parseFen(fen).unwrap()).unwrap();
expect(perft(pos, 1, false)).toBe(d1);
expect(perft(pos, 2, false)).toBe(d2);
expect(perft(pos, 3, false)).toBe(d3);
});

test.each(random)('random perft: %s: %s', (_, fen, d1, d2, d3, d4, d5) => {
const pos = Chess.fromSetup(parseFen(fen).unwrap()).unwrap();
expect(perft(pos, 1, false)).toBe(d1);
expect(perft(pos, 2, false)).toBe(d2);
expect(perft(pos, 3, false)).toBe(d3);
expect(perft(pos, 4, false)).toBe(d4);
if (d5 < 100000) expect(perft(pos, 5, false)).toBe(d5);
});
for (const [name, fen, d1, d2, d3] of tricky) {
test(`tricky perft: ${name}: ${fen}`, () => {
const pos = Chess.fromSetup(parseFen(fen).unwrap()).unwrap();
assert.strictEqual(perft(pos, 1, false), d1);
assert.strictEqual(perft(pos, 2, false), d2);
assert.strictEqual(perft(pos, 3, false), d3);
});
}

for (const [name, fen, d1, d2, d3, d4, d5] of random) {
test(`random perft: ${name}: ${fen}`, () => {
const pos = Chess.fromSetup(parseFen(fen).unwrap()).unwrap();
assert.strictEqual(perft(pos, 1, false), d1);
assert.strictEqual(perft(pos, 2, false), d2);
assert.strictEqual(perft(pos, 3, false), d3);
assert.strictEqual(perft(pos, 4, false), d4);
if (d5 < 100000) assert.strictEqual(perft(pos, 5, false), d5);
});
}

const insufficientMaterial: [string, boolean, boolean][] = [
['8/5k2/8/8/8/8/3K4/8 w - - 0 1', true, true],
Expand All @@ -164,43 +170,47 @@ const insufficientMaterial: [string, boolean, boolean][] = [
['3b4/8/8/6b1/8/8/R7/K1k5 w - - 0 1', false, true],
];

test.each(insufficientMaterial)('insufficient material: %s', (fen, white, black) => {
const pos = Chess.fromSetup(parseFen(fen).unwrap()).unwrap();
expect(pos.hasInsufficientMaterial('white')).toBe(white);
expect(pos.hasInsufficientMaterial('black')).toBe(black);
});
for (const [fen, white, black] of insufficientMaterial) {
test(`insufficient material: ${fen}`, () => {
const pos = Chess.fromSetup(parseFen(fen).unwrap()).unwrap();
assert.strictEqual(pos.hasInsufficientMaterial('white'), white);
assert.strictEqual(pos.hasInsufficientMaterial('black'), black);
});
}

test('impossible check', () => {
// Multiple checkers aligned with king.
const pos1 = Chess.fromSetup(parseFen('3R4/8/q4k2/2B5/1NK5/3b4/8/8 w - - 0 1').unwrap()).unwrap();
expect(isImpossibleCheck(pos1)).toBe(true);
assert.strictEqual(isImpossibleCheck(pos1), true);

// Checkers aligned with opponent king are fine.
const pos2 = Chess.fromSetup(parseFen('8/8/5k2/p1q5/PP1rp1P1/3P1N2/2RK1r2/5nN1 w - - 0 3').unwrap()).unwrap();
expect(isImpossibleCheck(pos2)).toBe(false);
assert.strictEqual(isImpossibleCheck(pos2), false);

// En passant square aligned with checker and king.
const pos3 = Chess.fromSetup(parseFen('8/8/8/1k6/3Pp3/8/8/4KQ2 b - d3 0 1').unwrap()).unwrap();
expect(isImpossibleCheck(pos3)).toBe(true);
assert.strictEqual(isImpossibleCheck(pos3), true);

// Multiple steppers.
const pos4 = Chess.fromSetup(parseFen('2b5/1nbn4/n3n3/1kn5/n3n3/1n1n4/5RQ1/2KQ1R2 w K - 0 1').unwrap()).unwrap();
expect(isImpossibleCheck(pos4)).toBe(true);
assert.strictEqual(isImpossibleCheck(pos4), true);
});

test('king captures unmoved rook', () => {
const pos = Chess.fromSetup(parseFen('8/8/8/B2p3Q/2qPp1P1/b7/2P2PkP/4K2R b K - 0 1').unwrap()).unwrap();
const move = parseUci('g2h1')!;
expect(move).toEqual({ from: 14, to: 7 });
expect(pos.isLegal(move)).toBe(true);
const move = parseUci('g2h1') as NormalMove;
assert.strictEqual(move.from, 14);
assert.strictEqual(move.to, 7);
assert.strictEqual(move.promotion, undefined);
assert.strictEqual(pos.isLegal(move), true);
pos.play(move);
expect(makeFen(pos.toSetup())).toBe('8/8/8/B2p3Q/2qPp1P1/b7/2P2P1P/4K2k w - - 0 2');
assert.strictEqual(makeFen(pos.toSetup()), '8/8/8/B2p3Q/2qPp1P1/b7/2P2P1P/4K2k w - - 0 2');
});

test('en passant and unrelated check', () => {
const setup = parseFen('rnbqk1nr/bb3p1p/1q2r3/2pPp3/3P4/7P/1PP1NpPP/R1BQKBNR w KQkq c6').unwrap();
const pos = Chess.fromSetup(setup).unwrap();
expect(isImpossibleCheck(pos)).toBe(true);
assert.strictEqual(isImpossibleCheck(pos), true);
const enPassant = parseUci('d5c6')!;
expect(pos.isLegal(enPassant)).toBe(false);
assert.strictEqual(pos.isLegal(enPassant), false);
});
36 changes: 17 additions & 19 deletions src/compat.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@jest/globals';
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { Chess } from './chess.js';
import { chessgroundDests, scalachessCharPair } from './compat.js';
import { parseFen } from './fen.js';
Expand All @@ -8,40 +9,37 @@ test('chessground dests with Kh8', () => {
const setup = parseFen('r1bq1r2/3n2k1/p1p1pp2/3pP2P/8/PPNB2Q1/2P2P2/R3K3 b Q - 1 22').unwrap();
const pos = Chess.fromSetup(setup).unwrap();
const dests = chessgroundDests(pos);
expect(dests.get('g7')).toContain('h8');
expect(dests.get('g7')).not.toContain('g8');
assert.ok(dests.get('g7')!.includes('h8'));
assert.ok(!dests.get('g7')!.includes('g8'));
});

test('chessground dests with regular castle', () => {
const setup = parseFen('r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1').unwrap();
const wtm = Chess.fromSetup(setup).unwrap();
expect(chessgroundDests(wtm).get('e1')!.sort()).toEqual(['a1', 'c1', 'd1', 'd2', 'e2', 'f1', 'f2', 'g1', 'h1']);
expect(chessgroundDests(wtm).get('e8')).toBeUndefined();
assert.deepStrictEqual(chessgroundDests(wtm).get('e1')!.sort(), ['a1', 'c1', 'd1', 'd2', 'e2', 'f1', 'f2', 'g1', 'h1']);
assert.strictEqual(chessgroundDests(wtm).get('e8'), undefined);

setup.turn = 'black';
const btm = Chess.fromSetup(setup).unwrap();
expect(chessgroundDests(btm).get('e8')!.sort()).toEqual(['a8', 'c8', 'd7', 'd8', 'e7', 'f7', 'f8', 'g8', 'h8']);
expect(chessgroundDests(btm).get('e1')).toBeUndefined();
assert.deepStrictEqual(chessgroundDests(btm).get('e8')!.sort(), ['a8', 'c8', 'd7', 'd8', 'e7', 'f7', 'f8', 'g8', 'h8']);
assert.strictEqual(chessgroundDests(btm).get('e1'), undefined);
});

test('chessground dests with chess960 castle', () => {
const setup = parseFen('rk2r3/pppbnppp/3p2n1/P2Pp3/4P2q/R5NP/1PP2PP1/1KNQRB2 b Kkq - 0 1').unwrap();
const pos = Chess.fromSetup(setup).unwrap();
expect(chessgroundDests(pos).get('b8')!.sort()).toEqual(['a8', 'c8', 'e8']);
assert.deepStrictEqual(chessgroundDests(pos).get('b8')!.sort(), ['a8', 'c8', 'e8']);
});

test('uci char pair', () => {
// regular moves
expect(scalachessCharPair(parseUci('a1b1')!)).toBe('#$');
expect(scalachessCharPair(parseUci('a1a2')!)).toBe('#+');
expect(scalachessCharPair(parseUci('h7h8')!)).toBe('Zb');
assert.strictEqual(scalachessCharPair(parseUci('a1b1')!), '#$');
assert.strictEqual(scalachessCharPair(parseUci('a1a2')!), '#+');
assert.strictEqual(scalachessCharPair(parseUci('h7h8')!), 'Zb');

// promotions
expect(scalachessCharPair(parseUci('b7b8q')!)).toBe('Td');
expect(scalachessCharPair(parseUci('b7c8q')!)).toBe('Te');
expect(scalachessCharPair(parseUci('b7c8n')!)).toBe('T}');
assert.strictEqual(scalachessCharPair(parseUci('b7b8q')!), 'Td');
assert.strictEqual(scalachessCharPair(parseUci('b7c8q')!), 'Te');
assert.strictEqual(scalachessCharPair(parseUci('b7c8n')!), 'T}');

// drops
expect(scalachessCharPair(parseUci('P@a1')!)).toBe('#\x8f');
expect(scalachessCharPair(parseUci('Q@h8')!)).toBe('b\x8b');
assert.strictEqual(scalachessCharPair(parseUci('P@a1')!), '#\x8f');
assert.strictEqual(scalachessCharPair(parseUci('Q@h8')!), 'b\x8b');
});
Loading
Loading