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
{
6 static get HasFlags() {
10 static GenRandInitFen(randomness
) {
12 return "rbnkknbr/pppppppp/8/8/8/8/PPPPPPPP/RBNKKNBR w 0 - 000000";
14 let pieces
= { w: new Array(8), b: new Array(8) };
15 for (let c
of ["w", "b"]) {
16 if (c
== 'b' && randomness
== 1) {
17 pieces
['b'] = pieces
['w'];
21 // Each pair of pieces on 2 colors:
22 const composition
= ['r', 'n', 'b', 'k', 'r', 'n', 'b', 'k'];
23 let positions
= shuffle(ArrayFun
.range(4));
24 for (let i
= 0; i
< 4; i
++)
25 pieces
[c
][2 * positions
[i
]] = composition
[i
];
26 positions
= shuffle(ArrayFun
.range(4));
27 for (let i
= 0; i
< 4; i
++)
28 pieces
[c
][2 * positions
[i
] + 1] = composition
[i
];
31 pieces
["b"].join("") +
32 "/pppppppp/8/8/8/8/PPPPPPPP/" +
33 pieces
["w"].join("").toUpperCase() +
34 // En-passant allowed, but no flags
39 static IsGoodFen(fen
) {
40 if (!ChessRules
.IsGoodFen(fen
)) return false;
41 const fenParsed
= V
.ParseFen(fen
);
43 if (!fenParsed
.captured
|| !fenParsed
.captured
.match(/^[0-9]{6,6}$/))
48 static IsGoodPosition(position
) {
49 if (position
.length
== 0) return false;
50 const rows
= position
.split("/");
51 if (rows
.length
!= V
.size
.x
) return false;
52 let kings
= { "k": 0, "K": 0 };
53 for (let row
of rows
) {
55 for (let i
= 0; i
< row
.length
; i
++) {
56 if (['K','k'].includes(row
[i
])) kings
[row
[i
]]++;
57 if (V
.PIECES
.includes(row
[i
].toLowerCase())) sumElts
++;
59 const num
= parseInt(row
[i
]);
60 if (isNaN(num
)) return false;
64 if (sumElts
!= V
.size
.y
) return false;
66 // Both kings should be on board. Exactly two per color.
67 if (Object
.values(kings
).some(v
=> v
!= 2)) return false;
71 static ParseFen(fen
) {
72 const fenParts
= fen
.split(" ");
74 ChessRules
.ParseFen(fen
),
75 { captured: fenParts
[4] }
80 return super.getFen() + " " + this.getCapturedFen();
84 return super.getFenForRepeat() + "_" + this.getCapturedFen();
88 let counts
= [...Array(6).fill(0)];
89 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
].forEach((p
,idx
) => {
90 counts
[idx
] = this.captured
["w"][p
];
91 counts
[3 + idx
] = this.captured
["b"][p
];
93 return counts
.join("");
98 setOtherVariables(fen
) {
99 super.setOtherVariables(fen
);
100 const fenParsed
= V
.ParseFen(fen
);
101 // Initialize captured pieces' counts from FEN
104 [V
.ROOK
]: parseInt(fenParsed
.captured
[0]),
105 [V
.KNIGHT
]: parseInt(fenParsed
.captured
[1]),
106 [V
.BISHOP
]: parseInt(fenParsed
.captured
[2]),
109 [V
.ROOK
]: parseInt(fenParsed
.captured
[3]),
110 [V
.KNIGHT
]: parseInt(fenParsed
.captured
[4]),
111 [V
.BISHOP
]: parseInt(fenParsed
.captured
[5]),
114 // Stack of "last move" only for intermediate captures
115 this.lastMoveEnd
= [null];
118 // Trim all non-capturing moves
119 static KeepCaptures(moves
) {
120 return moves
.filter(m
=> m
.vanish
.length
>= 2 || m
.appear
.length
== 0);
123 // Stop at the first capture found (if any)
124 atLeastOneCapture() {
125 const color
= this.turn
;
126 for (let i
= 0; i
< V
.size
.x
; i
++) {
127 for (let j
= 0; j
< V
.size
.y
; j
++) {
129 this.board
[i
][j
] != V
.EMPTY
&&
130 this.getColor(i
, j
) == color
&&
131 V
.KeepCaptures(this.getPotentialMovesFrom([i
, j
])).length
> 0
140 // En-passant after 2-sq jump
141 getEpSquare(moveOrSquare
) {
142 if (!moveOrSquare
) return undefined;
143 if (typeof moveOrSquare
=== "string") {
144 const square
= moveOrSquare
;
145 if (square
== "-") return undefined;
146 // Enemy pawn initial column must be given too:
148 const epParts
= square
.split(",");
149 res
.push(V
.SquareToCoords(epParts
[0]));
150 res
.push(V
.ColumnToCoord(epParts
[1]));
153 // Argument is a move:
154 const move = moveOrSquare
;
155 const [sx
, ex
, sy
, ey
] =
156 [move.start
.x
, move.end
.x
, move.start
.y
, move.end
.y
];
158 move.vanish
.length
== 1 &&
159 this.getPiece(sx
, sy
) == V
.PAWN
&&
160 Math
.abs(sx
- ex
) == 2 &&
161 Math
.abs(sy
- ey
) == 2
168 // The arrival column must be remembered, because
169 // potentially two pawns could be candidates to be captured:
170 // one on our left, and one on our right.
174 return undefined; //default
177 static IsGoodEnpassant(enpassant
) {
178 if (enpassant
!= "-") {
179 const epParts
= enpassant
.split(",");
180 const epSq
= V
.SquareToCoords(epParts
[0]);
181 if (isNaN(epSq
.x
) || isNaN(epSq
.y
) || !V
.OnBoard(epSq
)) return false;
182 const arrCol
= V
.ColumnToCoord(epParts
[1]);
183 if (isNaN(arrCol
) || arrCol
< 0 || arrCol
>= V
.size
.y
) return false;
189 const L
= this.epSquares
.length
;
190 if (!this.epSquares
[L
- 1]) return "-"; //no en-passant
192 V
.CoordsToSquare(this.epSquares
[L
- 1][0]) +
194 V
.CoordToColumn(this.epSquares
[L
- 1][1])
198 getPotentialMovesFrom([x
, y
], noPostprocess
) {
199 const L
= this.lastMoveEnd
.length
;
201 !!this.lastMoveEnd
[L
-1] &&
203 x
!= this.lastMoveEnd
[L
-1].x
||
204 y
!= this.lastMoveEnd
[L
-1].y
207 // A capture must continue: wrong square
211 switch (this.getPiece(x
, y
)) {
213 moves
= this.getPotentialPawnMoves([x
, y
]);
216 moves
= this.getPotentialRookMoves([x
, y
]);
219 moves
= this.getPotentialKnightMoves([x
, y
]);
222 moves
= this.getPotentialBishopMoves([x
, y
]);
225 moves
= this.getPotentialKingMoves([x
, y
]);
229 if (!noPostprocess
) {
230 // Post-process: if capture,
231 // can another capture be achieved with the same piece?
233 if (m
.vanish
.length
>= 2 || m
.appear
.length
== 0) {
235 const moreCaptures
= (
237 this.getPotentialMovesFrom([m
.end
.x
, m
.end
.y
], "noPostprocess")
242 if (!moreCaptures
) m
.last
= true;
250 // Special pawns movements
251 getPotentialPawnMoves([x
, y
]) {
252 const color
= this.turn
;
253 const oppCol
= V
.GetOppCol(color
);
255 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
256 const shiftX
= color
== "w" ? -1 : 1;
257 const startRank
= color
== "w" ? sizeX
- 2 : 1;
258 const potentialFinalPieces
=
259 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
].filter(p
=> this.captured
[color
][p
] > 0);
260 const lastRanks
= (color
== "w" ? [0, 1] : [sizeX
- 1, sizeX
- 2]);
261 if (x
+ shiftX
== lastRanks
[0] && potentialFinalPieces
.length
== 0)
262 // If no captured piece is available, the pawn cannot promote
266 x
+ shiftX
== lastRanks
[0]
267 ? potentialFinalPieces
269 x
+ shiftX
== lastRanks
[1]
270 ? potentialFinalPieces
.concat([V
.PAWN
])
272 // One square diagonally
273 for (let shiftY
of [-1, 1]) {
274 if (this.board
[x
+ shiftX
][y
+ shiftY
] == V
.EMPTY
) {
275 for (let piece
of finalPieces1
) {
277 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
284 V
.PawnSpecs
.twoSquares
&&
286 y
+ 2 * shiftY
>= 0 &&
287 y
+ 2 * shiftY
< sizeY
&&
288 this.board
[x
+ 2 * shiftX
][y
+ 2 * shiftY
] == V
.EMPTY
292 this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
+ 2 * shiftY
])
299 x
+ 2 * shiftX
== lastRanks
[0]
300 ? potentialFinalPieces
302 x
+ 2 * shiftX
== lastRanks
[1]
303 ? potentialFinalPieces
.concat([V
.PAWN
])
306 this.board
[x
+ shiftX
][y
] != V
.EMPTY
&&
307 this.canTake([x
, y
], [x
+ shiftX
, y
]) &&
308 V
.OnBoard(x
+ 2 * shiftX
, y
) &&
309 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
311 const oppPiece
= this.getPiece(x
+ shiftX
, y
);
312 for (let piece
of finalPieces2
) {
313 let mv
= this.getBasicMove(
314 [x
, y
], [x
+ 2 * shiftX
, y
], { c: color
, p: piece
});
326 const Lep
= this.epSquares
.length
;
327 const epSquare
= this.epSquares
[Lep
- 1]; //always at least one element
330 epSquare
[0].x
== x
+ shiftX
&&
331 epSquare
[0].y
== y
&&
332 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
334 for (let piece
of finalPieces2
) {
337 [x
, y
], [x
+ 2 * shiftX
, y
], { c: color
, p: piece
});
338 enpassantMove
.vanish
.push({
342 c: this.getColor(x
, epSquare
[1])
344 moves
.push(enpassantMove
);
348 // Add custodian captures:
349 const steps
= V
.steps
[V
.ROOK
];
351 // Try capturing in every direction
352 for (let step
of steps
) {
353 const sq2
= [m
.end
.x
+ 2 * step
[0], m
.end
.y
+ 2 * step
[1]];
355 V
.OnBoard(sq2
[0], sq2
[1]) &&
356 this.board
[sq2
[0]][sq2
[1]] != V
.EMPTY
&&
357 this.getColor(sq2
[0], sq2
[1]) == color
360 const sq1
= [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
362 this.board
[sq1
[0]][sq1
[1]] != V
.EMPTY
&&
363 this.getColor(sq1
[0], sq1
[1]) == oppCol
370 p: this.getPiece(sq1
[0], sq1
[1])
381 getSlides([x
, y
], steps
, options
) {
382 options
= options
|| {};
385 outerLoop: for (let step
of steps
) {
389 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
390 if (!options
["doubleStep"] || counter
% 2 == 0)
391 moves
.push(this.getBasicMove([x
, y
], [i
, j
]));
392 if (!!options
["oneStep"]) continue outerLoop
;
402 getPotentialRookMoves([x
, y
]) {
404 this.getSlides([x
, y
], V
.steps
[V
.ROOK
], { doubleStep: true })
405 .concat(this.getSlides([x
, y
], V
.steps
[V
.BISHOP
]));
407 const oppCol
= V
.GetOppCol(this.turn
);
409 const delta
= [m
.end
.x
- m
.start
.x
, m
.end
.y
- m
.start
.y
];
411 delta
[0] / Math
.abs(delta
[0]) || 0,
412 delta
[1] / Math
.abs(delta
[1]) || 0
414 if (step
[0] == 0 || step
[1] == 0) {
415 // Rook-like move, candidate for capturing
416 const [i
, j
] = [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]];
419 this.board
[i
][j
] != V
.EMPTY
&&
420 this.getColor(i
, j
) == oppCol
425 p: this.getPiece(i
, j
),
435 getPotentialKnightMoves([x
, y
]) {
437 this.getSlides([x
, y
], V
.steps
[V
.ROOK
], { doubleStep: true })
438 .concat(this.getSlides([x
, y
], V
.steps
[V
.BISHOP
]));
439 const oppCol
= V
.GetOppCol(this.turn
);
440 // Look for double-knight moves (could capture):
441 for (let step
of V
.steps
[V
.KNIGHT
]) {
442 const [i
, j
] = [x
+ 2 * step
[0], y
+ 2 * step
[1]];
443 if (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
444 const [ii
, jj
] = [x
+ step
[0], y
+ step
[1]];
445 if (this.board
[ii
][jj
] == V
.EMPTY
|| this.getColor(ii
, jj
) == oppCol
) {
446 let mv
= this.getBasicMove([x
, y
], [i
, j
]);
447 if (this.board
[ii
][jj
] != V
.EMPTY
) {
452 p: this.getPiece(ii
, jj
)
459 // Look for an enemy in every orthogonal direction
460 for (let step
of V
.steps
[V
.ROOK
]) {
461 let [i
, j
] = [x
+ step
[0], y
+ step
[1]];
463 while (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
471 this.getColor(i
, j
) == oppCol
473 const oppPiece
= this.getPiece(i
, j
);
474 // Candidate for capture: can I land after?
475 let [ii
, jj
] = [i
+ step
[0], j
+ step
[1]];
477 while (V
.OnBoard(ii
, jj
) && this.board
[ii
][jj
] == V
.EMPTY
) {
478 if (counter
% 2 == 0) {
479 // Same color: add capture
480 let mv
= this.getBasicMove([x
, y
], [ii
, jj
]);
499 getPotentialBishopMoves([x
, y
]) {
500 let moves
= this.getSlides([x
, y
], V
.steps
[V
.BISHOP
]);
502 const oppCol
= V
.GetOppCol(this.turn
);
504 for (let step
of V
.steps
[V
.ROOK
]) {
505 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
508 this.board
[i
][j
] != V
.EMPTY
&&
509 this.getColor(i
, j
) == oppCol
511 captures
.push([i
, j
]);
514 captures
.forEach(c
=> {
516 start: { x: x
, y: y
},
517 end: { x: c
[0], y: c
[1] },
519 vanish: captures
.map(ct
=> {
524 p: this.getPiece(ct
[0], ct
[1])
532 getPotentialKingMoves([x
, y
]) {
533 let moves
= this.getSlides([x
, y
], V
.steps
[V
.BISHOP
], { oneStep: true });
535 const oppCol
= V
.GetOppCol(this.turn
);
536 for (let step
of V
.steps
[V
.ROOK
]) {
537 const [i
, j
] = [x
+ 2 * step
[0], y
+ 2 * step
[1]];
538 if (V
.OnBoard(i
, j
) && this.board
[i
][j
] == V
.EMPTY
) {
539 const [ii
, jj
] = [x
+ step
[0], y
+ step
[1]];
540 if (this.board
[ii
][jj
] != V
.EMPTY
&& this.getColor(ii
, jj
) == oppCol
) {
541 let mv
= this.getBasicMove([x
, y
], [i
, j
]);
546 p: this.getPiece(ii
, jj
)
555 getPossibleMovesFrom(sq
) {
556 const L
= this.lastMoveEnd
.length
;
558 !!this.lastMoveEnd
[L
-1] &&
560 sq
[0] != this.lastMoveEnd
[L
-1].x
||
561 sq
[1] != this.lastMoveEnd
[L
-1].y
566 let moves
= this.getPotentialMovesFrom(sq
);
567 const captureMoves
= V
.KeepCaptures(moves
);
568 if (captureMoves
.length
> 0) return captureMoves
;
569 if (this.atLeastOneCapture()) return [];
574 const moves
= this.getAllPotentialMoves();
575 const captures
= V
.KeepCaptures(moves
);
576 if (captures
.length
> 0) return captures
;
586 this.epSquares
.push(this.getEpSquare(move));
587 V
.PlayOnBoard(this.board
, move);
588 if (move.vanish
.length
>= 2) {
589 // Capture: update this.captured
590 for (let i
=1; i
<move.vanish
.length
; i
++)
591 this.captured
[move.vanish
[i
].c
][move.vanish
[i
].p
]++;
594 // No capture, or no more capture available
595 this.turn
= V
.GetOppCol(this.turn
);
597 this.lastMoveEnd
.push(null);
599 else this.lastMoveEnd
.push(move.end
);
603 this.epSquares
.pop();
604 this.lastMoveEnd
.pop();
605 V
.UndoOnBoard(this.board
, move);
606 if (move.vanish
.length
>= 2) {
607 for (let i
=1; i
<move.vanish
.length
; i
++)
608 this.captured
[move.vanish
[i
].c
][move.vanish
[i
].p
]--;
611 this.turn
= V
.GetOppCol(this.turn
);
621 // Count kings: if one is missing, the side lost
622 let kingsCount
= { 'w': 0, 'b': 0 };
623 for (let i
=0; i
<8; i
++) {
624 for (let j
=0; j
<8; j
++) {
625 if (this.board
[i
][j
] != V
.EMPTY
&& this.getPiece(i
, j
) == V
.KING
)
626 kingsCount
[this.getColor(i
, j
)]++;
629 if (kingsCount
['w'] < 2) return "0-1";
630 if (kingsCount
['b'] < 2) return "1-0";
635 let moves
= this.getAllValidMoves();
636 if (moves
.length
== 0) return null;
637 // Just play random moves (for now at least. TODO?)
639 while (moves
.length
> 0) {
640 const mv
= moves
[randInt(moves
.length
)];
644 moves
= V
.KeepCaptures(
645 this.getPotentialMovesFrom([mv
.end
.x
, mv
.end
.y
]));
649 for (let i
= mvArray
.length
- 2; i
>= 0; i
--) this.undo(mvArray
[i
]);
650 return (mvArray
.length
> 1 ? mvArray : mvArray
[0]);
654 const initialSquare
= V
.CoordsToSquare(move.start
);
655 const finalSquare
= V
.CoordsToSquare(move.end
);
656 if (move.appear
.length
== 0)
657 // Remover captures 'R'
658 return initialSquare
+ "R";
659 let notation
= move.appear
[0].p
.toUpperCase() + finalSquare
;
660 // Add a capture mark (not describing what is captured...):
661 if (move.vanish
.length
>= 2) notation
+= "X";