A few fixes + draft Interweave and Takenmake. Only 1 1/2 variant to go now :)
[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], noPostprocess) {
199 const L = this.lastMoveEnd.length;
200 if (
201 !!this.lastMoveEnd[L-1] &&
202 (
203 x != this.lastMoveEnd[L-1].x ||
204 y != this.lastMoveEnd[L-1].y
205 )
206 ) {
207 // A capture must continue: wrong square
208 return [];
209 }
210 let moves = [];
211 switch (this.getPiece(x, y)) {
212 case V.PAWN:
213 moves = this.getPotentialPawnMoves([x, y]);
214 break;
215 case V.ROOK:
216 moves = this.getPotentialRookMoves([x, y]);
217 break;
218 case V.KNIGHT:
219 moves = this.getPotentialKnightMoves([x, y]);
220 break;
221 case V.BISHOP:
222 moves = this.getPotentialBishopMoves([x, y]);
223 break;
224 case V.KING:
225 moves = this.getPotentialKingMoves([x, y]);
226 break;
227 // No queens
228 }
229 if (!noPostprocess) {
230 // Post-process: if capture,
231 // can another capture be achieved with the same piece?
232 moves.forEach(m => {
233 if (m.vanish.length >= 2 || m.appear.length == 0) {
234 this.play(m);
235 const moreCaptures = (
236 V.KeepCaptures(
237 this.getPotentialMovesFrom([m.end.x, m.end.y], "noPostprocess")
238 )
239 .length > 0
240 );
241 this.undo(m);
242 if (!moreCaptures) m.last = true;
243 }
244 else m.last = true;
245 });
246 }
247 return moves;
248 }
249
250 // Special pawns movements
251 getPotentialPawnMoves([x, y]) {
252 const color = this.turn;
253 const oppCol = V.GetOppCol(color);
254 let moves = [];
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
263 return [];
264
265 const finalPieces1 =
266 x + shiftX == lastRanks[0]
267 ? potentialFinalPieces
268 :
269 x + shiftX == lastRanks[1]
270 ? potentialFinalPieces.concat([V.PAWN])
271 : [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) {
276 moves.push(
277 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
278 c: color,
279 p: piece
280 })
281 );
282 }
283 if (
284 V.PawnSpecs.twoSquares &&
285 x == startRank &&
286 y + 2 * shiftY >= 0 &&
287 y + 2 * shiftY < sizeY &&
288 this.board[x + 2 * shiftX][y + 2 * shiftY] == V.EMPTY
289 ) {
290 // Two squares jump
291 moves.push(
292 this.getBasicMove([x, y], [x + 2 * shiftX, y + 2 * shiftY])
293 );
294 }
295 }
296 }
297 // Capture
298 const finalPieces2 =
299 x + 2 * shiftX == lastRanks[0]
300 ? potentialFinalPieces
301 :
302 x + 2 * shiftX == lastRanks[1]
303 ? potentialFinalPieces.concat([V.PAWN])
304 : [V.PAWN];
305 if (
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
310 ) {
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 });
315 mv.vanish.push({
316 x: x + shiftX,
317 y: y,
318 p: oppPiece,
319 c: oppCol
320 });
321 moves.push(mv);
322 }
323 }
324
325 // En passant
326 const Lep = this.epSquares.length;
327 const epSquare = this.epSquares[Lep - 1]; //always at least one element
328 if (
329 !!epSquare &&
330 epSquare[0].x == x + shiftX &&
331 epSquare[0].y == y &&
332 this.board[x + 2 * shiftX][y] == V.EMPTY
333 ) {
334 for (let piece of finalPieces2) {
335 let enpassantMove =
336 this.getBasicMove(
337 [x, y], [x + 2 * shiftX, y], { c: color, p: piece});
338 enpassantMove.vanish.push({
339 x: x,
340 y: epSquare[1],
341 p: "p",
342 c: this.getColor(x, epSquare[1])
343 });
344 moves.push(enpassantMove);
345 }
346 }
347
348 // Add custodian captures:
349 const steps = V.steps[V.ROOK];
350 moves.forEach(m => {
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]];
354 if (
355 V.OnBoard(sq2[0], sq2[1]) &&
356 this.board[sq2[0]][sq2[1]] != V.EMPTY &&
357 this.getColor(sq2[0], sq2[1]) == color
358 ) {
359 // Potential capture
360 const sq1 = [m.end.x + step[0], m.end.y + step[1]];
361 if (
362 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
363 this.getColor(sq1[0], sq1[1]) == oppCol
364 ) {
365 m.vanish.push(
366 new PiPo({
367 x: sq1[0],
368 y: sq1[1],
369 c: oppCol,
370 p: this.getPiece(sq1[0], sq1[1])
371 })
372 );
373 }
374 }
375 }
376 });
377
378 return moves;
379 }
380
381 getSlides([x, y], steps, options) {
382 options = options || {};
383 // No captures:
384 let moves = [];
385 outerLoop: for (let step of steps) {
386 let i = x + step[0];
387 let j = y + step[1];
388 let counter = 1;
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;
393 i += step[0];
394 j += step[1];
395 counter++;
396 }
397 }
398 return moves;
399 }
400
401 // Smasher
402 getPotentialRookMoves([x, y]) {
403 let moves =
404 this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true })
405 .concat(this.getSlides([x, y], V.steps[V.BISHOP]));
406 // Add captures
407 const oppCol = V.GetOppCol(this.turn);
408 moves.forEach(m => {
409 const delta = [m.end.x - m.start.x, m.end.y - m.start.y];
410 const step = [
411 delta[0] / Math.abs(delta[0]) || 0,
412 delta[1] / Math.abs(delta[1]) || 0
413 ];
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]];
417 if (
418 V.OnBoard(i, j) &&
419 this.board[i][j] != V.EMPTY &&
420 this.getColor(i, j) == oppCol
421 ) {
422 m.vanish.push({
423 x: i,
424 y: j,
425 p: this.getPiece(i, j),
426 c: oppCol
427 });
428 }
429 }
430 });
431 return moves;
432 }
433
434 // Leaper
435 getPotentialKnightMoves([x, y]) {
436 let moves =
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) {
448 mv.vanish.push({
449 x: ii,
450 y: jj,
451 c: oppCol,
452 p: this.getPiece(ii, jj)
453 });
454 }
455 moves.push(mv);
456 }
457 }
458 }
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]];
462 let counter = 1;
463 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
464 i += step[0];
465 j += step[1];
466 counter++;
467 }
468 if (
469 V.OnBoard(i, j) &&
470 counter % 2 == 1 &&
471 this.getColor(i, j) == oppCol
472 ) {
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]];
476 counter++;
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]);
481 mv.vanish.push({
482 x: i,
483 y: j,
484 c: oppCol,
485 p: oppPiece
486 });
487 moves.push(mv);
488 }
489 ii += step[0];
490 jj += step[1];
491 counter++;
492 }
493 }
494 }
495 return moves;
496 }
497
498 // Remover
499 getPotentialBishopMoves([x, y]) {
500 let moves = this.getSlides([x, y], V.steps[V.BISHOP]);
501 // Add captures
502 const oppCol = V.GetOppCol(this.turn);
503 let captures = [];
504 for (let step of V.steps[V.ROOK]) {
505 const [i, j] = [x + step[0], y + step[1]];
506 if (
507 V.OnBoard(i, j) &&
508 this.board[i][j] != V.EMPTY &&
509 this.getColor(i, j) == oppCol
510 ) {
511 captures.push([i, j]);
512 }
513 }
514 captures.forEach(c => {
515 moves.push({
516 start: { x: x, y: y },
517 end: { x: c[0], y: c[1] },
518 appear: [],
519 vanish: captures.map(ct => {
520 return {
521 x: ct[0],
522 y: ct[1],
523 c: oppCol,
524 p: this.getPiece(ct[0], ct[1])
525 };
526 })
527 });
528 });
529 return moves;
530 }
531
532 getPotentialKingMoves([x, y]) {
533 let moves = this.getSlides([x, y], V.steps[V.BISHOP], { oneStep: true });
534 // Add captures
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]);
542 mv.vanish.push({
543 x: ii,
544 y: jj,
545 c: oppCol,
546 p: this.getPiece(ii, jj)
547 });
548 moves.push(mv);
549 }
550 }
551 }
552 return moves;
553 }
554
555 getPossibleMovesFrom(sq) {
556 const L = this.lastMoveEnd.length;
557 if (
558 !!this.lastMoveEnd[L-1] &&
559 (
560 sq[0] != this.lastMoveEnd[L-1].x ||
561 sq[1] != this.lastMoveEnd[L-1].y
562 )
563 ) {
564 return [];
565 }
566 let moves = this.getPotentialMovesFrom(sq);
567 const captureMoves = V.KeepCaptures(moves);
568 if (captureMoves.length > 0) return captureMoves;
569 if (this.atLeastOneCapture()) return [];
570 return moves;
571 }
572
573 getAllValidMoves() {
574 const moves = this.getAllPotentialMoves();
575 const captures = V.KeepCaptures(moves);
576 if (captures.length > 0) return captures;
577 return moves;
578 }
579
580 filterValid(moves) {
581 // No checks
582 return moves;
583 }
584
585 play(move) {
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]++;
592 }
593 if (!!move.last) {
594 // No capture, or no more capture available
595 this.turn = V.GetOppCol(this.turn);
596 this.movesCount++;
597 this.lastMoveEnd.push(null);
598 }
599 else this.lastMoveEnd.push(move.end);
600 }
601
602 undo(move) {
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]--;
609 }
610 if (!!move.last) {
611 this.turn = V.GetOppCol(this.turn);
612 this.movesCount--;
613 }
614 }
615
616 getCheckSquares() {
617 return [];
618 }
619
620 getCurrentScore() {
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)]++;
627 }
628 }
629 if (kingsCount['w'] < 2) return "0-1";
630 if (kingsCount['b'] < 2) return "1-0";
631 return "*";
632 }
633
634 getComputerMove() {
635 let moves = this.getAllValidMoves();
636 if (moves.length == 0) return null;
637 // Just play random moves (for now at least. TODO?)
638 let mvArray = [];
639 while (moves.length > 0) {
640 const mv = moves[randInt(moves.length)];
641 mvArray.push(mv);
642 if (!mv.last) {
643 this.play(mv);
644 moves = V.KeepCaptures(
645 this.getPotentialMovesFrom([mv.end.x, mv.end.y]));
646 }
647 else break;
648 }
649 for (let i = mvArray.length - 2; i >= 0; i--) this.undo(mvArray[i]);
650 return (mvArray.length > 1 ? mvArray : mvArray[0]);
651 }
652
653 getNotation(move) {
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";
662 return notation;
663 }
664 };