@connectorvol/chessops 0.15.1

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.
Files changed (93) hide show
  1. package/LICENSE.txt +674 -0
  2. package/README.md +61 -0
  3. package/dist/cjs/attacks.js +152 -0
  4. package/dist/cjs/attacks.js.map +1 -0
  5. package/dist/cjs/board.js +143 -0
  6. package/dist/cjs/board.js.map +1 -0
  7. package/dist/cjs/chess.js +638 -0
  8. package/dist/cjs/chess.js.map +1 -0
  9. package/dist/cjs/compat.js +89 -0
  10. package/dist/cjs/compat.js.map +1 -0
  11. package/dist/cjs/debug.js +103 -0
  12. package/dist/cjs/debug.js.map +1 -0
  13. package/dist/cjs/fen.js +325 -0
  14. package/dist/cjs/fen.js.map +1 -0
  15. package/dist/cjs/index.js +94 -0
  16. package/dist/cjs/index.js.map +1 -0
  17. package/dist/cjs/pgn.js +796 -0
  18. package/dist/cjs/pgn.js.map +1 -0
  19. package/dist/cjs/san.js +174 -0
  20. package/dist/cjs/san.js.map +1 -0
  21. package/dist/cjs/setup.js +167 -0
  22. package/dist/cjs/setup.js.map +1 -0
  23. package/dist/cjs/squareSet.js +206 -0
  24. package/dist/cjs/squareSet.js.map +1 -0
  25. package/dist/cjs/transform.js +57 -0
  26. package/dist/cjs/transform.js.map +1 -0
  27. package/dist/cjs/types.js +24 -0
  28. package/dist/cjs/types.js.map +1 -0
  29. package/dist/cjs/util.js +104 -0
  30. package/dist/cjs/util.js.map +1 -0
  31. package/dist/cjs/variant.js +833 -0
  32. package/dist/cjs/variant.js.map +1 -0
  33. package/dist/esm/attacks.js +140 -0
  34. package/dist/esm/attacks.js.map +1 -0
  35. package/dist/esm/board.js +138 -0
  36. package/dist/esm/board.js.map +1 -0
  37. package/dist/esm/chess.js +624 -0
  38. package/dist/esm/chess.js.map +1 -0
  39. package/dist/esm/compat.js +81 -0
  40. package/dist/esm/compat.js.map +1 -0
  41. package/dist/esm/debug.js +94 -0
  42. package/dist/esm/debug.js.map +1 -0
  43. package/dist/esm/fen.js +308 -0
  44. package/dist/esm/fen.js.map +1 -0
  45. package/dist/esm/index.js +15 -0
  46. package/dist/esm/index.js.map +1 -0
  47. package/dist/esm/pgn.js +769 -0
  48. package/dist/esm/pgn.js.map +1 -0
  49. package/dist/esm/san.js +167 -0
  50. package/dist/esm/san.js.map +1 -0
  51. package/dist/esm/setup.js +157 -0
  52. package/dist/esm/setup.js.map +1 -0
  53. package/dist/esm/squareSet.js +202 -0
  54. package/dist/esm/squareSet.js.map +1 -0
  55. package/dist/esm/transform.js +48 -0
  56. package/dist/esm/transform.js.map +1 -0
  57. package/dist/esm/types.js +19 -0
  58. package/dist/esm/types.js.map +1 -0
  59. package/dist/esm/util.js +87 -0
  60. package/dist/esm/util.js.map +1 -0
  61. package/dist/esm/variant.js +812 -0
  62. package/dist/esm/variant.js.map +1 -0
  63. package/dist/types/attacks.d.ts +58 -0
  64. package/dist/types/board.d.ts +62 -0
  65. package/dist/types/chess.d.ts +82 -0
  66. package/dist/types/compat.d.ts +26 -0
  67. package/dist/types/debug.d.ts +10 -0
  68. package/dist/types/fen.d.ts +40 -0
  69. package/dist/types/index.d.ts +14 -0
  70. package/dist/types/pgn.d.ts +203 -0
  71. package/dist/types/san.d.ts +6 -0
  72. package/dist/types/setup.d.ts +65 -0
  73. package/dist/types/squareSet.d.ts +50 -0
  74. package/dist/types/transform.d.ts +9 -0
  75. package/dist/types/types.d.ts +58 -0
  76. package/dist/types/util.d.ts +21 -0
  77. package/dist/types/variant.d.ts +92 -0
  78. package/package.json +86 -0
  79. package/src/attacks.ts +160 -0
  80. package/src/board.ts +168 -0
  81. package/src/chess.ts +687 -0
  82. package/src/compat.ts +120 -0
  83. package/src/debug.ts +100 -0
  84. package/src/fen.ts +328 -0
  85. package/src/index.ts +85 -0
  86. package/src/pgn.ts +876 -0
  87. package/src/san.ts +190 -0
  88. package/src/setup.ts +203 -0
  89. package/src/squareSet.ts +243 -0
  90. package/src/transform.ts +49 -0
  91. package/src/types.ts +93 -0
  92. package/src/util.ts +116 -0
  93. package/src/variant.ts +939 -0
package/src/chess.ts ADDED
@@ -0,0 +1,687 @@
1
+ import { Result } from "@badrap/result";
2
+ import {
3
+ attacks,
4
+ between,
5
+ bishopAttacks,
6
+ kingAttacks,
7
+ knightAttacks,
8
+ pawnAttacks,
9
+ queenAttacks,
10
+ ray,
11
+ rookAttacks,
12
+ } from "./attacks.js";
13
+ import { Board, boardEquals } from "./board.js";
14
+ import { Material, RemainingChecks, Setup } from "./setup.js";
15
+ import { SquareSet } from "./squareSet.js";
16
+ import {
17
+ ByCastlingSide,
18
+ ByColor,
19
+ CASTLING_SIDES,
20
+ CastlingSide,
21
+ Color,
22
+ COLORS,
23
+ isDrop,
24
+ Move,
25
+ NormalMove,
26
+ Outcome,
27
+ Piece,
28
+ Rules,
29
+ Square,
30
+ } from "./types.js";
31
+ import { defined, kingCastlesTo, opposite, rookCastlesTo, squareRank } from "./util.js";
32
+
33
+ export enum IllegalSetup {
34
+ Empty = "ERR_EMPTY",
35
+ OppositeCheck = "ERR_OPPOSITE_CHECK",
36
+ PawnsOnBackrank = "ERR_PAWNS_ON_BACKRANK",
37
+ Kings = "ERR_KINGS",
38
+ Variant = "ERR_VARIANT",
39
+ }
40
+
41
+ export class PositionError extends Error {}
42
+
43
+ const attacksTo = (square: Square, attacker: Color, board: Board, occupied: SquareSet): SquareSet =>
44
+ board[attacker].intersect(
45
+ rookAttacks(square, occupied)
46
+ .intersect(board.rooksAndQueens())
47
+ .union(bishopAttacks(square, occupied).intersect(board.bishopsAndQueens()))
48
+ .union(knightAttacks(square).intersect(board.knight))
49
+ .union(kingAttacks(square).intersect(board.king))
50
+ .union(pawnAttacks(opposite(attacker), square).intersect(board.pawn)),
51
+ );
52
+
53
+ export class Castles {
54
+ castlingRights: SquareSet;
55
+ rook: ByColor<ByCastlingSide<Square | undefined>>;
56
+ path: ByColor<ByCastlingSide<SquareSet>>;
57
+
58
+ private constructor() {}
59
+
60
+ static default(): Castles {
61
+ const castles = new Castles();
62
+ castles.castlingRights = SquareSet.corners();
63
+ castles.rook = {
64
+ white: { a: 0, h: 7 },
65
+ black: { a: 56, h: 63 },
66
+ };
67
+ castles.path = {
68
+ white: { a: new SquareSet(0xe, 0), h: new SquareSet(0x60, 0) },
69
+ black: { a: new SquareSet(0, 0x0e000000), h: new SquareSet(0, 0x60000000) },
70
+ };
71
+ return castles;
72
+ }
73
+
74
+ static empty(): Castles {
75
+ const castles = new Castles();
76
+ castles.castlingRights = SquareSet.empty();
77
+ castles.rook = {
78
+ white: { a: undefined, h: undefined },
79
+ black: { a: undefined, h: undefined },
80
+ };
81
+ castles.path = {
82
+ white: { a: SquareSet.empty(), h: SquareSet.empty() },
83
+ black: { a: SquareSet.empty(), h: SquareSet.empty() },
84
+ };
85
+ return castles;
86
+ }
87
+
88
+ clone(): Castles {
89
+ const castles = new Castles();
90
+ castles.castlingRights = this.castlingRights;
91
+ castles.rook = {
92
+ white: { a: this.rook.white.a, h: this.rook.white.h },
93
+ black: { a: this.rook.black.a, h: this.rook.black.h },
94
+ };
95
+ castles.path = {
96
+ white: { a: this.path.white.a, h: this.path.white.h },
97
+ black: { a: this.path.black.a, h: this.path.black.h },
98
+ };
99
+ return castles;
100
+ }
101
+
102
+ private add(color: Color, side: CastlingSide, king: Square, rook: Square): void {
103
+ const kingTo = kingCastlesTo(color, side);
104
+ const rookTo = rookCastlesTo(color, side);
105
+ this.castlingRights = this.castlingRights.with(rook);
106
+ this.rook[color][side] = rook;
107
+ this.path[color][side] = between(rook, rookTo)
108
+ .with(rookTo)
109
+ .union(between(king, kingTo).with(kingTo))
110
+ .without(king)
111
+ .without(rook);
112
+ }
113
+
114
+ static fromSetup(setup: Setup): Castles {
115
+ const castles = Castles.empty();
116
+ const rooks = setup.castlingRights.intersect(setup.board.rook);
117
+ for (const color of COLORS) {
118
+ const backrank = SquareSet.backrank(color);
119
+ const king = setup.board.kingOf(color);
120
+ if (!defined(king) || !backrank.has(king)) continue;
121
+ const side = rooks.intersect(setup.board[color]).intersect(backrank);
122
+ const aSide = side.first();
123
+ if (defined(aSide) && aSide < king) castles.add(color, "a", king, aSide);
124
+ const hSide = side.last();
125
+ if (defined(hSide) && king < hSide) castles.add(color, "h", king, hSide);
126
+ }
127
+ return castles;
128
+ }
129
+
130
+ discardRook(square: Square): void {
131
+ if (this.castlingRights.has(square)) {
132
+ this.castlingRights = this.castlingRights.without(square);
133
+ for (const color of COLORS) {
134
+ for (const side of CASTLING_SIDES) {
135
+ if (this.rook[color][side] === square) this.rook[color][side] = undefined;
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ discardColor(color: Color): void {
142
+ this.castlingRights = this.castlingRights.diff(SquareSet.backrank(color));
143
+ this.rook[color].a = undefined;
144
+ this.rook[color].h = undefined;
145
+ }
146
+ }
147
+
148
+ export interface Context {
149
+ king: Square | undefined;
150
+ blockers: SquareSet;
151
+ checkers: SquareSet;
152
+ variantEnd: boolean;
153
+ mustCapture: boolean;
154
+ }
155
+
156
+ export abstract class Position {
157
+ board: Board;
158
+ pockets: Material | undefined;
159
+ turn: Color;
160
+ castles: Castles;
161
+ epSquare: Square | undefined;
162
+ remainingChecks: RemainingChecks | undefined;
163
+ halfmoves: number;
164
+ fullmoves: number;
165
+
166
+ protected constructor(readonly rules: Rules) {}
167
+
168
+ reset() {
169
+ this.board = Board.default();
170
+ this.pockets = undefined;
171
+ this.turn = "white";
172
+ this.castles = Castles.default();
173
+ this.epSquare = undefined;
174
+ this.remainingChecks = undefined;
175
+ this.halfmoves = 0;
176
+ this.fullmoves = 1;
177
+ }
178
+
179
+ protected setupUnchecked(setup: Setup) {
180
+ this.board = setup.board.clone();
181
+ this.board.promoted = SquareSet.empty();
182
+ this.pockets = undefined;
183
+ this.turn = setup.turn;
184
+ this.castles = Castles.fromSetup(setup);
185
+ this.epSquare = validEpSquare(this, setup.epSquare);
186
+ this.remainingChecks = undefined;
187
+ this.halfmoves = setup.halfmoves;
188
+ this.fullmoves = setup.fullmoves;
189
+ }
190
+
191
+ // When subclassing overwrite at least:
192
+ //
193
+ // - static default()
194
+ // - static fromSetup()
195
+ // - static clone()
196
+ //
197
+ // - dests()
198
+ // - isVariantEnd()
199
+ // - variantOutcome()
200
+ // - hasInsufficientMaterial()
201
+ // - isStandardMaterial()
202
+
203
+ kingAttackers(square: Square, attacker: Color, occupied: SquareSet): SquareSet {
204
+ return attacksTo(square, attacker, this.board, occupied);
205
+ }
206
+
207
+ protected playCaptureAt(square: Square, captured: Piece): void {
208
+ this.halfmoves = 0;
209
+ if (captured.role === "rook") this.castles.discardRook(square);
210
+ if (this.pockets)
211
+ this.pockets[opposite(captured.color)][captured.promoted ? "pawn" : captured.role]++;
212
+ }
213
+
214
+ ctx(): Context {
215
+ const variantEnd = this.isVariantEnd();
216
+ const king = this.board.kingOf(this.turn);
217
+ if (!defined(king)) {
218
+ return {
219
+ king,
220
+ blockers: SquareSet.empty(),
221
+ checkers: SquareSet.empty(),
222
+ variantEnd,
223
+ mustCapture: false,
224
+ };
225
+ }
226
+ const snipers = rookAttacks(king, SquareSet.empty())
227
+ .intersect(this.board.rooksAndQueens())
228
+ .union(bishopAttacks(king, SquareSet.empty()).intersect(this.board.bishopsAndQueens()))
229
+ .intersect(this.board[opposite(this.turn)]);
230
+ let blockers = SquareSet.empty();
231
+ for (const sniper of snipers) {
232
+ const b = between(king, sniper).intersect(this.board.occupied);
233
+ if (!b.moreThanOne()) blockers = blockers.union(b);
234
+ }
235
+ const checkers = this.kingAttackers(king, opposite(this.turn), this.board.occupied);
236
+ return {
237
+ king,
238
+ blockers,
239
+ checkers,
240
+ variantEnd,
241
+ mustCapture: false,
242
+ };
243
+ }
244
+
245
+ clone(): Position {
246
+ const pos = new (this as any).constructor();
247
+ pos.board = this.board.clone();
248
+ pos.pockets = this.pockets?.clone();
249
+ pos.turn = this.turn;
250
+ pos.castles = this.castles.clone();
251
+ pos.epSquare = this.epSquare;
252
+ pos.remainingChecks = this.remainingChecks?.clone();
253
+ pos.halfmoves = this.halfmoves;
254
+ pos.fullmoves = this.fullmoves;
255
+ return pos;
256
+ }
257
+
258
+ protected validate(): Result<undefined, PositionError> {
259
+ if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty));
260
+ if (this.board.king.size() !== 2) return Result.err(new PositionError(IllegalSetup.Kings));
261
+
262
+ if (!defined(this.board.kingOf(this.turn)))
263
+ return Result.err(new PositionError(IllegalSetup.Kings));
264
+
265
+ const otherKing = this.board.kingOf(opposite(this.turn));
266
+ if (!defined(otherKing)) return Result.err(new PositionError(IllegalSetup.Kings));
267
+ if (this.kingAttackers(otherKing, this.turn, this.board.occupied).nonEmpty()) {
268
+ return Result.err(new PositionError(IllegalSetup.OppositeCheck));
269
+ }
270
+
271
+ if (SquareSet.backranks().intersects(this.board.pawn)) {
272
+ return Result.err(new PositionError(IllegalSetup.PawnsOnBackrank));
273
+ }
274
+
275
+ return Result.ok(undefined);
276
+ }
277
+
278
+ dropDests(_ctx?: Context): SquareSet {
279
+ return SquareSet.empty();
280
+ }
281
+
282
+ dests(square: Square, ctx?: Context): SquareSet {
283
+ ctx = ctx || this.ctx();
284
+ if (ctx.variantEnd) return SquareSet.empty();
285
+ const piece = this.board.get(square);
286
+ if (!piece || piece.color !== this.turn) return SquareSet.empty();
287
+
288
+ let pseudo, legal;
289
+ if (piece.role === "pawn") {
290
+ pseudo = pawnAttacks(this.turn, square).intersect(this.board[opposite(this.turn)]);
291
+ const delta = this.turn === "white" ? 8 : -8;
292
+ const step = square + delta;
293
+ if (0 <= step && step < 64 && !this.board.occupied.has(step)) {
294
+ pseudo = pseudo.with(step);
295
+ const canDoubleStep = this.turn === "white" ? square < 16 : square >= 64 - 16;
296
+ const doubleStep = step + delta;
297
+ if (canDoubleStep && !this.board.occupied.has(doubleStep)) {
298
+ pseudo = pseudo.with(doubleStep);
299
+ }
300
+ }
301
+ if (defined(this.epSquare) && canCaptureEp(this, square, ctx)) {
302
+ legal = SquareSet.fromSquare(this.epSquare);
303
+ }
304
+ } else if (piece.role === "bishop") pseudo = bishopAttacks(square, this.board.occupied);
305
+ else if (piece.role === "knight") pseudo = knightAttacks(square);
306
+ else if (piece.role === "rook") pseudo = rookAttacks(square, this.board.occupied);
307
+ else if (piece.role === "queen") pseudo = queenAttacks(square, this.board.occupied);
308
+ else pseudo = kingAttacks(square);
309
+
310
+ pseudo = pseudo.diff(this.board[this.turn]);
311
+
312
+ if (defined(ctx.king)) {
313
+ if (piece.role === "king") {
314
+ const occ = this.board.occupied.without(square);
315
+ for (const to of pseudo) {
316
+ if (this.kingAttackers(to, opposite(this.turn), occ).nonEmpty())
317
+ pseudo = pseudo.without(to);
318
+ }
319
+ return pseudo.union(castlingDest(this, "a", ctx)).union(castlingDest(this, "h", ctx));
320
+ }
321
+
322
+ if (ctx.checkers.nonEmpty()) {
323
+ const checker = ctx.checkers.singleSquare();
324
+ if (!defined(checker)) return SquareSet.empty();
325
+ pseudo = pseudo.intersect(between(checker, ctx.king).with(checker));
326
+ }
327
+
328
+ if (ctx.blockers.has(square)) pseudo = pseudo.intersect(ray(square, ctx.king));
329
+ }
330
+
331
+ if (legal) pseudo = pseudo.union(legal);
332
+ return pseudo;
333
+ }
334
+
335
+ isVariantEnd(): boolean {
336
+ return false;
337
+ }
338
+
339
+ variantOutcome(_ctx?: Context): Outcome | undefined {
340
+ return;
341
+ }
342
+
343
+ hasInsufficientMaterial(color: Color): boolean {
344
+ if (this.board[color].intersect(this.board.pawn.union(this.board.rooksAndQueens())).nonEmpty())
345
+ return false;
346
+ if (this.board[color].intersects(this.board.knight)) {
347
+ return (
348
+ this.board[color].size() <= 2 &&
349
+ this.board[opposite(color)].diff(this.board.king).diff(this.board.queen).isEmpty()
350
+ );
351
+ }
352
+ if (this.board[color].intersects(this.board.bishop)) {
353
+ const sameColor =
354
+ !this.board.bishop.intersects(SquareSet.darkSquares()) ||
355
+ !this.board.bishop.intersects(SquareSet.lightSquares());
356
+ return sameColor && this.board.pawn.isEmpty() && this.board.knight.isEmpty();
357
+ }
358
+ return true;
359
+ }
360
+
361
+ // The following should be identical in all subclasses
362
+
363
+ toSetup(): Setup {
364
+ return {
365
+ board: this.board.clone(),
366
+ pockets: this.pockets?.clone(),
367
+ turn: this.turn,
368
+ castlingRights: this.castles.castlingRights,
369
+ epSquare: legalEpSquare(this),
370
+ remainingChecks: this.remainingChecks?.clone(),
371
+ halfmoves: Math.min(this.halfmoves, 150),
372
+ fullmoves: Math.min(Math.max(this.fullmoves, 1), 9999),
373
+ };
374
+ }
375
+
376
+ isInsufficientMaterial(): boolean {
377
+ return COLORS.every((color) => this.hasInsufficientMaterial(color));
378
+ }
379
+
380
+ hasDests(ctx?: Context): boolean {
381
+ ctx = ctx || this.ctx();
382
+ for (const square of this.board[this.turn]) {
383
+ if (this.dests(square, ctx).nonEmpty()) return true;
384
+ }
385
+ return this.dropDests(ctx).nonEmpty();
386
+ }
387
+
388
+ isLegal(move: Move, ctx?: Context): boolean {
389
+ if (isDrop(move)) {
390
+ if (!this.pockets || this.pockets[this.turn][move.role] <= 0) return false;
391
+ if (move.role === "pawn" && SquareSet.backranks().has(move.to)) return false;
392
+ return this.dropDests(ctx).has(move.to);
393
+ } else {
394
+ if (move.promotion === "pawn") return false;
395
+ if (move.promotion === "king" && this.rules !== "antichess") return false;
396
+ if (
397
+ !!move.promotion !== (this.board.pawn.has(move.from) && SquareSet.backranks().has(move.to))
398
+ )
399
+ return false;
400
+ const dests = this.dests(move.from, ctx);
401
+ return dests.has(move.to) || dests.has(normalizeMove(this, move).to);
402
+ }
403
+ }
404
+
405
+ isCheck(): boolean {
406
+ const king = this.board.kingOf(this.turn);
407
+ return (
408
+ defined(king) && this.kingAttackers(king, opposite(this.turn), this.board.occupied).nonEmpty()
409
+ );
410
+ }
411
+
412
+ isEnd(ctx?: Context): boolean {
413
+ if (ctx ? ctx.variantEnd : this.isVariantEnd()) return true;
414
+ return this.isInsufficientMaterial() || !this.hasDests(ctx);
415
+ }
416
+
417
+ isCheckmate(ctx?: Context): boolean {
418
+ ctx = ctx || this.ctx();
419
+ return !ctx.variantEnd && ctx.checkers.nonEmpty() && !this.hasDests(ctx);
420
+ }
421
+
422
+ isStalemate(ctx?: Context): boolean {
423
+ ctx = ctx || this.ctx();
424
+ return !ctx.variantEnd && ctx.checkers.isEmpty() && !this.hasDests(ctx);
425
+ }
426
+
427
+ outcome(ctx?: Context): Outcome | undefined {
428
+ const variantOutcome = this.variantOutcome(ctx);
429
+ if (variantOutcome) return variantOutcome;
430
+ ctx = ctx || this.ctx();
431
+ if (this.isCheckmate(ctx)) return { winner: opposite(this.turn) };
432
+ else if (this.isInsufficientMaterial() || this.isStalemate(ctx)) return { winner: undefined };
433
+ else return;
434
+ }
435
+
436
+ allDests(ctx?: Context): Map<Square, SquareSet> {
437
+ ctx = ctx || this.ctx();
438
+ const d = new Map();
439
+ if (ctx.variantEnd) return d;
440
+ for (const square of this.board[this.turn]) {
441
+ d.set(square, this.dests(square, ctx));
442
+ }
443
+ return d;
444
+ }
445
+
446
+ play(move: Move): void {
447
+ const turn = this.turn;
448
+ const epSquare = this.epSquare;
449
+ const castling = castlingSide(this, move);
450
+
451
+ this.epSquare = undefined;
452
+ this.halfmoves += 1;
453
+ if (turn === "black") this.fullmoves += 1;
454
+ this.turn = opposite(turn);
455
+
456
+ if (isDrop(move)) {
457
+ this.board.set(move.to, { role: move.role, color: turn });
458
+ if (this.pockets) this.pockets[turn][move.role]--;
459
+ if (move.role === "pawn") this.halfmoves = 0;
460
+ } else {
461
+ const piece = this.board.take(move.from);
462
+ if (!piece) return;
463
+
464
+ let epCapture: Piece | undefined;
465
+ if (piece.role === "pawn") {
466
+ this.halfmoves = 0;
467
+ if (move.to === epSquare) {
468
+ epCapture = this.board.take(move.to + (turn === "white" ? -8 : 8));
469
+ }
470
+ const delta = move.from - move.to;
471
+ if (Math.abs(delta) === 16 && 8 <= move.from && move.from <= 55) {
472
+ this.epSquare = (move.from + move.to) >> 1;
473
+ }
474
+ if (move.promotion) {
475
+ piece.role = move.promotion;
476
+ piece.promoted = !!this.pockets;
477
+ }
478
+ } else if (piece.role === "rook") {
479
+ this.castles.discardRook(move.from);
480
+ } else if (piece.role === "king") {
481
+ if (castling) {
482
+ const rookFrom = this.castles.rook[turn][castling];
483
+ if (defined(rookFrom)) {
484
+ const rook = this.board.take(rookFrom);
485
+ this.board.set(kingCastlesTo(turn, castling), piece);
486
+ if (rook) this.board.set(rookCastlesTo(turn, castling), rook);
487
+ }
488
+ }
489
+ this.castles.discardColor(turn);
490
+ }
491
+
492
+ if (!castling) {
493
+ const capture = this.board.set(move.to, piece) || epCapture;
494
+ if (capture) this.playCaptureAt(move.to, capture);
495
+ }
496
+ }
497
+
498
+ if (this.remainingChecks) {
499
+ if (this.isCheck()) this.remainingChecks[turn] = Math.max(this.remainingChecks[turn] - 1, 0);
500
+ }
501
+ }
502
+ }
503
+
504
+ export class Chess extends Position {
505
+ private constructor() {
506
+ super("chess");
507
+ }
508
+
509
+ static default(): Chess {
510
+ const pos = new this();
511
+ pos.reset();
512
+ return pos;
513
+ }
514
+
515
+ static fromSetup(setup: Setup): Result<Chess, PositionError> {
516
+ const pos = new this();
517
+ pos.setupUnchecked(setup);
518
+ return pos.validate().map((_) => pos);
519
+ }
520
+
521
+ clone(): Chess {
522
+ return super.clone() as Chess;
523
+ }
524
+ }
525
+
526
+ const validEpSquare = (pos: Position, square: Square | undefined): Square | undefined => {
527
+ if (!defined(square)) return;
528
+ const epRank = pos.turn === "white" ? 5 : 2;
529
+ const forward = pos.turn === "white" ? 8 : -8;
530
+ if (squareRank(square) !== epRank) return;
531
+ if (pos.board.occupied.has(square + forward)) return;
532
+ const pawn = square - forward;
533
+ if (!pos.board.pawn.has(pawn) || !pos.board[opposite(pos.turn)].has(pawn)) return;
534
+ return square;
535
+ };
536
+
537
+ const legalEpSquare = (pos: Position): Square | undefined => {
538
+ if (!defined(pos.epSquare)) return;
539
+ const ctx = pos.ctx();
540
+ const ourPawns = pos.board.pieces(pos.turn, "pawn");
541
+ const candidates = ourPawns.intersect(pawnAttacks(opposite(pos.turn), pos.epSquare));
542
+ for (const candidate of candidates) {
543
+ if (pos.dests(candidate, ctx).has(pos.epSquare)) return pos.epSquare;
544
+ }
545
+ return;
546
+ };
547
+
548
+ const canCaptureEp = (pos: Position, pawnFrom: Square, ctx: Context): boolean => {
549
+ if (!defined(pos.epSquare)) return false;
550
+ if (!pawnAttacks(pos.turn, pawnFrom).has(pos.epSquare)) return false;
551
+ if (!defined(ctx.king)) return true;
552
+ const delta = pos.turn === "white" ? 8 : -8;
553
+ const captured = pos.epSquare - delta;
554
+ return pos
555
+ .kingAttackers(
556
+ ctx.king,
557
+ opposite(pos.turn),
558
+ pos.board.occupied.toggle(pawnFrom).toggle(captured).with(pos.epSquare),
559
+ )
560
+ .without(captured)
561
+ .isEmpty();
562
+ };
563
+
564
+ const castlingDest = (pos: Position, side: CastlingSide, ctx: Context): SquareSet => {
565
+ if (!defined(ctx.king) || ctx.checkers.nonEmpty()) return SquareSet.empty();
566
+ const rook = pos.castles.rook[pos.turn][side];
567
+ if (!defined(rook)) return SquareSet.empty();
568
+ if (pos.castles.path[pos.turn][side].intersects(pos.board.occupied)) return SquareSet.empty();
569
+
570
+ const kingTo = kingCastlesTo(pos.turn, side);
571
+ const kingPath = between(ctx.king, kingTo);
572
+ const occ = pos.board.occupied.without(ctx.king);
573
+ for (const sq of kingPath) {
574
+ if (pos.kingAttackers(sq, opposite(pos.turn), occ).nonEmpty()) return SquareSet.empty();
575
+ }
576
+
577
+ const rookTo = rookCastlesTo(pos.turn, side);
578
+ const after = pos.board.occupied.toggle(ctx.king).toggle(rook).toggle(rookTo);
579
+ if (pos.kingAttackers(kingTo, opposite(pos.turn), after).nonEmpty()) return SquareSet.empty();
580
+
581
+ return SquareSet.fromSquare(rook);
582
+ };
583
+
584
+ export const pseudoDests = (pos: Position, square: Square, ctx: Context): SquareSet => {
585
+ if (ctx.variantEnd) return SquareSet.empty();
586
+ const piece = pos.board.get(square);
587
+ if (!piece || piece.color !== pos.turn) return SquareSet.empty();
588
+
589
+ let pseudo = attacks(piece, square, pos.board.occupied);
590
+ if (piece.role === "pawn") {
591
+ let captureTargets = pos.board[opposite(pos.turn)];
592
+ if (defined(pos.epSquare)) captureTargets = captureTargets.with(pos.epSquare);
593
+ pseudo = pseudo.intersect(captureTargets);
594
+ const delta = pos.turn === "white" ? 8 : -8;
595
+ const step = square + delta;
596
+ if (0 <= step && step < 64 && !pos.board.occupied.has(step)) {
597
+ pseudo = pseudo.with(step);
598
+ const canDoubleStep = pos.turn === "white" ? square < 16 : square >= 64 - 16;
599
+ const doubleStep = step + delta;
600
+ if (canDoubleStep && !pos.board.occupied.has(doubleStep)) {
601
+ pseudo = pseudo.with(doubleStep);
602
+ }
603
+ }
604
+ return pseudo;
605
+ } else {
606
+ pseudo = pseudo.diff(pos.board[pos.turn]);
607
+ }
608
+ if (square === ctx.king)
609
+ return pseudo.union(castlingDest(pos, "a", ctx)).union(castlingDest(pos, "h", ctx));
610
+ else return pseudo;
611
+ };
612
+
613
+ export const equalsIgnoreMoves = (left: Position, right: Position): boolean =>
614
+ left.rules === right.rules &&
615
+ boardEquals(left.board, right.board) &&
616
+ ((right.pockets && left.pockets?.equals(right.pockets)) || (!left.pockets && !right.pockets)) &&
617
+ left.turn === right.turn &&
618
+ left.castles.castlingRights.equals(right.castles.castlingRights) &&
619
+ legalEpSquare(left) === legalEpSquare(right) &&
620
+ ((right.remainingChecks && left.remainingChecks?.equals(right.remainingChecks)) ||
621
+ (!left.remainingChecks && !right.remainingChecks));
622
+
623
+ export const castlingSide = (pos: Position, move: Move): CastlingSide | undefined => {
624
+ if (isDrop(move)) return;
625
+ const delta = move.to - move.from;
626
+ if (Math.abs(delta) !== 2 && !pos.board[pos.turn].has(move.to)) return;
627
+ if (!pos.board.king.has(move.from)) return;
628
+ return delta > 0 ? "h" : "a";
629
+ };
630
+
631
+ export const normalizeMove = (pos: Position, move: Move): Move => {
632
+ const side = castlingSide(pos, move);
633
+ if (!side) return move;
634
+ const rookFrom = pos.castles.rook[pos.turn][side];
635
+ return {
636
+ from: (move as NormalMove).from,
637
+ to: defined(rookFrom) ? rookFrom : move.to,
638
+ };
639
+ };
640
+
641
+ export const isStandardMaterialSide = (board: Board, color: Color): boolean => {
642
+ const promoted =
643
+ Math.max(board.pieces(color, "queen").size() - 1, 0) +
644
+ Math.max(board.pieces(color, "rook").size() - 2, 0) +
645
+ Math.max(board.pieces(color, "knight").size() - 2, 0) +
646
+ Math.max(board.pieces(color, "bishop").intersect(SquareSet.lightSquares()).size() - 1, 0) +
647
+ Math.max(board.pieces(color, "bishop").intersect(SquareSet.darkSquares()).size() - 1, 0);
648
+ return board.pieces(color, "pawn").size() + promoted <= 8;
649
+ };
650
+
651
+ export const isStandardMaterial = (pos: Chess): boolean =>
652
+ COLORS.every((color) => isStandardMaterialSide(pos.board, color));
653
+
654
+ export const isImpossibleCheck = (pos: Position): boolean => {
655
+ const ourKing = pos.board.kingOf(pos.turn);
656
+ if (!defined(ourKing)) return false;
657
+ const checkers = pos.kingAttackers(ourKing, opposite(pos.turn), pos.board.occupied);
658
+ if (checkers.isEmpty()) return false;
659
+ if (defined(pos.epSquare)) {
660
+ // The pushed pawn must be the only checker, or it has uncovered
661
+ // check by a single sliding piece.
662
+ const pushedTo = pos.epSquare ^ 8;
663
+ const pushedFrom = pos.epSquare ^ 24;
664
+ return (
665
+ checkers.moreThanOne() ||
666
+ (checkers.first()! !== pushedTo &&
667
+ pos
668
+ .kingAttackers(
669
+ ourKing,
670
+ opposite(pos.turn),
671
+ pos.board.occupied.without(pushedTo).with(pushedFrom),
672
+ )
673
+ .nonEmpty())
674
+ );
675
+ } else if (pos.rules === "atomic") {
676
+ // Other king moving away can cause many checks to be given at the same
677
+ // time. Not checking details, or even that the king is close enough.
678
+ return false;
679
+ } else {
680
+ // Sliding checkers aligned with king.
681
+ return (
682
+ checkers.size() > 2 ||
683
+ (checkers.size() === 2 && ray(checkers.first()!, checkers.last()!).has(ourKing)) || // Sliding checkers aligned with king
684
+ checkers.intersect(pos.board.steppers()).moreThanOne()
685
+ );
686
+ }
687
+ };