bitboard-chess 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/index.mjs +672 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# bitboard-chess
|
|
2
|
+
|
|
3
|
+
Lightweight bitboard chess engine for position updates. **No move validation** — assumes validated input (e.g. from replaying PGN games). Supports castling, en passant, promotions, full FEN, and deterministic Zobrist hashing.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install bitboard-chess
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage (ESM)
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import BitboardChess from 'bitboard-chess';
|
|
15
|
+
|
|
16
|
+
const board = new BitboardChess();
|
|
17
|
+
|
|
18
|
+
// Apply moves by SAN (Standard Algebraic Notation)
|
|
19
|
+
board.makeMoveSAN('e4');
|
|
20
|
+
board.makeMoveSAN('e5');
|
|
21
|
+
board.makeMoveSAN('Nf3');
|
|
22
|
+
|
|
23
|
+
console.log(board.toFEN());
|
|
24
|
+
// rnbqkbnr/pppppppp/8/8/4P3/5N2/PPPP1PPP/RNBQKBR1 b Qkq - 1 2
|
|
25
|
+
|
|
26
|
+
// Or with raw move objects: { from, to, promotion?, castle?, enpassant? }
|
|
27
|
+
// Squares are 0–63 (a1=0, h8=63).
|
|
28
|
+
board.makeMove({ from: 12, to: 28 }); // e2-e4
|
|
29
|
+
|
|
30
|
+
// Load from FEN
|
|
31
|
+
board.loadFromFEN('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1');
|
|
32
|
+
|
|
33
|
+
// Deterministic Zobrist key (bigint) for the current position
|
|
34
|
+
const key = board.getZobristKey();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
- **`new BitboardChess()`** — Start from initial position.
|
|
40
|
+
- **`makeMove(move)`** — Apply a move object `{ from, to, promotion?, castle?, enpassant? }`. No validation.
|
|
41
|
+
- **`makeMoveSAN(san)`** — Apply a move by SAN string. Returns `true`/`false`. No legality check.
|
|
42
|
+
- **`resolveSAN(san)`** — Resolve SAN to a move object, or `null` if ambiguous/unresolvable.
|
|
43
|
+
- **`loadFromFEN(fen)`** — Set position from FEN (piece placement, side, castling, en passant, halfmove, fullmove). No validation.
|
|
44
|
+
- **`toFEN()`** — Return current position as FEN string.
|
|
45
|
+
- **`getZobristKey()`** — Return deterministic Zobrist key (bigint) for the current position.
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/index.mjs
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Bitboard Chess Engine Core — no move validation.
|
|
3
|
+
// Assumes validated input (e.g. from replaying PGN games). Supports:
|
|
4
|
+
// castling, en passant, promotions, full FEN. Used to generate FEN
|
|
5
|
+
// for the same move sequence as chess.js; final position must match.
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
const WHITE = 0;
|
|
9
|
+
const BLACK = 1;
|
|
10
|
+
|
|
11
|
+
const bit = (sq) => 1n << BigInt(sq);
|
|
12
|
+
|
|
13
|
+
// Precomputed attack tables
|
|
14
|
+
const knightAttacks = new Array(64).fill(0n);
|
|
15
|
+
const kingAttacks = new Array(64).fill(0n);
|
|
16
|
+
const pawnAttacks = [new Array(64).fill(0n), new Array(64).fill(0n)];
|
|
17
|
+
|
|
18
|
+
function inBoard(f, r) {
|
|
19
|
+
return f >= 0 && f < 8 && r >= 0 && r < 8;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function initAttackTables() {
|
|
23
|
+
for (let sq = 0; sq < 64; sq++) {
|
|
24
|
+
const f = sq % 8;
|
|
25
|
+
const r = Math.floor(sq / 8);
|
|
26
|
+
|
|
27
|
+
// Knight
|
|
28
|
+
const kD = [
|
|
29
|
+
[1, 2], [2, 1], [2, -1], [1, -2],
|
|
30
|
+
[-1, -2], [-2, -1], [-2, 1], [-1, 2]
|
|
31
|
+
];
|
|
32
|
+
let bb = 0n;
|
|
33
|
+
for (const [df, dr] of kD) {
|
|
34
|
+
const nf = f + df, nr = r + dr;
|
|
35
|
+
if (inBoard(nf, nr)) bb |= bit(nr * 8 + nf);
|
|
36
|
+
}
|
|
37
|
+
knightAttacks[sq] = bb;
|
|
38
|
+
|
|
39
|
+
// King
|
|
40
|
+
const gD = [
|
|
41
|
+
[1, 0], [1, 1], [0, 1], [-1, 1],
|
|
42
|
+
[-1, 0], [-1, -1], [0, -1], [1, -1]
|
|
43
|
+
];
|
|
44
|
+
bb = 0n;
|
|
45
|
+
for (const [df, dr] of gD) {
|
|
46
|
+
const nf = f + df, nr = r + dr;
|
|
47
|
+
if (inBoard(nf, nr)) bb |= bit(nr * 8 + nf);
|
|
48
|
+
}
|
|
49
|
+
kingAttacks[sq] = bb;
|
|
50
|
+
|
|
51
|
+
// Pawn attacks
|
|
52
|
+
// White
|
|
53
|
+
let w = 0n;
|
|
54
|
+
if (inBoard(f - 1, r + 1)) w |= bit((r + 1) * 8 + (f - 1));
|
|
55
|
+
if (inBoard(f + 1, r + 1)) w |= bit((r + 1) * 8 + (f + 1));
|
|
56
|
+
pawnAttacks[WHITE][sq] = w;
|
|
57
|
+
|
|
58
|
+
// Black
|
|
59
|
+
let b = 0n;
|
|
60
|
+
if (inBoard(f - 1, r - 1)) b |= bit((r - 1) * 8 + (f - 1));
|
|
61
|
+
if (inBoard(f + 1, r - 1)) b |= bit((r - 1) * 8 + (f + 1));
|
|
62
|
+
pawnAttacks[BLACK][sq] = b;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
initAttackTables();
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Sliding piece attacks from a square (for SAN resolution)
|
|
69
|
+
// ============================================================
|
|
70
|
+
function getRookAttacks(sq, occ) {
|
|
71
|
+
const f = sq % 8;
|
|
72
|
+
const r = (sq / 8) | 0;
|
|
73
|
+
let bb = 0n;
|
|
74
|
+
for (let nf = f + 1; nf < 8; nf++) {
|
|
75
|
+
const s = r * 8 + nf;
|
|
76
|
+
bb |= bit(s);
|
|
77
|
+
if (occ & bit(s)) break;
|
|
78
|
+
}
|
|
79
|
+
for (let nf = f - 1; nf >= 0; nf--) {
|
|
80
|
+
const s = r * 8 + nf;
|
|
81
|
+
bb |= bit(s);
|
|
82
|
+
if (occ & bit(s)) break;
|
|
83
|
+
}
|
|
84
|
+
for (let nr = r + 1; nr < 8; nr++) {
|
|
85
|
+
const s = nr * 8 + f;
|
|
86
|
+
bb |= bit(s);
|
|
87
|
+
if (occ & bit(s)) break;
|
|
88
|
+
}
|
|
89
|
+
for (let nr = r - 1; nr >= 0; nr--) {
|
|
90
|
+
const s = nr * 8 + f;
|
|
91
|
+
bb |= bit(s);
|
|
92
|
+
if (occ & bit(s)) break;
|
|
93
|
+
}
|
|
94
|
+
return bb;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getBishopAttacks(sq, occ) {
|
|
98
|
+
const f = sq % 8;
|
|
99
|
+
const r = (sq / 8) | 0;
|
|
100
|
+
let bb = 0n;
|
|
101
|
+
for (let d = 1; d < 8; d++) {
|
|
102
|
+
if (f + d >= 8 || r + d >= 8) break;
|
|
103
|
+
const s = (r + d) * 8 + (f + d);
|
|
104
|
+
bb |= bit(s);
|
|
105
|
+
if (occ & bit(s)) break;
|
|
106
|
+
}
|
|
107
|
+
for (let d = 1; d < 8; d++) {
|
|
108
|
+
if (f - d < 0 || r + d >= 8) break;
|
|
109
|
+
const s = (r + d) * 8 + (f - d);
|
|
110
|
+
bb |= bit(s);
|
|
111
|
+
if (occ & bit(s)) break;
|
|
112
|
+
}
|
|
113
|
+
for (let d = 1; d < 8; d++) {
|
|
114
|
+
if (f + d >= 8 || r - d < 0) break;
|
|
115
|
+
const s = (r - d) * 8 + (f + d);
|
|
116
|
+
bb |= bit(s);
|
|
117
|
+
if (occ & bit(s)) break;
|
|
118
|
+
}
|
|
119
|
+
for (let d = 1; d < 8; d++) {
|
|
120
|
+
if (f - d < 0 || r - d < 0) break;
|
|
121
|
+
const s = (r - d) * 8 + (f - d);
|
|
122
|
+
bb |= bit(s);
|
|
123
|
+
if (occ & bit(s)) break;
|
|
124
|
+
}
|
|
125
|
+
return bb;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** "e4" -> 28 (a1=0, h8=63). */
|
|
129
|
+
function squareToIndex(sqStr) {
|
|
130
|
+
const file = sqStr.charCodeAt(0) - 97;
|
|
131
|
+
const rank = parseInt(sqStr[1], 10) - 1;
|
|
132
|
+
return rank * 8 + file;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Squares from which a pawn of color can capture to toSq (bitboard). */
|
|
136
|
+
function pawnCaptureSources(toSq, color) {
|
|
137
|
+
const f = toSq % 8;
|
|
138
|
+
const r = (toSq / 8) | 0;
|
|
139
|
+
let bb = 0n;
|
|
140
|
+
if (color === WHITE) {
|
|
141
|
+
if (r >= 1 && f >= 1) bb |= bit((r - 1) * 8 + (f - 1));
|
|
142
|
+
if (r >= 1 && f < 7) bb |= bit((r - 1) * 8 + (f + 1));
|
|
143
|
+
} else {
|
|
144
|
+
if (r < 7 && f >= 1) bb |= bit((r + 1) * 8 + (f - 1));
|
|
145
|
+
if (r < 7 && f < 7) bb |= bit((r + 1) * 8 + (f + 1));
|
|
146
|
+
}
|
|
147
|
+
return bb;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Filter candidate bitboard by optional file/rank; return single sq index or -1. */
|
|
151
|
+
function filterDisamb(candidates, disambFile, disambRank) {
|
|
152
|
+
let bb = candidates;
|
|
153
|
+
if (disambFile !== undefined) {
|
|
154
|
+
const fileIdx = disambFile.charCodeAt(0) - 97;
|
|
155
|
+
const fileMask = (1n << BigInt(fileIdx)) | (1n << BigInt(8 + fileIdx)) | (1n << BigInt(16 + fileIdx)) | (1n << BigInt(24 + fileIdx)) | (1n << BigInt(32 + fileIdx)) | (1n << BigInt(40 + fileIdx)) | (1n << BigInt(48 + fileIdx)) | (1n << BigInt(56 + fileIdx));
|
|
156
|
+
bb = bb & fileMask;
|
|
157
|
+
}
|
|
158
|
+
if (disambRank !== undefined) {
|
|
159
|
+
const rankIdx = parseInt(disambRank, 10) - 1;
|
|
160
|
+
bb = bb & (0xffn << BigInt(rankIdx * 8));
|
|
161
|
+
}
|
|
162
|
+
let found = -1;
|
|
163
|
+
for (let sq = 0; sq < 64; sq++) {
|
|
164
|
+
if (bb & bit(sq)) {
|
|
165
|
+
if (found >= 0) return -1;
|
|
166
|
+
found = sq;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Parse SAN to { piece, targetIndex, disambFile, disambRank, promotion, castle }. piece: 'N'|'B'|'R'|'Q'|'K'|null (pawn). */
|
|
173
|
+
function parseSAN(san) {
|
|
174
|
+
const s = String(san).trim();
|
|
175
|
+
if (s === 'O-O' || s === 'O-O-O') {
|
|
176
|
+
return { piece: null, targetIndex: -1, disambFile: undefined, disambRank: undefined, promotion: undefined, castle: s === 'O-O' ? 'K' : 'Q' };
|
|
177
|
+
}
|
|
178
|
+
const targetPromo = s.match(/([a-h][1-8])(?:=([NBRQ]))?$/);
|
|
179
|
+
if (!targetPromo) return null;
|
|
180
|
+
const targetStr = targetPromo[1];
|
|
181
|
+
const promotion = targetPromo[2] ? targetPromo[2].toLowerCase() : undefined;
|
|
182
|
+
const targetIndex = squareToIndex(targetStr);
|
|
183
|
+
let rest = s.slice(0, s.length - targetPromo[0].length).replace(/x$/, '');
|
|
184
|
+
let piece = null;
|
|
185
|
+
let disambFile;
|
|
186
|
+
let disambRank;
|
|
187
|
+
if (rest.length > 0 && 'NBRQK'.includes(rest[0])) {
|
|
188
|
+
piece = rest[0];
|
|
189
|
+
rest = rest.slice(1);
|
|
190
|
+
}
|
|
191
|
+
if (rest.length >= 1) {
|
|
192
|
+
if (rest[0] >= 'a' && rest[0] <= 'h') disambFile = rest[0];
|
|
193
|
+
if (rest[rest.length - 1] >= '1' && rest[rest.length - 1] <= '8') disambRank = rest[rest.length - 1];
|
|
194
|
+
if (rest.length === 2 && rest[0] >= 'a' && rest[0] <= 'h' && rest[1] >= '1' && rest[1] <= '8') {
|
|
195
|
+
disambFile = rest[0];
|
|
196
|
+
disambRank = rest[1];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { piece, targetIndex, disambFile, disambRank, promotion, castle: undefined };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================
|
|
203
|
+
// Zobrist hashing (deterministic, seeded)
|
|
204
|
+
// ============================================================
|
|
205
|
+
function mulberry32(seed) {
|
|
206
|
+
return function () {
|
|
207
|
+
let t = (seed += 0x6d2b79f5);
|
|
208
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
209
|
+
t ^= t >>> 7;
|
|
210
|
+
return (t ^ (t >>> 12)) >>> 0;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function random64(prng) {
|
|
215
|
+
const lo = prng();
|
|
216
|
+
const hi = prng();
|
|
217
|
+
return (BigInt(hi) << 32n) | BigInt(lo);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const ZOBRIST_PIECES = [];
|
|
221
|
+
const ZOBRIST_CASTLE = [];
|
|
222
|
+
const ZOBRIST_EP = [];
|
|
223
|
+
const ZOBRIST_SIDE = { value: 0n };
|
|
224
|
+
|
|
225
|
+
(function initZobrist() {
|
|
226
|
+
const prng = mulberry32(0x5eed);
|
|
227
|
+
for (let sq = 0; sq < 64; sq++) {
|
|
228
|
+
ZOBRIST_PIECES[sq] = [];
|
|
229
|
+
for (let pt = 0; pt < 12; pt++) ZOBRIST_PIECES[sq][pt] = random64(prng);
|
|
230
|
+
}
|
|
231
|
+
ZOBRIST_SIDE.value = random64(prng);
|
|
232
|
+
for (let i = 0; i < 4; i++) ZOBRIST_CASTLE.push(random64(prng));
|
|
233
|
+
for (let f = 0; f < 8; f++) ZOBRIST_EP.push(random64(prng));
|
|
234
|
+
})();
|
|
235
|
+
|
|
236
|
+
// ============================================================
|
|
237
|
+
// Engine Class
|
|
238
|
+
// ============================================================
|
|
239
|
+
|
|
240
|
+
class BitboardChess {
|
|
241
|
+
constructor() {
|
|
242
|
+
this.reset();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
reset() {
|
|
246
|
+
// Piece bitboards
|
|
247
|
+
this.pawns = [0n, 0n];
|
|
248
|
+
this.knights = [0n, 0n];
|
|
249
|
+
this.bishops = [0n, 0n];
|
|
250
|
+
this.rooks = [0n, 0n];
|
|
251
|
+
this.queens = [0n, 0n];
|
|
252
|
+
this.kings = [0n, 0n];
|
|
253
|
+
|
|
254
|
+
// Game state
|
|
255
|
+
this.sideToMove = WHITE;
|
|
256
|
+
this.castling = "KQkq";
|
|
257
|
+
this.enPassant = -1;
|
|
258
|
+
this.halfmove = 0;
|
|
259
|
+
this.fullmove = 1;
|
|
260
|
+
|
|
261
|
+
this._setInitialPosition();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_setInitialPosition() {
|
|
265
|
+
this.pawns[WHITE] = 0x000000000000FF00n;
|
|
266
|
+
this.pawns[BLACK] = 0x00FF000000000000n;
|
|
267
|
+
|
|
268
|
+
this.rooks[WHITE] = 0x0000000000000081n;
|
|
269
|
+
this.rooks[BLACK] = 0x8100000000000000n;
|
|
270
|
+
|
|
271
|
+
this.knights[WHITE] = 0x0000000000000042n;
|
|
272
|
+
this.knights[BLACK] = 0x4200000000000000n;
|
|
273
|
+
|
|
274
|
+
this.bishops[WHITE] = 0x0000000000000024n;
|
|
275
|
+
this.bishops[BLACK] = 0x2400000000000000n;
|
|
276
|
+
|
|
277
|
+
this.queens[WHITE] = 0x0000000000000008n;
|
|
278
|
+
this.queens[BLACK] = 0x0800000000000000n;
|
|
279
|
+
|
|
280
|
+
this.kings[WHITE] = 0x0000000000000010n;
|
|
281
|
+
this.kings[BLACK] = 0x1000000000000000n;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Set position from FEN (piece placement, side, castling, en passant).
|
|
286
|
+
* Used for ECO Zobrist indexing and any FEN→key use. No validation.
|
|
287
|
+
*/
|
|
288
|
+
loadFromFEN(fen) {
|
|
289
|
+
if (!fen || typeof fen !== 'string') return;
|
|
290
|
+
const tokens = fen.trim().split(/\s+/);
|
|
291
|
+
const placement = tokens[0];
|
|
292
|
+
if (!placement) return;
|
|
293
|
+
const ranks = placement.split('/');
|
|
294
|
+
if (ranks.length !== 8) return;
|
|
295
|
+
|
|
296
|
+
this.pawns[WHITE] = 0n;
|
|
297
|
+
this.pawns[BLACK] = 0n;
|
|
298
|
+
this.knights[WHITE] = 0n;
|
|
299
|
+
this.knights[BLACK] = 0n;
|
|
300
|
+
this.bishops[WHITE] = 0n;
|
|
301
|
+
this.bishops[BLACK] = 0n;
|
|
302
|
+
this.rooks[WHITE] = 0n;
|
|
303
|
+
this.rooks[BLACK] = 0n;
|
|
304
|
+
this.queens[WHITE] = 0n;
|
|
305
|
+
this.queens[BLACK] = 0n;
|
|
306
|
+
this.kings[WHITE] = 0n;
|
|
307
|
+
this.kings[BLACK] = 0n;
|
|
308
|
+
|
|
309
|
+
for (let r = 0; r < 8; r++) {
|
|
310
|
+
const rankStr = ranks[7 - r];
|
|
311
|
+
let f = 0;
|
|
312
|
+
for (const ch of rankStr) {
|
|
313
|
+
if (f >= 8) break;
|
|
314
|
+
if (ch >= '1' && ch <= '8') {
|
|
315
|
+
f += parseInt(ch, 10);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const sq = r * 8 + f;
|
|
319
|
+
const b = bit(sq);
|
|
320
|
+
if (ch === 'P') this.pawns[WHITE] |= b;
|
|
321
|
+
else if (ch === 'N') this.knights[WHITE] |= b;
|
|
322
|
+
else if (ch === 'B') this.bishops[WHITE] |= b;
|
|
323
|
+
else if (ch === 'R') this.rooks[WHITE] |= b;
|
|
324
|
+
else if (ch === 'Q') this.queens[WHITE] |= b;
|
|
325
|
+
else if (ch === 'K') this.kings[WHITE] |= b;
|
|
326
|
+
else if (ch === 'p') this.pawns[BLACK] |= b;
|
|
327
|
+
else if (ch === 'n') this.knights[BLACK] |= b;
|
|
328
|
+
else if (ch === 'b') this.bishops[BLACK] |= b;
|
|
329
|
+
else if (ch === 'r') this.rooks[BLACK] |= b;
|
|
330
|
+
else if (ch === 'q') this.queens[BLACK] |= b;
|
|
331
|
+
else if (ch === 'k') this.kings[BLACK] |= b;
|
|
332
|
+
f++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
this.sideToMove = tokens[1] === 'b' ? BLACK : WHITE;
|
|
337
|
+
this.castling = (tokens[2] && tokens[2] !== '-') ? tokens[2] : '';
|
|
338
|
+
const ep = tokens[3];
|
|
339
|
+
if (ep && ep !== '-') {
|
|
340
|
+
const file = ep.charCodeAt(0) - 97;
|
|
341
|
+
const rank = parseInt(ep[1], 10) - 1;
|
|
342
|
+
if (file >= 0 && file < 8 && rank >= 0 && rank < 8) this.enPassant = rank * 8 + file;
|
|
343
|
+
else this.enPassant = -1;
|
|
344
|
+
} else {
|
|
345
|
+
this.enPassant = -1;
|
|
346
|
+
}
|
|
347
|
+
this.halfmove = parseInt(tokens[4], 10) || 0;
|
|
348
|
+
this.fullmove = parseInt(tokens[5], 10) || 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ============================================================
|
|
352
|
+
// Utility
|
|
353
|
+
// ============================================================
|
|
354
|
+
|
|
355
|
+
occupancy(color) {
|
|
356
|
+
return (
|
|
357
|
+
this.pawns[color] |
|
|
358
|
+
this.knights[color] |
|
|
359
|
+
this.bishops[color] |
|
|
360
|
+
this.rooks[color] |
|
|
361
|
+
this.queens[color] |
|
|
362
|
+
this.kings[color]
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
allOcc() {
|
|
367
|
+
return this.occupancy(WHITE) | this.occupancy(BLACK);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ============================================================
|
|
371
|
+
// SAN resolution (reachability + disambiguation; no legality check)
|
|
372
|
+
// ============================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Resolve SAN to move object { from, to, promotion?, castle?, enpassant? }.
|
|
376
|
+
* Returns null if SAN cannot be resolved (ambiguous or no matching piece).
|
|
377
|
+
*/
|
|
378
|
+
resolveSAN(san) {
|
|
379
|
+
const p = parseSAN(san);
|
|
380
|
+
if (!p) return null;
|
|
381
|
+
const side = this.sideToMove;
|
|
382
|
+
const occ = this.allOcc();
|
|
383
|
+
const toSq = p.targetIndex;
|
|
384
|
+
|
|
385
|
+
if (p.castle) {
|
|
386
|
+
const fromSq = side === WHITE ? 4 : 60;
|
|
387
|
+
const toSqKing = p.castle === 'K' ? (side === WHITE ? 6 : 62) : (side === WHITE ? 2 : 58);
|
|
388
|
+
return { from: fromSq, to: toSqKing, castle: p.castle };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let candidates;
|
|
392
|
+
if (p.piece === 'K') {
|
|
393
|
+
candidates = kingAttacks[toSq] & this.kings[side];
|
|
394
|
+
} else if (p.piece === 'N') {
|
|
395
|
+
candidates = knightAttacks[toSq] & this.knights[side];
|
|
396
|
+
} else if (p.piece === 'R') {
|
|
397
|
+
candidates = getRookAttacks(toSq, occ) & this.rooks[side];
|
|
398
|
+
} else if (p.piece === 'B') {
|
|
399
|
+
candidates = getBishopAttacks(toSq, occ) & this.bishops[side];
|
|
400
|
+
} else if (p.piece === 'Q') {
|
|
401
|
+
candidates = (getRookAttacks(toSq, occ) | getBishopAttacks(toSq, occ)) & this.queens[side];
|
|
402
|
+
} else {
|
|
403
|
+
// Pawn
|
|
404
|
+
if (p.disambFile !== undefined) {
|
|
405
|
+
// Capture (e.g. exd5, exd6 e.p.): squares from which a pawn can capture to toSq
|
|
406
|
+
candidates = pawnCaptureSources(toSq, side) & this.pawns[side];
|
|
407
|
+
const fileIdx = p.disambFile.charCodeAt(0) - 97;
|
|
408
|
+
const fileMask = (1n << BigInt(fileIdx)) | (1n << BigInt(8 + fileIdx)) | (1n << BigInt(16 + fileIdx)) | (1n << BigInt(24 + fileIdx)) | (1n << BigInt(32 + fileIdx)) | (1n << BigInt(40 + fileIdx)) | (1n << BigInt(48 + fileIdx)) | (1n << BigInt(56 + fileIdx));
|
|
409
|
+
candidates = candidates & fileMask;
|
|
410
|
+
} else {
|
|
411
|
+
// Push
|
|
412
|
+
const oneBack = side === WHITE ? toSq - 8 : toSq + 8;
|
|
413
|
+
const twoBack = side === WHITE ? toSq - 16 : toSq + 16;
|
|
414
|
+
const toRank = (toSq / 8) | 0;
|
|
415
|
+
if (this.pawns[side] & bit(oneBack)) {
|
|
416
|
+
candidates = bit(oneBack);
|
|
417
|
+
} else if ((side === WHITE ? toRank === 3 : toRank === 4) && (this.pawns[side] & bit(twoBack)) && !(occ & bit(oneBack))) {
|
|
418
|
+
candidates = bit(twoBack);
|
|
419
|
+
} else {
|
|
420
|
+
candidates = 0n;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let fromSq = filterDisamb(candidates, p.disambFile, p.disambRank);
|
|
426
|
+
if (fromSq < 0 && candidates) {
|
|
427
|
+
let count = 0;
|
|
428
|
+
for (let sq = 0; sq < 64; sq++) {
|
|
429
|
+
if (candidates & bit(sq)) { count++; fromSq = sq; if (count > 1) break; }
|
|
430
|
+
}
|
|
431
|
+
if (count !== 1) fromSq = -1;
|
|
432
|
+
}
|
|
433
|
+
if (fromSq < 0) return null;
|
|
434
|
+
|
|
435
|
+
const move = { from: fromSq, to: toSq };
|
|
436
|
+
if (p.promotion) move.promotion = p.promotion;
|
|
437
|
+
if (p.piece === null && p.disambFile !== undefined && !(occ & bit(toSq))) move.enpassant = true;
|
|
438
|
+
return move;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Apply a move given in SAN. No validation; assumes valid (e.g. from PGN).
|
|
443
|
+
* Use makeMove(move) for raw move objects.
|
|
444
|
+
*/
|
|
445
|
+
makeMoveSAN(san) {
|
|
446
|
+
const move = typeof san === 'string' ? this.resolveSAN(san) : san;
|
|
447
|
+
if (!move) return false;
|
|
448
|
+
this.makeMove(move);
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================================
|
|
453
|
+
// Move Making (NO VALIDATION)
|
|
454
|
+
// move = { from, to, promotion, castle, enpassant }
|
|
455
|
+
// ============================================================
|
|
456
|
+
|
|
457
|
+
makeMove(move) {
|
|
458
|
+
const side = this.sideToMove;
|
|
459
|
+
const enemy = side ^ 1;
|
|
460
|
+
|
|
461
|
+
const fromBB = bit(move.from);
|
|
462
|
+
const toBB = bit(move.to);
|
|
463
|
+
|
|
464
|
+
// -------------------------
|
|
465
|
+
// 1. Handle castling
|
|
466
|
+
// -------------------------
|
|
467
|
+
if (move.castle) {
|
|
468
|
+
if (move.castle === "K") {
|
|
469
|
+
// King-side
|
|
470
|
+
this.kings[side] ^= fromBB | toBB;
|
|
471
|
+
if (side === WHITE) {
|
|
472
|
+
this.rooks[WHITE] ^= bit(7) | bit(5);
|
|
473
|
+
} else {
|
|
474
|
+
this.rooks[BLACK] ^= bit(63) | bit(61);
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// Queen-side
|
|
478
|
+
this.kings[side] ^= fromBB | toBB;
|
|
479
|
+
if (side === WHITE) {
|
|
480
|
+
this.rooks[WHITE] ^= bit(0) | bit(3);
|
|
481
|
+
} else {
|
|
482
|
+
this.rooks[BLACK] ^= bit(56) | bit(59);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Remove castling rights
|
|
487
|
+
this.castling = this.castling.replace(side === WHITE ? /K|Q/g : /k|q/g, "");
|
|
488
|
+
this._finishMove();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// -------------------------
|
|
493
|
+
// 2. Handle en-passant capture
|
|
494
|
+
// -------------------------
|
|
495
|
+
if (move.enpassant) {
|
|
496
|
+
const capSq = side === WHITE ? move.to - 8 : move.to + 8;
|
|
497
|
+
const capBB = bit(capSq);
|
|
498
|
+
this.pawns[enemy] &= ~capBB;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// -------------------------
|
|
502
|
+
// 3. Remove captured piece (normal capture)
|
|
503
|
+
// -------------------------
|
|
504
|
+
const removeAt = (arr) => (arr[enemy] &= ~toBB);
|
|
505
|
+
[this.pawns, this.knights, this.bishops, this.rooks, this.queens, this.kings]
|
|
506
|
+
.forEach(removeAt);
|
|
507
|
+
|
|
508
|
+
// -------------------------
|
|
509
|
+
// 4. Move our piece
|
|
510
|
+
// -------------------------
|
|
511
|
+
const movePiece = (arr) => {
|
|
512
|
+
if (arr[side] & fromBB) {
|
|
513
|
+
arr[side] ^= fromBB;
|
|
514
|
+
arr[side] |= toBB;
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
return false;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
let movedPawn = false;
|
|
521
|
+
const movedRook = !!(this.rooks[side] & fromBB);
|
|
522
|
+
|
|
523
|
+
if (movePiece(this.pawns)) movedPawn = true;
|
|
524
|
+
else if (movePiece(this.knights));
|
|
525
|
+
else if (movePiece(this.bishops));
|
|
526
|
+
else if (movePiece(this.rooks));
|
|
527
|
+
else if (movePiece(this.queens));
|
|
528
|
+
else movePiece(this.kings);
|
|
529
|
+
|
|
530
|
+
// -------------------------
|
|
531
|
+
// 5. Promotion
|
|
532
|
+
// -------------------------
|
|
533
|
+
if (move.promotion) {
|
|
534
|
+
// Remove pawn
|
|
535
|
+
this.pawns[side] &= ~toBB;
|
|
536
|
+
|
|
537
|
+
const target =
|
|
538
|
+
move.promotion === "q" ? this.queens :
|
|
539
|
+
move.promotion === "r" ? this.rooks :
|
|
540
|
+
move.promotion === "b" ? this.bishops :
|
|
541
|
+
this.knights;
|
|
542
|
+
|
|
543
|
+
target[side] |= toBB;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// -------------------------
|
|
547
|
+
// 6. Update en-passant square
|
|
548
|
+
// -------------------------
|
|
549
|
+
if (movedPawn && Math.abs(move.to - move.from) === 16) {
|
|
550
|
+
this.enPassant = (move.from + move.to) >> 1;
|
|
551
|
+
} else {
|
|
552
|
+
this.enPassant = -1;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// -------------------------
|
|
556
|
+
// 7. Remove castling rights if king or rook moved
|
|
557
|
+
// -------------------------
|
|
558
|
+
if (fromBB & this.kings[side]) {
|
|
559
|
+
this.castling = this.castling.replace(side === WHITE ? /K|Q/g : /k|q/g, "");
|
|
560
|
+
}
|
|
561
|
+
if (movedRook) {
|
|
562
|
+
if (move.from === 0) this.castling = this.castling.replace("Q", "");
|
|
563
|
+
if (move.from === 7) this.castling = this.castling.replace("K", "");
|
|
564
|
+
if (move.from === 56) this.castling = this.castling.replace("q", "");
|
|
565
|
+
if (move.from === 63) this.castling = this.castling.replace("k", "");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// -------------------------
|
|
569
|
+
// 8. Finish move
|
|
570
|
+
// -------------------------
|
|
571
|
+
this._finishMove();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_finishMove() {
|
|
575
|
+
this.sideToMove ^= 1;
|
|
576
|
+
if (this.sideToMove === WHITE) this.fullmove++;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ============================================================
|
|
580
|
+
// Zobrist key (from current position; deterministic)
|
|
581
|
+
// ============================================================
|
|
582
|
+
|
|
583
|
+
getZobristKey() {
|
|
584
|
+
let key = 0n;
|
|
585
|
+
const pieceSets = [
|
|
586
|
+
[this.pawns, 0, 6],
|
|
587
|
+
[this.knights, 1, 7],
|
|
588
|
+
[this.bishops, 2, 8],
|
|
589
|
+
[this.rooks, 3, 9],
|
|
590
|
+
[this.queens, 4, 10],
|
|
591
|
+
[this.kings, 5, 11],
|
|
592
|
+
];
|
|
593
|
+
for (let sq = 0; sq < 64; sq++) {
|
|
594
|
+
const b = bit(sq);
|
|
595
|
+
for (const [arr, wPt, bPt] of pieceSets) {
|
|
596
|
+
if (arr[WHITE] & b) {
|
|
597
|
+
key ^= ZOBRIST_PIECES[sq][wPt];
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
if (arr[BLACK] & b) {
|
|
601
|
+
key ^= ZOBRIST_PIECES[sq][bPt];
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (this.sideToMove === BLACK) key ^= ZOBRIST_SIDE.value;
|
|
607
|
+
const castlingIndex = { K: 0, Q: 1, k: 2, q: 3 };
|
|
608
|
+
for (const c of this.castling) key ^= ZOBRIST_CASTLE[castlingIndex[c]];
|
|
609
|
+
if (this.enPassant >= 0) key ^= ZOBRIST_EP[this.enPassant % 8];
|
|
610
|
+
return key;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ============================================================
|
|
614
|
+
// FEN
|
|
615
|
+
// ============================================================
|
|
616
|
+
|
|
617
|
+
toFEN() {
|
|
618
|
+
let fen = "";
|
|
619
|
+
|
|
620
|
+
for (let r = 7; r >= 0; r--) {
|
|
621
|
+
let empty = 0;
|
|
622
|
+
for (let f = 0; f < 8; f++) {
|
|
623
|
+
const sq = r * 8 + f;
|
|
624
|
+
const bb = bit(sq);
|
|
625
|
+
|
|
626
|
+
let piece = null;
|
|
627
|
+
|
|
628
|
+
const check = (arr, w, b) =>
|
|
629
|
+
arr[WHITE] & bb ? w :
|
|
630
|
+
arr[BLACK] & bb ? b : null;
|
|
631
|
+
|
|
632
|
+
piece =
|
|
633
|
+
check(this.pawns, "P", "p") ||
|
|
634
|
+
check(this.knights, "N", "n") ||
|
|
635
|
+
check(this.bishops, "B", "b") ||
|
|
636
|
+
check(this.rooks, "R", "r") ||
|
|
637
|
+
check(this.queens, "Q", "q") ||
|
|
638
|
+
check(this.kings, "K", "k");
|
|
639
|
+
|
|
640
|
+
if (!piece) {
|
|
641
|
+
empty++;
|
|
642
|
+
} else {
|
|
643
|
+
if (empty) {
|
|
644
|
+
fen += empty;
|
|
645
|
+
empty = 0;
|
|
646
|
+
}
|
|
647
|
+
fen += piece;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (empty) fen += empty;
|
|
651
|
+
if (r > 0) fen += "/";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fen += " ";
|
|
655
|
+
fen += this.sideToMove === WHITE ? "w" : "b";
|
|
656
|
+
fen += " ";
|
|
657
|
+
fen += this.castling || "-";
|
|
658
|
+
fen += " ";
|
|
659
|
+
fen += this.enPassant === -1
|
|
660
|
+
? "-"
|
|
661
|
+
: String.fromCharCode("a".charCodeAt(0) + (this.enPassant % 8)) +
|
|
662
|
+
(Math.floor(this.enPassant / 8) + 1);
|
|
663
|
+
fen += " ";
|
|
664
|
+
fen += this.halfmove;
|
|
665
|
+
fen += " ";
|
|
666
|
+
fen += this.fullmove;
|
|
667
|
+
|
|
668
|
+
return fen;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export default BitboardChess;
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bitboard-chess",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight bitboard chess engine for position updates (FEN, Zobrist, SAN). No move validation; assumes validated input.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"exports": "./index.mjs",
|
|
8
|
+
"files": ["index.mjs"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "mocha test/**/*.cjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["chess", "bitboard", "fen", "zobrist"],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"chai": "^4.0.2",
|
|
16
|
+
"chess.js": "^0.10.2",
|
|
17
|
+
"mocha": "^11.1.0"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">= 11.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|