1 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
2 import { ArrayFun
} from "@/utils/array";
3 import { randInt
, shuffle
} from "@/utils/alea";
5 export class InterweaveRules
extends ChessRules
{
7 static get HasFlags() {
11 static GenRandInitFen(options
) {
12 if (options
.randomness
== 0)
13 return "rbnkknbr/pppppppp/8/8/8/8/PPPPPPPP/RBNKKNBR w 0 - 000000";
15 let pieces
= { w: new Array(8), b: new Array(8) };
16 for (let c
of ["w", "b"]) {
17 if (c
== 'b' && options
.randomness
== 1) {
18 pieces
['b'] = pieces
['w'];
22 // Each pair of pieces on 2 colors:
23 const composition
= ['r', 'n', 'b', 'k', 'r', 'n', 'b', 'k'];
24 let positions
= shuffle(ArrayFun
.range(4));
25 for (let i
= 0; i
< 4; i
++)
26 pieces
[c
][2 * positions
[i
]] = composition
[i
];
27 positions
= shuffle(ArrayFun
.range(4));
28 for (let i
= 0; i
< 4; i
++)
29 pieces
[c
][2 * positions
[i
] + 1] = composition
[i
];
32 pieces
["b"].join("") +
33 "/pppppppp/8/8/8/8/PPPPPPPP/" +
34 pieces
["w"].join("").toUpperCase() +
35 // En-passant allowed, but no flags
40 static IsGoodFen(fen
) {
41 if (!ChessRules
.IsGoodFen(fen
)) return false;
42 const fenParsed
= V
.ParseFen(fen
);
44 if (!fenParsed
.captured
|| !fenParsed
.captured
.match(/^[0-9]{6,6}$/))
49 static IsGoodPosition(position
) {
50 if (position
.length
== 0) return false;
51 const rows
= position
.split("/");
52 if (rows
.length
!= V
.size
.x
) return false;
53 let kings
= { "k": 0, "K": 0 };
54 for (let row
of rows
) {
56 for (let i
= 0; i
< row
.length
; i
++) {
57 if (['K','k'].includes(row
[i
])) kings
[row
[i
]]++;
58 if (V
.PIECES
.includes(row
[i
].toLowerCase())) sumElts
++;
60 const num
= parseInt(row
[i
], 10);
61 if (isNaN(num
)) return false;
65 if (sumElts
!= V
.size
.y
) return false;
67 // Both kings should be on board. Exactly two per color.
68 if (Object
.values(kings
).some(v
=> v
!= 2)) return false;
72 static ParseFen(fen
) {
73 const fenParts
= fen
.split(" ");
75 ChessRules
.ParseFen(fen
),
76 { captured: fenParts
[4] }
81 return super.getFen() + " " + this.getCapturedFen();
85 return super.getFenForRepeat() + "_" + this.getCapturedFen();
89 let counts
= [...Array(6).fill(0)];
90 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
].forEach((p
,idx
) => {
91 counts
[idx
] = this.captured
["w"][p
];
92 counts
[3 + idx
] = this.captured
["b"][p
];
94 return counts
.join("");
99 setOtherVariables(fen
) {
100 super.setOtherVariables(fen
);
102 V
.ParseFen(fen
).captured
.split("").map(x
=> parseInt(x
, 10));
103 // Initialize captured pieces' counts from FEN
106 [V
.ROOK
]: captured
[0],
107 [V
.KNIGHT
]: captured
[1],
108 [V
.BISHOP
]: captured
[2]
111 [V
.ROOK
]: captured
[3],
112 [V
.KNIGHT
]: captured
[4],
113 [V
.BISHOP
]: captured
[5]
116 // Stack of "last move" only for intermediate captures
117 this.lastMoveEnd
= [null];
120 // Trim all non-capturing moves
121 static KeepCaptures(moves
) {
122 return moves
.filter(m
=> m
.vanish
.length
>= 2 || m
.appear
.length
== 0);
125 // Stop at the first capture found (if any)
126 atLeastOneCapture() {
127 const color
= this.turn
;
128 for (let i
= 0; i
< V
.size
.x
; i
++) {
129 for (let j
= 0; j
< V
.size
.y
; j
++) {
131 this.board
[i
][j
] != V
.EMPTY
&&
132 this.getColor(i
, j
) == color
&&
133 V
.KeepCaptures(this.getPotentialMovesFrom([i
, j
])).length
> 0
142 // En-passant after 2-sq jump
143 getEpSquare(moveOrSquare
) {
144 if (!moveOrSquare
) return undefined;
145 if (typeof moveOrSquare
=== "string") {
146 const square
= moveOrSquare
;
147 if (square
== "-") return undefined;
148 // Enemy pawn initial column must be given too:
150 const epParts
= square
.split(",");
151 res
.push(V
.SquareToCoords(epParts
[0]));
152 res
.push(V
.ColumnToCoord(epParts
[1]));
155 // Argument is a move:
156 const move = moveOrSquare
;
157 const [sx
, ex
, sy
, ey
] =
158 [move.start
.x
, move.end
.x
, move.start
.y
, move.end
.y
];
160 move.vanish
.length
== 1 &&
161 this.getPiece(sx
, sy
) == V
.PAWN
&&
162 Math
.abs(sx
- ex
) == 2 &&
163 Math
.abs(sy
- ey
) == 2
170 // The arrival column must be remembered, because
171 // potentially two pawns could be candidates to be captured:
172 // one on our left, and one on our right.
176 return undefined; //default
179 static IsGoodEnpassant(enpassant
) {
180 if (enpassant
!= "-") {
181 const epParts
= enpassant
.split(",");
182 const epSq
= V
.SquareToCoords(epParts
[0]);
183 if (isNaN(epSq
.x
) || isNaN(epSq
.y
) || !V
.OnBoard(epSq
)) return false;
184 const arrCol
= V
.ColumnToCoord(epParts
[1]);
185 if (isNaN(arrCol
) || arrCol
< 0 || arrCol
>= V
.size
.y
) return false;
191 const L
= this.epSquares
.length
;
192 if (!this.epSquares
[L
- 1]) return "-"; //no en-passant
194 V
.CoordsToSquare(this.epSquares
[L
- 1][0]) +
196 V
.CoordToColumn(this.epSquares
[L
- 1][1])
200 getPotentialMovesFrom([x
, y
]) {
201 switch (this.getPiece(x
, y
)) {
203 return this.getPotentialPawnMoves([x
, y
]);
205 return this.getPotentialRookMoves([x
, y
]);
207 return this.getPotentialKnightMoves([x
, y
]);
209 return this.getPotentialBishopMoves([x
, y
]);
211 return this.getPotentialKingMoves([x
, y
]);
217 // Special pawns movements
218 getPotentialPawnMoves([x
, y
]) {
219 const color
= this.turn
;
220 const oppCol
= V
.GetOppCol(color
);
222 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
223 const shiftX
= color
== "w" ? -1 : 1;
224 const startRank
= color
== "w" ? sizeX
- 2 : 1;
225 const potentialFinalPieces
=
226 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
].filter(p
=> this.captured
[color
][p
] > 0);
227 const lastRanks
= (color
== "w" ? [0, 1] : [sizeX
- 1, sizeX
- 2]);
228 if (x
+ shiftX
== lastRanks
[0] && potentialFinalPieces
.length
== 0)
229 // If no captured piece is available, the pawn cannot promote
233 x
+ shiftX
== lastRanks
[0]
234 ? potentialFinalPieces
236 x
+ shiftX
== lastRanks
[1]
237 ? potentialFinalPieces
.concat([V
.PAWN
])
239 // One square diagonally
240 for (let shiftY
of [-1, 1]) {
241 if (this.board
[x
+ shiftX
][y
+ shiftY
] == V
.EMPTY
) {
242 for (let piece
of finalPieces1
) {
244 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
251 V
.PawnSpecs
.twoSquares
&&
253 y
+ 2 * shiftY
>= 0 &&
254 y
+ 2 * shiftY
< sizeY
&&
255 this.board
[x
+ 2 * shiftX
][y
+ 2 * shiftY
] == V
.EMPTY
259 this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
+ 2 * shiftY
])
266 x
+ 2 * shiftX
== lastRanks
[0]
267 ? potentialFinalPieces
269 x
+ 2 * shiftX
== lastRanks
[1]
270 ? potentialFinalPieces
.concat([V
.PAWN
])
273 this.board
[x
+ shiftX
][y
] != V
.EMPTY
&&
274 this.canTake([x
, y
], [x
+ shiftX
, y
]) &&
275 V
.OnBoard(x
+ 2 * shiftX
, y
) &&
276 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
278 const oppPiece
= this.getPiece(x
+ shiftX
, y
);
279 for (let piece
of finalPieces2
) {
280 let mv
= this.getBasicMove(
281 [x
, y
], [x
+ 2 * shiftX
, y
], { c: color
, p: piece
});
293 const Lep
= this.epSquares
.length
;
294 const epSquare
= this.epSquares
[Lep
- 1]; //always at least one element
297 epSquare
[0].x
== x
+ shiftX
&&
298 epSquare
[0].y
== y
&&
299 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
301 for (let piece
of finalPieces2
) {
304 [x
, y
], [x
+ 2 * shiftX
, y
], { c: color
, p: piece
});
305 enpassantMove
.vanish
.push({
309 c: this.getColor(x
, epSquare
[1])
311 moves
.push(enpassantMove
);
315 // Add custodian captures:
316 const steps
= V
.steps
[V
.ROOK
];
318 // Try capturing in every direction
319 for (let step
of steps
) {
320 const sq2
= [m
.end
.x
+ 2 * step
[0], m
.end
.y
+ 2 * step
[1]];
322 V
.OnBoard(sq2
[0], sq2
[1]) &&
323 this.board
[sq2
[0]][sq2
[1]] != V
.EMPTY
&&
324 this.getColor(sq2
[0], sq2
[1]) == color
327 const sq1
= [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
329 this.board
[sq1
[0]][sq1
[1]] != V
.EMPTY
&&
330 this.getColor(sq1
[0], sq1
[1]) == oppCol
337 p: this.getPiece(sq1
[0], sq1
[1])
348 getSlides([x
, y
], steps
, options
) {
349 options
= options
|| {};
352 outerLoop: for (let step
of steps
) {
356 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
357 if (!options
["doubleStep"] || counter
% 2 == 0)
358 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
359 if (!!options
["oneStep"]) continue outerLoop
;
369 getPotentialRookMoves([x
, y
]) {
371 this.getSlides([x
, y
], V
.steps
[V
.ROOK
], { doubleStep: true })
372 .concat(this.getSlides([x
, y
], V
.steps
[V
.BISHOP
]));
374 const oppCol
= V
.GetOppCol(this.turn
);
376 const delta
= [m
.end
.x
- m
.start
.x
, m
.end
.y
- m
.start
.y
];
378 delta
[0] / Math
.abs(delta
[0]) || 0,
379 delta
[1] / Math
.abs(delta
[1]) || 0
381 if (step
[0] == 0 || step
[1] == 0) {
382 // Rook-like move, candidate for capturing
383 const [i
, j
] = [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
386 this.board
[i
][j
] != V
.EMPTY
&&
387 this.getColor(i
, j
) == oppCol
392 p: this.getPiece(i
, j
),
402 getPotentialKnightMoves([x
, y
]) {
404 this.getSlides([x
, y
], V
.steps
[V
.ROOK
], { doubleStep: true })
405 .concat(this.getSlides([x
, y
], V
.steps
[V
.BISHOP
]));
406 const oppCol
= V
.GetOppCol(this.turn
);
407 // Look for double-knight moves (could capture):
408 for (let step
of V
.steps
[V
.KNIGHT
]) {
409 const [i
, j
] = [x
+ 2 * step
[0], y
+ 2 * step
[1]];
410 if (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
411 const [ii
, jj
] = [x
+ step
[0], y
+ step
[1]];
412 if (this.board
[ii
][jj
] == V
.EMPTY
|| this.getColor(ii
, jj
) == oppCol
) {
413 let mv
= this.getBasicMove([x
, y
], [i
, j
]);
414 if (this.board
[ii
][jj
] != V
.EMPTY
) {
419 p: this.getPiece(ii
, jj
)
426 // Look for an enemy in every orthogonal direction
427 for (let step
of V
.steps
[V
.ROOK
]) {
428 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
430 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
438 this.getColor(i
, j
) == oppCol
440 const oppPiece
= this.getPiece(i
, j
);
441 // Candidate for capture: can I land after?
442 let [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
444 while (V
.OnBoard(ii
, jj
) && this.board
[ii
][jj
] == V
.EMPTY
) {
445 if (counter
% 2 == 0) {
446 // Same color: add capture
447 let mv
= this.getBasicMove([x
, y
], [ii
, jj
]);
466 getPotentialBishopMoves([x
, y
]) {
467 let moves
= this.getSlides([x
, y
], V
.steps
[V
.BISHOP
]);
469 const oppCol
= V
.GetOppCol(this.turn
);
471 for (let step
of V
.steps
[V
.ROOK
]) {
472 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
475 this.board
[i
][j
] != V
.EMPTY
&&
476 this.getColor(i
, j
) == oppCol
478 captures
.push([i
, j
]);
481 captures
.forEach(c
=> {
483 start: { x: x
, y: y
},
484 end: { x: c
[0], y: c
[1] },
486 vanish: captures
.map(ct
=> {
491 p: this.getPiece(ct
[0], ct
[1])
499 getPotentialKingMoves([x
, y
]) {
500 let moves
= this.getSlides([x
, y
], V
.steps
[V
.BISHOP
], { oneStep: true });
502 const oppCol
= V
.GetOppCol(this.turn
);
503 for (let step
of V
.steps
[V
.ROOK
]) {
504 const [i
, j
] = [x
+ 2 * step
[0], y
+ 2 * step
[1]];
505 if (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
506 const [ii
, jj
] = [x
+ step
[0], y
+ step
[1]];
507 if (this.board
[ii
][jj
] != V
.EMPTY
&& this.getColor(ii
, jj
) == oppCol
) {
508 let mv
= this.getBasicMove([x
, y
], [i
, j
]);
513 p: this.getPiece(ii
, jj
)
522 getPossibleMovesFrom(sq
) {
523 const L
= this.lastMoveEnd
.length
;
525 !!this.lastMoveEnd
[L
-1] &&
527 sq
[0] != this.lastMoveEnd
[L
-1].x
||
528 sq
[1] != this.lastMoveEnd
[L
-1].y
533 let moves
= this.getPotentialMovesFrom(sq
);
534 const captureMoves
= V
.KeepCaptures(moves
);
535 if (captureMoves
.length
> 0) return captureMoves
;
536 if (this.atLeastOneCapture()) return [];
541 const moves
= this.getAllPotentialMoves();
542 const captures
= V
.KeepCaptures(moves
);
543 if (captures
.length
> 0) return captures
;
553 this.epSquares
.push(this.getEpSquare(move));
554 V
.PlayOnBoard(this.board
, move);
555 if (move.vanish
.length
>= 2) {
556 // Capture: update this.captured
557 for (let i
=1; i
<move.vanish
.length
; i
++)
558 this.captured
[move.vanish
[i
].c
][move.vanish
[i
].p
]++;
560 // Check if the move is the last of the turn
561 if (move.vanish
.length
>= 2 || move.appear
.length
== 0) {
562 const moreCaptures
= (
564 this.getPotentialMovesFrom([move.end
.x
, move.end
.y
])
568 move.last
= !moreCaptures
;
570 else move.last
= true;
572 // No capture, or no more capture available
573 this.turn
= V
.GetOppCol(this.turn
);
575 this.lastMoveEnd
.push(null);
576 move.last
= true; //will be used in undo and computer play
578 else this.lastMoveEnd
.push(move.end
);
582 this.epSquares
.pop();
583 this.lastMoveEnd
.pop();
584 V
.UndoOnBoard(this.board
, move);
585 if (move.vanish
.length
>= 2) {
586 for (let i
=1; i
<move.vanish
.length
; i
++)
587 this.captured
[move.vanish
[i
].c
][move.vanish
[i
].p
]--;
590 this.turn
= V
.GetOppCol(this.turn
);
600 // Count kings: if one is missing, the side lost
601 let kingsCount
= { 'w': 0, 'b': 0 };
602 for (let i
=0; i
<8; i
++) {
603 for (let j
=0; j
<8; j
++) {
604 if (this.board
[i
][j
] != V
.EMPTY
&& this.getPiece(i
, j
) == V
.KING
)
605 kingsCount
[this.getColor(i
, j
)]++;
608 if (kingsCount
['w'] < 2) return "0-1";
609 if (kingsCount
['b'] < 2) return "1-0";
614 let moves
= this.getAllValidMoves();
615 if (moves
.length
== 0) return null;
616 // Just play random moves (for now at least. TODO?)
618 while (moves
.length
> 0) {
619 const mv
= moves
[randInt(moves
.length
)];
623 moves
= V
.KeepCaptures(
624 this.getPotentialMovesFrom([mv
.end
.x
, mv
.end
.y
]));
628 for (let i
= mvArray
.length
- 1; i
>= 0; i
--) this.undo(mvArray
[i
]);
629 return (mvArray
.length
> 1 ? mvArray : mvArray
[0]);
633 const initialSquare
= V
.CoordsToSquare(move.start
);
634 const finalSquare
= V
.CoordsToSquare(move.end
);
635 if (move.appear
.length
== 0)
636 // Remover captures 'R'
637 return initialSquare
+ "R";
638 let notation
= move.appear
[0].p
.toUpperCase() + finalSquare
;
639 // Add a capture mark (not describing what is captured...):
640 if (move.vanish
.length
>= 2) notation
+= "X";