Add Swap, Switching, pawns variants
[vchess.git] / client / src / variants / Checkered1.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2
3 export class Checkered1Rules extends ChessRules {
4 static board2fen(b) {
5 const checkered_codes = {
6 p: "s",
7 q: "t",
8 r: "u",
9 b: "c",
10 n: "o"
11 };
12 if (b[0] == "c") return checkered_codes[b[1]];
13 return ChessRules.board2fen(b);
14 }
15
16 static fen2board(f) {
17 // Tolerate upper-case versions of checkered pieces (why not?)
18 const checkered_pieces = {
19 s: "p",
20 S: "p",
21 t: "q",
22 T: "q",
23 u: "r",
24 U: "r",
25 c: "b",
26 C: "b",
27 o: "n",
28 O: "n"
29 };
30 if (Object.keys(checkered_pieces).includes(f))
31 return "c" + checkered_pieces[f];
32 return ChessRules.fen2board(f);
33 }
34
35 static get PIECES() {
36 return ChessRules.PIECES.concat(["s", "t", "u", "c", "o"]);
37 }
38
39 getPpath(b) {
40 return (b[0] == "c" ? "Checkered/" : "") + b;
41 }
42
43 setOtherVariables(fen) {
44 super.setOtherVariables(fen);
45 // Local stack of non-capturing checkered moves:
46 this.cmoves = [];
47 const cmove = V.ParseFen(fen).cmove;
48 if (cmove == "-") this.cmoves.push(null);
49 else {
50 this.cmoves.push({
51 start: ChessRules.SquareToCoords(cmove.substr(0, 2)),
52 end: ChessRules.SquareToCoords(cmove.substr(2))
53 });
54 }
55 // Stage 1: as Checkered2. Stage 2: checkered pieces are autonomous
56 const stageInfo = V.ParseFen(fen).stage;
57 this.stage = parseInt(stageInfo[0]);
58 this.sideCheckered = (this.stage == 2 ? stageInfo[1] : undefined);
59 }
60
61 static IsGoodFen(fen) {
62 if (!ChessRules.IsGoodFen(fen)) return false;
63 const fenParts = fen.split(" ");
64 if (fenParts.length != 7) return false;
65 if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/))
66 return false;
67 if (!fenParts[6].match(/^[12][wb]?$/)) return false;
68 return true;
69 }
70
71 static IsGoodFlags(flags) {
72 // 4 for castle + 16 for pawns
73 return !!flags.match(/^[a-z]{4,4}[01]{16,16}$/);
74 }
75
76 setFlags(fenflags) {
77 super.setFlags(fenflags); //castleFlags
78 this.pawnFlags = {
79 w: [...Array(8)], //pawns can move 2 squares?
80 b: [...Array(8)]
81 };
82 const flags = fenflags.substr(4); //skip first 4 letters, for castle
83 for (let c of ["w", "b"]) {
84 for (let i = 0; i < 8; i++)
85 this.pawnFlags[c][i] = flags.charAt((c == "w" ? 0 : 8) + i) == "1";
86 }
87 }
88
89 aggregateFlags() {
90 return [this.castleFlags, this.pawnFlags];
91 }
92
93 disaggregateFlags(flags) {
94 this.castleFlags = flags[0];
95 this.pawnFlags = flags[1];
96 }
97
98 getEpSquare(moveOrSquare) {
99 // At stage 2, all pawns can be captured en-passant
100 if (
101 this.stage == 2 ||
102 typeof moveOrSquare !== "object" ||
103 (moveOrSquare.appear.length > 0 && moveOrSquare.appear[0].c != 'c')
104 )
105 return super.getEpSquare(moveOrSquare);
106 // Checkered or switch move: no en-passant
107 return undefined;
108 }
109
110 getCmove(move) {
111 // No checkered move to undo at stage 2:
112 if (this.stage == 1 && move.vanish.length == 1 && move.appear[0].c == "c")
113 return { start: move.start, end: move.end };
114 return null;
115 }
116
117 canTake([x1, y1], [x2, y2]) {
118 const color1 = this.getColor(x1, y1);
119 const color2 = this.getColor(x2, y2);
120 if (this.stage == 2) {
121 // Black & White <-- takes --> Checkered
122 const color1 = this.getColor(x1, y1);
123 const color2 = this.getColor(x2, y2);
124 return color1 != color2 && [color1, color2].includes('c');
125 }
126 // Checkered aren't captured
127 return (
128 color1 != color2 &&
129 color2 != "c" &&
130 (color1 != "c" || color2 != this.turn)
131 );
132 }
133
134 getPotentialMovesFrom([x, y]) {
135 let standardMoves = super.getPotentialMovesFrom([x, y]);
136 if (this.stage == 1) {
137 const color = this.turn;
138 // Post-processing: apply "checkerization" of standard moves
139 const lastRank = (color == "w" ? 0 : 7);
140 let moves = [];
141 // King is treated differently: it never turn checkered
142 if (this.getPiece(x, y) == V.KING) {
143 // If at least one checkered piece, allow switching:
144 if (this.board.some(b => b.some(cell => cell[0] == 'c'))) {
145 const oppCol = V.GetOppCol(color);
146 moves.push(
147 new Move({
148 start: { x: x, y: y },
149 end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] },
150 appear: [],
151 vanish: []
152 })
153 );
154 }
155 return standardMoves.concat(moves);
156 }
157 standardMoves.forEach(m => {
158 if (m.vanish[0].p == V.PAWN) {
159 if (
160 Math.abs(m.end.x - m.start.x) == 2 &&
161 !this.pawnFlags[this.turn][m.start.y]
162 ) {
163 return; //skip forbidden 2-squares jumps
164 }
165 if (
166 this.board[m.end.x][m.end.y] == V.EMPTY &&
167 m.vanish.length == 2 &&
168 this.getColor(m.start.x, m.start.y) == "c"
169 ) {
170 return; //checkered pawns cannot take en-passant
171 }
172 }
173 if (m.vanish.length == 1)
174 // No capture
175 moves.push(m);
176 else {
177 // A capture occured (m.vanish.length == 2)
178 m.appear[0].c = "c";
179 moves.push(m);
180 if (
181 // Avoid promotions (already treated):
182 m.appear[0].p != m.vanish[1].p &&
183 (m.vanish[0].p != V.PAWN || m.end.x != lastRank)
184 ) {
185 // Add transformation into captured piece
186 let m2 = JSON.parse(JSON.stringify(m));
187 m2.appear[0].p = m.vanish[1].p;
188 moves.push(m2);
189 }
190 }
191 });
192 return moves;
193 }
194 return standardMoves;
195 }
196
197 getPotentialPawnMoves([x, y]) {
198 const color = this.getColor(x, y);
199 if (this.stage == 2) {
200 const saveTurn = this.turn;
201 if (this.sideCheckered == this.turn) {
202 // Cannot change PawnSpecs.bidirectional, so cheat a little:
203 this.turn = 'w';
204 const wMoves = super.getPotentialPawnMoves([x, y]);
205 this.turn = 'b';
206 const bMoves = super.getPotentialPawnMoves([x, y]);
207 this.turn = saveTurn;
208 return wMoves.concat(bMoves);
209 }
210 // Playing with both colors:
211 this.turn = color;
212 const moves = super.getPotentialPawnMoves([x, y]);
213 this.turn = saveTurn;
214 return moves;
215 }
216 let moves = super.getPotentialPawnMoves([x, y]);
217 // Post-process: set right color for checkered moves
218 if (color == 'c') {
219 moves.forEach(m => {
220 m.appear[0].c = 'c'; //may be done twice if capture
221 m.vanish[0].c = 'c';
222 });
223 }
224 return moves;
225 }
226
227 canIplay(side, [x, y]) {
228 if (this.stage == 2) {
229 const color = this.getColor(x, y);
230 return (
231 this.turn == this.sideCheckered
232 ? color == 'c'
233 : ['w', 'b'].includes(color)
234 );
235 }
236 return side == this.turn && [side, "c"].includes(this.getColor(x, y));
237 }
238
239 // Does m2 un-do m1 ? (to disallow undoing checkered moves)
240 oppositeMoves(m1, m2) {
241 return (
242 !!m1 &&
243 m2.appear[0].c == "c" &&
244 m2.appear.length == 1 &&
245 m2.vanish.length == 1 &&
246 m1.start.x == m2.end.x &&
247 m1.end.x == m2.start.x &&
248 m1.start.y == m2.end.y &&
249 m1.end.y == m2.start.y
250 );
251 }
252
253 filterValid(moves) {
254 if (moves.length == 0) return [];
255 const color = this.turn;
256 const oppCol = V.GetOppCol(color);
257 const L = this.cmoves.length; //at least 1: init from FEN
258 const stage = this.stage; //may change if switch
259 return moves.filter(m => {
260 // Checkered cannot be under check (no king)
261 if (stage == 2 && this.sideCheckered == color) return true;
262 this.play(m);
263 let res = true;
264 if (stage == 1) {
265 if (m.appear.length == 0 && m.vanish.length == 0) {
266 // Special "switch" move: kings must not be attacked by checkered.
267 // Not checking for oppositeMoves here: checkered are autonomous
268 res = (
269 !this.isAttacked(this.kingPos['w'], ['c']) &&
270 !this.isAttacked(this.kingPos['b'], ['c']) &&
271 this.getAllPotentialMoves().length > 0
272 );
273 }
274 else res = !this.oppositeMoves(this.cmoves[L - 1], m);
275 }
276 if (res && m.appear.length > 0) res = !this.underCheck(color);
277 // At stage 2, side with B & W can be undercheck with both kings:
278 if (res && stage == 2) res = !this.underCheck(oppCol);
279 this.undo(m);
280 return res;
281 });
282 }
283
284 getAllPotentialMoves() {
285 const color = this.turn;
286 const oppCol = V.GetOppCol(color);
287 let potentialMoves = [];
288 for (let i = 0; i < V.size.x; i++) {
289 for (let j = 0; j < V.size.y; j++) {
290 const colIJ = this.getColor(i, j);
291 if (
292 this.board[i][j] != V.EMPTY &&
293 (
294 (this.stage == 1 && colIJ != oppCol) ||
295 (this.stage == 2 &&
296 (
297 (this.sideCheckered == color && colIJ == 'c') ||
298 (this.sideCheckered != color && ['w', 'b'].includes(colIJ))
299 )
300 )
301 )
302 ) {
303 Array.prototype.push.apply(
304 potentialMoves,
305 this.getPotentialMovesFrom([i, j])
306 );
307 }
308 }
309 }
310 return potentialMoves;
311 }
312
313 atLeastOneMove() {
314 const color = this.turn;
315 const oppCol = V.GetOppCol(color);
316 for (let i = 0; i < V.size.x; i++) {
317 for (let j = 0; j < V.size.y; j++) {
318 const colIJ = this.getColor(i, j);
319 if (
320 this.board[i][j] != V.EMPTY &&
321 (
322 (this.stage == 1 && colIJ != oppCol) ||
323 (this.stage == 2 &&
324 (
325 (this.sideCheckered == color && colIJ == 'c') ||
326 (this.sideCheckered != color && ['w', 'b'].includes(colIJ))
327 )
328 )
329 )
330 ) {
331 const moves = this.getPotentialMovesFrom([i, j]);
332 if (moves.length > 0) {
333 for (let k = 0; k < moves.length; k++)
334 if (this.filterValid([moves[k]]).length > 0) return true;
335 }
336 }
337 }
338 }
339 return false;
340 }
341
342 // colors: array, 'w' and 'c' or 'b' and 'c' at stage 1,
343 // just 'c' (or unused) at stage 2
344 isAttacked(sq, colors) {
345 if (!Array.isArray(colors)) colors = [colors];
346 return (
347 this.isAttackedByPawn(sq, colors) ||
348 this.isAttackedByRook(sq, colors) ||
349 this.isAttackedByKnight(sq, colors) ||
350 this.isAttackedByBishop(sq, colors) ||
351 this.isAttackedByQueen(sq, colors) ||
352 this.isAttackedByKing(sq, colors)
353 );
354 }
355
356 isAttackedByPawn([x, y], colors) {
357 for (let c of colors) {
358 let shifts = [];
359 if (this.stage == 1) {
360 const color = (c == "c" ? this.turn : c);
361 shifts = [color == "w" ? 1 : -1];
362 }
363 else {
364 // Stage 2: checkered pawns are bidirectional
365 if (c == 'c') shifts = [-1, 1];
366 else shifts = [c == "w" ? 1 : -1];
367 }
368 for (let pawnShift of shifts) {
369 if (x + pawnShift >= 0 && x + pawnShift < 8) {
370 for (let i of [-1, 1]) {
371 if (
372 y + i >= 0 &&
373 y + i < 8 &&
374 this.getPiece(x + pawnShift, y + i) == V.PAWN &&
375 this.getColor(x + pawnShift, y + i) == c
376 ) {
377 return true;
378 }
379 }
380 }
381 }
382 }
383 return false;
384 }
385
386 isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) {
387 for (let step of steps) {
388 let rx = x + step[0],
389 ry = y + step[1];
390 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
391 rx += step[0];
392 ry += step[1];
393 }
394 if (
395 V.OnBoard(rx, ry) &&
396 this.getPiece(rx, ry) === piece &&
397 colors.includes(this.getColor(rx, ry))
398 ) {
399 return true;
400 }
401 }
402 return false;
403 }
404
405 isAttackedByRook(sq, colors) {
406 return this.isAttackedBySlideNJump(sq, colors, V.ROOK, V.steps[V.ROOK]);
407 }
408
409 isAttackedByKnight(sq, colors) {
410 return this.isAttackedBySlideNJump(
411 sq,
412 colors,
413 V.KNIGHT,
414 V.steps[V.KNIGHT],
415 "oneStep"
416 );
417 }
418
419 isAttackedByBishop(sq, colors) {
420 return this.isAttackedBySlideNJump(
421 sq, colors, V.BISHOP, V.steps[V.BISHOP]);
422 }
423
424 isAttackedByQueen(sq, colors) {
425 return this.isAttackedBySlideNJump(
426 sq,
427 colors,
428 V.QUEEN,
429 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
430 );
431 }
432
433 isAttackedByKing(sq, colors) {
434 return this.isAttackedBySlideNJump(
435 sq,
436 colors,
437 V.KING,
438 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
439 "oneStep"
440 );
441 }
442
443 underCheck(color) {
444 if (this.stage == 1)
445 return this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"]);
446 if (color == this.sideCheckered) return false;
447 return (
448 this.isAttacked(this.kingPos['w'], ["c"]) ||
449 this.isAttacked(this.kingPos['b'], ["c"])
450 );
451 }
452
453 getCheckSquares() {
454 const color = this.turn;
455 if (this.stage == 1) {
456 // Artifically change turn, for checkered pawns
457 this.turn = V.GetOppCol(color);
458 const kingAttacked =
459 this.isAttacked(
460 this.kingPos[color],
461 [V.GetOppCol(color), "c"]
462 );
463 let res = kingAttacked
464 ? [JSON.parse(JSON.stringify(this.kingPos[color]))]
465 : [];
466 this.turn = color;
467 return res;
468 }
469 if (this.sideCheckered == color) return [];
470 let res = [];
471 for (let c of ['w', 'b']) {
472 if (this.isAttacked(this.kingPos[c], ['c']))
473 res.push(JSON.parse(JSON.stringify(this.kingPos[c])));
474 }
475 return res;
476 }
477
478 play(move) {
479 move.flags = JSON.stringify(this.aggregateFlags());
480 this.epSquares.push(this.getEpSquare(move));
481 V.PlayOnBoard(this.board, move);
482 if (move.appear.length > 0 || move.vanish.length > 0)
483 {
484 this.turn = V.GetOppCol(this.turn);
485 this.movesCount++;
486 }
487 this.postPlay(move);
488 }
489
490 updateCastleFlags(move, piece) {
491 const c = V.GetOppCol(this.turn);
492 const firstRank = (c == "w" ? V.size.x - 1 : 0);
493 // Update castling flags if rooks are moved
494 const oppCol = this.turn;
495 const oppFirstRank = V.size.x - 1 - firstRank;
496 if (piece == V.KING && move.appear.length > 0)
497 this.castleFlags[c] = [V.size.y, V.size.y];
498 else if (
499 move.start.x == firstRank && //our rook moves?
500 this.castleFlags[c].includes(move.start.y)
501 ) {
502 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
503 this.castleFlags[c][flagIdx] = V.size.y;
504 }
505 // NOTE: not "else if" because a rook could take an opposing rook
506 if (
507 move.end.x == oppFirstRank && //we took opponent rook?
508 this.castleFlags[oppCol].includes(move.end.y)
509 ) {
510 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
511 this.castleFlags[oppCol][flagIdx] = V.size.y;
512 }
513 }
514
515 postPlay(move) {
516 if (move.appear.length == 0 && move.vanish.length == 0) {
517 this.stage = 2;
518 this.sideCheckered = this.turn;
519 }
520 else {
521 const c = move.vanish[0].c;
522 const piece = move.vanish[0].p;
523 if (piece == V.KING) {
524 this.kingPos[c][0] = move.appear[0].x;
525 this.kingPos[c][1] = move.appear[0].y;
526 }
527 this.updateCastleFlags(move, piece);
528 // Does this move turn off a 2-squares pawn flag?
529 if ([1, 6].includes(move.start.x) && move.vanish[0].p == V.PAWN)
530 this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
531 }
532 this.cmoves.push(this.getCmove(move));
533 }
534
535 undo(move) {
536 this.epSquares.pop();
537 this.disaggregateFlags(JSON.parse(move.flags));
538 V.UndoOnBoard(this.board, move);
539 if (move.appear.length > 0 || move.vanish.length > 0)
540 {
541 this.turn = V.GetOppCol(this.turn);
542 this.movesCount--;
543 }
544 this.postUndo(move);
545 }
546
547 postUndo(move) {
548 if (move.appear.length == 0 && move.vanish.length == 0) this.stage = 1;
549 else super.postUndo(move);
550 this.cmoves.pop();
551 }
552
553 getCurrentScore() {
554 const color = this.turn;
555 if (this.stage == 1) {
556 if (this.atLeastOneMove()) return "*";
557 // Artifically change turn, for checkered pawns
558 this.turn = V.GetOppCol(this.turn);
559 const res =
560 this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"])
561 ? color == "w"
562 ? "0-1"
563 : "1-0"
564 : "1/2";
565 this.turn = V.GetOppCol(this.turn);
566 return res;
567 }
568 // Stage == 2:
569 if (this.sideCheckered == this.turn) {
570 // Check if remaining checkered pieces: if none, I lost
571 if (this.board.some(b => b.some(cell => cell[0] == 'c'))) {
572 if (!this.atLeastOneMove()) return "1/2";
573 return "*";
574 }
575 return color == 'w' ? "0-1" : "1-0";
576 }
577 if (this.atLeastOneMove()) return "*";
578 let res = this.isAttacked(this.kingPos['w'], ["c"]);
579 if (!res) res = this.isAttacked(this.kingPos['b'], ["c"]);
580 if (res) return color == 'w' ? "0-1" : "1-0";
581 return "1/2";
582 }
583
584 evalPosition() {
585 let evaluation = 0;
586 // Just count material for now, considering checkered neutral at stage 1.
587 const baseSign = (this.turn == 'w' ? 1 : -1);
588 for (let i = 0; i < V.size.x; i++) {
589 for (let j = 0; j < V.size.y; j++) {
590 if (this.board[i][j] != V.EMPTY) {
591 const sqColor = this.getColor(i, j);
592 if (this.stage == 1) {
593 if (["w", "b"].includes(sqColor)) {
594 const sign = sqColor == "w" ? 1 : -1;
595 evaluation += sign * V.VALUES[this.getPiece(i, j)];
596 }
597 }
598 else {
599 const sign =
600 this.sideCheckered == this.turn
601 ? (sqColor == 'c' ? 1 : -1) * baseSign
602 : (sqColor == 'c' ? -1 : 1) * baseSign;
603 evaluation += sign * V.VALUES[this.getPiece(i, j)];
604 }
605 }
606 }
607 }
608 return evaluation;
609 }
610
611 static GenRandInitFen(randomness) {
612 // Add 16 pawns flags + empty cmovei + stage == 1:
613 return ChessRules.GenRandInitFen(randomness)
614 .slice(0, -2) + "1111111111111111 - - 1";
615 }
616
617 static ParseFen(fen) {
618 const fenParts = fen.split(" ");
619 return Object.assign(
620 ChessRules.ParseFen(fen),
621 {
622 cmove: fenParts[5],
623 stage: fenParts[6]
624 }
625 );
626 }
627
628 getCmoveFen() {
629 const L = this.cmoves.length;
630 return (
631 !this.cmoves[L - 1]
632 ? "-"
633 : ChessRules.CoordsToSquare(this.cmoves[L - 1].start) +
634 ChessRules.CoordsToSquare(this.cmoves[L - 1].end)
635 );
636 }
637
638 getStageFen() {
639 return (this.stage == 1 ? "1" : "2" + this.sideCheckered);
640 }
641
642 getFen() {
643 return (
644 super.getFen() + " " + this.getCmoveFen() + " " + this.getStageFen()
645 );
646 }
647
648 getFenForRepeat() {
649 return (
650 super.getFenForRepeat() + "_" +
651 this.getCmoveFen() + "_" + this.getStageFen()
652 );
653 }
654
655 getFlagsFen() {
656 let fen = super.getFlagsFen();
657 // Add pawns flags
658 for (let c of ["w", "b"])
659 for (let i = 0; i < 8; i++) fen += (this.pawnFlags[c][i] ? "1" : "0");
660 return fen;
661 }
662
663 static get SEARCH_DEPTH() {
664 return 2;
665 }
666
667 getComputerMove() {
668 // To simplify, prevent the bot from switching (TODO...)
669 return (
670 super.getComputerMove(
671 this.getAllValidMoves().filter(m => m.appear.length > 0)
672 )
673 );
674 }
675
676 getNotation(move) {
677 if (move.appear.length == 0 && move.vanish.length == 0) return "S";
678 if (move.appear.length == 2) {
679 // Castle
680 if (move.end.y < move.start.y) return "0-0-0";
681 return "0-0";
682 }
683
684 const finalSquare = V.CoordsToSquare(move.end);
685 const piece = this.getPiece(move.start.x, move.start.y);
686 let notation = "";
687 if (piece == V.PAWN) {
688 if (move.vanish.length > 1) {
689 const startColumn = V.CoordToColumn(move.start.y);
690 notation = startColumn + "x" + finalSquare;
691 } else notation = finalSquare;
692 } else {
693 // Piece movement
694 notation =
695 piece.toUpperCase() +
696 (move.vanish.length > 1 ? "x" : "") +
697 finalSquare;
698 }
699 if (move.appear[0].p != move.vanish[0].p)
700 notation += "=" + move.appear[0].p.toUpperCase();
701 return notation;
702 }
703 };