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