Fix Koopa promotions with captures, and Balakhlava: pawns move forward
[vchess.git] / client / src / variants / Interweave.js
CommitLineData
6e47d367
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
3import { randInt, shuffle } from "@/utils/alea";
4
5export class InterweaveRules extends ChessRules {
6 static get HasFlags() {
7 return false;
8 }
9
10 static GenRandInitFen(randomness) {
11 if (randomness == 0)
12 return "rbnkknbr/pppppppp/8/8/8/8/PPPPPPPP/RBNKKNBR w 0 - 000000";
13
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'];
18 break;
19 }
20
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];
29 }
30 return (
31 pieces["b"].join("") +
32 "/pppppppp/8/8/8/8/PPPPPPPP/" +
33 pieces["w"].join("").toUpperCase() +
34 // En-passant allowed, but no flags
35 " w 0 - 000000"
36 );
37 }
38
39 static IsGoodFen(fen) {
40 if (!ChessRules.IsGoodFen(fen)) return false;
41 const fenParsed = V.ParseFen(fen);
42 // 4) Check captures
43 if (!fenParsed.captured || !fenParsed.captured.match(/^[0-9]{6,6}$/))
44 return false;
45 return true;
46 }
47
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) {
54 let sumElts = 0;
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++;
58 else {
e50a8025 59 const num = parseInt(row[i], 10);
6e47d367
BA
60 if (isNaN(num)) return false;
61 sumElts += num;
62 }
63 }
64 if (sumElts != V.size.y) return false;
65 }
66 // Both kings should be on board. Exactly two per color.
67 if (Object.values(kings).some(v => v != 2)) return false;
68 return true;
69 }
70
71 static ParseFen(fen) {
72 const fenParts = fen.split(" ");
73 return Object.assign(
74 ChessRules.ParseFen(fen),
75 { captured: fenParts[4] }
76 );
77 }
78
79 getFen() {
80 return super.getFen() + " " + this.getCapturedFen();
81 }
82
83 getFenForRepeat() {
84 return super.getFenForRepeat() + "_" + this.getCapturedFen();
85 }
86
87 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];
92 });
93 return counts.join("");
94 }
95
96 scanKings() {}
97
98 setOtherVariables(fen) {
99 super.setOtherVariables(fen);
e50a8025
BA
100 const captured =
101 V.ParseFen(fen).captured.split("").map(x => parseInt(x, 10));
6e47d367
BA
102 // Initialize captured pieces' counts from FEN
103 this.captured = {
104 w: {
e50a8025
BA
105 [V.ROOK]: captured[0],
106 [V.KNIGHT]: captured[1],
107 [V.BISHOP]: captured[2]
6e47d367
BA
108 },
109 b: {
e50a8025
BA
110 [V.ROOK]: captured[3],
111 [V.KNIGHT]: captured[4],
112 [V.BISHOP]: captured[5]
6e47d367
BA
113 }
114 };
115 // Stack of "last move" only for intermediate captures
116 this.lastMoveEnd = [null];
117 }
118
119 // Trim all non-capturing moves
120 static KeepCaptures(moves) {
121 return moves.filter(m => m.vanish.length >= 2 || m.appear.length == 0);
122 }
123
124 // Stop at the first capture found (if any)
125 atLeastOneCapture() {
126 const color = this.turn;
127 for (let i = 0; i < V.size.x; i++) {
128 for (let j = 0; j < V.size.y; j++) {
129 if (
130 this.board[i][j] != V.EMPTY &&
131 this.getColor(i, j) == color &&
132 V.KeepCaptures(this.getPotentialMovesFrom([i, j])).length > 0
133 ) {
134 return true;
135 }
136 }
137 }
138 return false;
139 }
140
141 // En-passant after 2-sq jump
142 getEpSquare(moveOrSquare) {
143 if (!moveOrSquare) return undefined;
144 if (typeof moveOrSquare === "string") {
145 const square = moveOrSquare;
146 if (square == "-") return undefined;
147 // Enemy pawn initial column must be given too:
148 let res = [];
149 const epParts = square.split(",");
150 res.push(V.SquareToCoords(epParts[0]));
151 res.push(V.ColumnToCoord(epParts[1]));
152 return res;
153 }
154 // Argument is a move:
155 const move = moveOrSquare;
156 const [sx, ex, sy, ey] =
157 [move.start.x, move.end.x, move.start.y, move.end.y];
158 if (
159 move.vanish.length == 1 &&
160 this.getPiece(sx, sy) == V.PAWN &&
161 Math.abs(sx - ex) == 2 &&
162 Math.abs(sy - ey) == 2
163 ) {
164 return [
165 {
166 x: (ex + sx) / 2,
167 y: (ey + sy) / 2
168 },
169 // The arrival column must be remembered, because
170 // potentially two pawns could be candidates to be captured:
171 // one on our left, and one on our right.
172 move.end.y
173 ];
174 }
175 return undefined; //default
176 }
177
178 static IsGoodEnpassant(enpassant) {
179 if (enpassant != "-") {
180 const epParts = enpassant.split(",");
181 const epSq = V.SquareToCoords(epParts[0]);
182 if (isNaN(epSq.x) || isNaN(epSq.y) || !V.OnBoard(epSq)) return false;
183 const arrCol = V.ColumnToCoord(epParts[1]);
184 if (isNaN(arrCol) || arrCol < 0 || arrCol >= V.size.y) return false;
185 }
186 return true;
187 }
188
189 getEnpassantFen() {
190 const L = this.epSquares.length;
191 if (!this.epSquares[L - 1]) return "-"; //no en-passant
192 return (
193 V.CoordsToSquare(this.epSquares[L - 1][0]) +
194 "," +
195 V.CoordToColumn(this.epSquares[L - 1][1])
196 );
197 }
198
7ddfec38 199 getPotentialMovesFrom([x, y]) {
6e47d367
BA
200 switch (this.getPiece(x, y)) {
201 case V.PAWN:
7ddfec38 202 return this.getPotentialPawnMoves([x, y]);
6e47d367 203 case V.ROOK:
7ddfec38 204 return this.getPotentialRookMoves([x, y]);
6e47d367 205 case V.KNIGHT:
7ddfec38 206 return this.getPotentialKnightMoves([x, y]);
6e47d367 207 case V.BISHOP:
7ddfec38 208 return this.getPotentialBishopMoves([x, y]);
6e47d367 209 case V.KING:
7ddfec38 210 return this.getPotentialKingMoves([x, y]);
6e47d367
BA
211 // No queens
212 }
7ddfec38 213 return [];
6e47d367
BA
214 }
215
216 // Special pawns movements
217 getPotentialPawnMoves([x, y]) {
218 const color = this.turn;
219 const oppCol = V.GetOppCol(color);
220 let moves = [];
221 const [sizeX, sizeY] = [V.size.x, V.size.y];
222 const shiftX = color == "w" ? -1 : 1;
223 const startRank = color == "w" ? sizeX - 2 : 1;
224 const potentialFinalPieces =
225 [V.ROOK, V.KNIGHT, V.BISHOP].filter(p => this.captured[color][p] > 0);
226 const lastRanks = (color == "w" ? [0, 1] : [sizeX - 1, sizeX - 2]);
227 if (x + shiftX == lastRanks[0] && potentialFinalPieces.length == 0)
228 // If no captured piece is available, the pawn cannot promote
229 return [];
230
231 const finalPieces1 =
232 x + shiftX == lastRanks[0]
233 ? potentialFinalPieces
234 :
235 x + shiftX == lastRanks[1]
236 ? potentialFinalPieces.concat([V.PAWN])
237 : [V.PAWN];
238 // One square diagonally
239 for (let shiftY of [-1, 1]) {
240 if (this.board[x + shiftX][y + shiftY] == V.EMPTY) {
241 for (let piece of finalPieces1) {
242 moves.push(
243 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
244 c: color,
245 p: piece
246 })
247 );
248 }
249 if (
250 V.PawnSpecs.twoSquares &&
251 x == startRank &&
252 y + 2 * shiftY >= 0 &&
253 y + 2 * shiftY < sizeY &&
254 this.board[x + 2 * shiftX][y + 2 * shiftY] == V.EMPTY
255 ) {
256 // Two squares jump
257 moves.push(
258 this.getBasicMove([x, y], [x + 2 * shiftX, y + 2 * shiftY])
259 );
260 }
261 }
262 }
263 // Capture
264 const finalPieces2 =
265 x + 2 * shiftX == lastRanks[0]
266 ? potentialFinalPieces
267 :
268 x + 2 * shiftX == lastRanks[1]
269 ? potentialFinalPieces.concat([V.PAWN])
270 : [V.PAWN];
271 if (
272 this.board[x + shiftX][y] != V.EMPTY &&
273 this.canTake([x, y], [x + shiftX, y]) &&
274 V.OnBoard(x + 2 * shiftX, y) &&
275 this.board[x + 2 * shiftX][y] == V.EMPTY
276 ) {
277 const oppPiece = this.getPiece(x + shiftX, y);
278 for (let piece of finalPieces2) {
279 let mv = this.getBasicMove(
280 [x, y], [x + 2 * shiftX, y], { c: color, p: piece });
281 mv.vanish.push({
282 x: x + shiftX,
283 y: y,
284 p: oppPiece,
285 c: oppCol
286 });
287 moves.push(mv);
288 }
289 }
290
291 // En passant
292 const Lep = this.epSquares.length;
293 const epSquare = this.epSquares[Lep - 1]; //always at least one element
294 if (
295 !!epSquare &&
296 epSquare[0].x == x + shiftX &&
297 epSquare[0].y == y &&
298 this.board[x + 2 * shiftX][y] == V.EMPTY
299 ) {
300 for (let piece of finalPieces2) {
301 let enpassantMove =
302 this.getBasicMove(
303 [x, y], [x + 2 * shiftX, y], { c: color, p: piece});
304 enpassantMove.vanish.push({
305 x: x,
306 y: epSquare[1],
307 p: "p",
308 c: this.getColor(x, epSquare[1])
309 });
310 moves.push(enpassantMove);
311 }
312 }
313
314 // Add custodian captures:
315 const steps = V.steps[V.ROOK];
316 moves.forEach(m => {
317 // Try capturing in every direction
318 for (let step of steps) {
319 const sq2 = [m.end.x + 2 * step[0], m.end.y + 2 * step[1]];
320 if (
321 V.OnBoard(sq2[0], sq2[1]) &&
322 this.board[sq2[0]][sq2[1]] != V.EMPTY &&
323 this.getColor(sq2[0], sq2[1]) == color
324 ) {
325 // Potential capture
326 const sq1 = [m.end.x + step[0], m.end.y + step[1]];
327 if (
328 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
329 this.getColor(sq1[0], sq1[1]) == oppCol
330 ) {
331 m.vanish.push(
332 new PiPo({
333 x: sq1[0],
334 y: sq1[1],
335 c: oppCol,
336 p: this.getPiece(sq1[0], sq1[1])
337 })
338 );
339 }
340 }
341 }
342 });
343
344 return moves;
345 }
346
347 getSlides([x, y], steps, options) {
348 options = options || {};
349 // No captures:
350 let moves = [];
351 outerLoop: for (let step of steps) {
352 let i = x + step[0];
353 let j = y + step[1];
354 let counter = 1;
355 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
356 if (!options["doubleStep"] || counter % 2 == 0)
357 moves.push(this.getBasicMove([x, y], [i, j]));
358 if (!!options["oneStep"]) continue outerLoop;
359 i += step[0];
360 j += step[1];
361 counter++;
362 }
363 }
364 return moves;
365 }
366
367 // Smasher
368 getPotentialRookMoves([x, y]) {
369 let moves =
370 this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true })
371 .concat(this.getSlides([x, y], V.steps[V.BISHOP]));
372 // Add captures
373 const oppCol = V.GetOppCol(this.turn);
374 moves.forEach(m => {
375 const delta = [m.end.x - m.start.x, m.end.y - m.start.y];
376 const step = [
377 delta[0] / Math.abs(delta[0]) || 0,
378 delta[1] / Math.abs(delta[1]) || 0
379 ];
380 if (step[0] == 0 || step[1] == 0) {
381 // Rook-like move, candidate for capturing
382 const [i, j] = [m.end.x + step[0], m.end.y + step[1]];
383 if (
384 V.OnBoard(i, j) &&
385 this.board[i][j] != V.EMPTY &&
386 this.getColor(i, j) == oppCol
387 ) {
388 m.vanish.push({
389 x: i,
390 y: j,
391 p: this.getPiece(i, j),
392 c: oppCol
393 });
394 }
395 }
396 });
397 return moves;
398 }
399
400 // Leaper
401 getPotentialKnightMoves([x, y]) {
402 let moves =
403 this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true })
404 .concat(this.getSlides([x, y], V.steps[V.BISHOP]));
405 const oppCol = V.GetOppCol(this.turn);
406 // Look for double-knight moves (could capture):
407 for (let step of V.steps[V.KNIGHT]) {
408 const [i, j] = [x + 2 * step[0], y + 2 * step[1]];
409 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
410 const [ii, jj] = [x + step[0], y + step[1]];
411 if (this.board[ii][jj] == V.EMPTY || this.getColor(ii, jj) == oppCol) {
412 let mv = this.getBasicMove([x, y], [i, j]);
413 if (this.board[ii][jj] != V.EMPTY) {
414 mv.vanish.push({
415 x: ii,
416 y: jj,
417 c: oppCol,
418 p: this.getPiece(ii, jj)
419 });
420 }
421 moves.push(mv);
422 }
423 }
424 }
425 // Look for an enemy in every orthogonal direction
426 for (let step of V.steps[V.ROOK]) {
427 let [i, j] = [x + step[0], y+ step[1]];
428 let counter = 1;
429 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
430 i += step[0];
431 j += step[1];
432 counter++;
433 }
434 if (
435 V.OnBoard(i, j) &&
436 counter % 2 == 1 &&
437 this.getColor(i, j) == oppCol
438 ) {
439 const oppPiece = this.getPiece(i, j);
440 // Candidate for capture: can I land after?
441 let [ii, jj] = [i + step[0], j + step[1]];
442 counter++;
443 while (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY) {
444 if (counter % 2 == 0) {
445 // Same color: add capture
446 let mv = this.getBasicMove([x, y], [ii, jj]);
447 mv.vanish.push({
448 x: i,
449 y: j,
450 c: oppCol,
451 p: oppPiece
452 });
453 moves.push(mv);
454 }
455 ii += step[0];
456 jj += step[1];
457 counter++;
458 }
459 }
460 }
461 return moves;
462 }
463
464 // Remover
465 getPotentialBishopMoves([x, y]) {
466 let moves = this.getSlides([x, y], V.steps[V.BISHOP]);
467 // Add captures
468 const oppCol = V.GetOppCol(this.turn);
469 let captures = [];
470 for (let step of V.steps[V.ROOK]) {
471 const [i, j] = [x + step[0], y + step[1]];
472 if (
473 V.OnBoard(i, j) &&
474 this.board[i][j] != V.EMPTY &&
475 this.getColor(i, j) == oppCol
476 ) {
477 captures.push([i, j]);
478 }
479 }
480 captures.forEach(c => {
481 moves.push({
482 start: { x: x, y: y },
483 end: { x: c[0], y: c[1] },
484 appear: [],
485 vanish: captures.map(ct => {
486 return {
487 x: ct[0],
488 y: ct[1],
489 c: oppCol,
490 p: this.getPiece(ct[0], ct[1])
491 };
492 })
493 });
494 });
495 return moves;
496 }
497
498 getPotentialKingMoves([x, y]) {
499 let moves = this.getSlides([x, y], V.steps[V.BISHOP], { oneStep: true });
500 // Add captures
501 const oppCol = V.GetOppCol(this.turn);
502 for (let step of V.steps[V.ROOK]) {
503 const [i, j] = [x + 2 * step[0], y + 2 * step[1]];
504 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
505 const [ii, jj] = [x + step[0], y + step[1]];
506 if (this.board[ii][jj] != V.EMPTY && this.getColor(ii, jj) == oppCol) {
507 let mv = this.getBasicMove([x, y], [i, j]);
508 mv.vanish.push({
509 x: ii,
510 y: jj,
511 c: oppCol,
512 p: this.getPiece(ii, jj)
513 });
514 moves.push(mv);
515 }
516 }
517 }
518 return moves;
519 }
520
521 getPossibleMovesFrom(sq) {
522 const L = this.lastMoveEnd.length;
523 if (
524 !!this.lastMoveEnd[L-1] &&
525 (
526 sq[0] != this.lastMoveEnd[L-1].x ||
527 sq[1] != this.lastMoveEnd[L-1].y
528 )
529 ) {
530 return [];
531 }
532 let moves = this.getPotentialMovesFrom(sq);
533 const captureMoves = V.KeepCaptures(moves);
534 if (captureMoves.length > 0) return captureMoves;
535 if (this.atLeastOneCapture()) return [];
536 return moves;
537 }
538
539 getAllValidMoves() {
540 const moves = this.getAllPotentialMoves();
541 const captures = V.KeepCaptures(moves);
542 if (captures.length > 0) return captures;
543 return moves;
544 }
545
546 filterValid(moves) {
547 // No checks
548 return moves;
549 }
550
551 play(move) {
552 this.epSquares.push(this.getEpSquare(move));
553 V.PlayOnBoard(this.board, move);
554 if (move.vanish.length >= 2) {
555 // Capture: update this.captured
556 for (let i=1; i<move.vanish.length; i++)
557 this.captured[move.vanish[i].c][move.vanish[i].p]++;
558 }
7ddfec38
BA
559 // Check if the move is the last of the turn
560 if (move.vanish.length >= 2 || move.appear.length == 0) {
561 const moreCaptures = (
562 V.KeepCaptures(
563 this.getPotentialMovesFrom([move.end.x, move.end.y])
564 )
565 .length > 0
566 );
567 move.last = !moreCaptures;
568 }
569 else move.last = true;
6e47d367
BA
570 if (!!move.last) {
571 // No capture, or no more capture available
572 this.turn = V.GetOppCol(this.turn);
573 this.movesCount++;
574 this.lastMoveEnd.push(null);
7ddfec38 575 move.last = true; //will be used in undo and computer play
6e47d367
BA
576 }
577 else this.lastMoveEnd.push(move.end);
578 }
579
580 undo(move) {
581 this.epSquares.pop();
582 this.lastMoveEnd.pop();
583 V.UndoOnBoard(this.board, move);
584 if (move.vanish.length >= 2) {
585 for (let i=1; i<move.vanish.length; i++)
586 this.captured[move.vanish[i].c][move.vanish[i].p]--;
587 }
588 if (!!move.last) {
589 this.turn = V.GetOppCol(this.turn);
590 this.movesCount--;
591 }
592 }
593
594 getCheckSquares() {
595 return [];
596 }
597
598 getCurrentScore() {
599 // Count kings: if one is missing, the side lost
600 let kingsCount = { 'w': 0, 'b': 0 };
601 for (let i=0; i<8; i++) {
602 for (let j=0; j<8; j++) {
603 if (this.board[i][j] != V.EMPTY && this.getPiece(i, j) == V.KING)
604 kingsCount[this.getColor(i, j)]++;
605 }
606 }
607 if (kingsCount['w'] < 2) return "0-1";
608 if (kingsCount['b'] < 2) return "1-0";
609 return "*";
610 }
611
612 getComputerMove() {
613 let moves = this.getAllValidMoves();
614 if (moves.length == 0) return null;
615 // Just play random moves (for now at least. TODO?)
616 let mvArray = [];
617 while (moves.length > 0) {
618 const mv = moves[randInt(moves.length)];
619 mvArray.push(mv);
7ddfec38 620 this.play(mv);
6e47d367 621 if (!mv.last) {
6e47d367
BA
622 moves = V.KeepCaptures(
623 this.getPotentialMovesFrom([mv.end.x, mv.end.y]));
624 }
625 else break;
626 }
7ddfec38 627 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
6e47d367
BA
628 return (mvArray.length > 1 ? mvArray : mvArray[0]);
629 }
630
631 getNotation(move) {
632 const initialSquare = V.CoordsToSquare(move.start);
633 const finalSquare = V.CoordsToSquare(move.end);
634 if (move.appear.length == 0)
635 // Remover captures 'R'
636 return initialSquare + "R";
637 let notation = move.appear[0].p.toUpperCase() + finalSquare;
638 // Add a capture mark (not describing what is captured...):
639 if (move.vanish.length >= 2) notation += "X";
640 return notation;
641 }
642};