4556f07b3f6cf2fe0c0c7aee91598a4971eee5e8
[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], 10);
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 postPlay(move) {
491 if (move.appear.length == 0 && move.vanish.length == 0) {
492 this.stage = 2;
493 this.sideCheckered = this.turn;
494 }
495 else {
496 const c = move.vanish[0].c;
497 const piece = move.vanish[0].p;
498 if (piece == V.KING) {
499 this.kingPos[c][0] = move.appear[0].x;
500 this.kingPos[c][1] = move.appear[0].y;
501 }
502 super.updateCastleFlags(move, piece);
503 // Does this move turn off a 2-squares pawn flag?
504 if ([1, 6].includes(move.start.x) && move.vanish[0].p == V.PAWN)
505 this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
506 }
507 this.cmoves.push(this.getCmove(move));
508 }
509
510 undo(move) {
511 this.epSquares.pop();
512 this.disaggregateFlags(JSON.parse(move.flags));
513 V.UndoOnBoard(this.board, move);
514 if (move.appear.length > 0 || move.vanish.length > 0)
515 {
516 this.turn = V.GetOppCol(this.turn);
517 this.movesCount--;
518 }
519 this.postUndo(move);
520 }
521
522 postUndo(move) {
523 if (move.appear.length == 0 && move.vanish.length == 0) this.stage = 1;
524 else super.postUndo(move);
525 this.cmoves.pop();
526 }
527
528 getCurrentScore() {
529 const color = this.turn;
530 if (this.stage == 1) {
531 if (this.atLeastOneMove()) return "*";
532 // Artifically change turn, for checkered pawns
533 this.turn = V.GetOppCol(this.turn);
534 const res =
535 this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"])
536 ? color == "w"
537 ? "0-1"
538 : "1-0"
539 : "1/2";
540 this.turn = V.GetOppCol(this.turn);
541 return res;
542 }
543 // Stage == 2:
544 if (this.sideCheckered == this.turn) {
545 // Check if remaining checkered pieces: if none, I lost
546 if (this.board.some(b => b.some(cell => cell[0] == 'c'))) {
547 if (!this.atLeastOneMove()) return "1/2";
548 return "*";
549 }
550 return color == 'w' ? "0-1" : "1-0";
551 }
552 if (this.atLeastOneMove()) return "*";
553 let res = this.isAttacked(this.kingPos['w'], ["c"]);
554 if (!res) res = this.isAttacked(this.kingPos['b'], ["c"]);
555 if (res) return color == 'w' ? "0-1" : "1-0";
556 return "1/2";
557 }
558
559 evalPosition() {
560 let evaluation = 0;
561 // Just count material for now, considering checkered neutral at stage 1.
562 const baseSign = (this.turn == 'w' ? 1 : -1);
563 for (let i = 0; i < V.size.x; i++) {
564 for (let j = 0; j < V.size.y; j++) {
565 if (this.board[i][j] != V.EMPTY) {
566 const sqColor = this.getColor(i, j);
567 if (this.stage == 1) {
568 if (["w", "b"].includes(sqColor)) {
569 const sign = sqColor == "w" ? 1 : -1;
570 evaluation += sign * V.VALUES[this.getPiece(i, j)];
571 }
572 }
573 else {
574 const sign =
575 this.sideCheckered == this.turn
576 ? (sqColor == 'c' ? 1 : -1) * baseSign
577 : (sqColor == 'c' ? -1 : 1) * baseSign;
578 evaluation += sign * V.VALUES[this.getPiece(i, j)];
579 }
580 }
581 }
582 }
583 return evaluation;
584 }
585
586 static GenRandInitFen(randomness) {
587 // Add 16 pawns flags + empty cmove + stage == 1:
588 return ChessRules.GenRandInitFen(randomness)
589 .slice(0, -2) + "1111111111111111 - - 1";
590 }
591
592 static ParseFen(fen) {
593 const fenParts = fen.split(" ");
594 return Object.assign(
595 ChessRules.ParseFen(fen),
596 {
597 cmove: fenParts[5],
598 stage: fenParts[6]
599 }
600 );
601 }
602
603 getCmoveFen() {
604 const L = this.cmoves.length;
605 return (
606 !this.cmoves[L - 1]
607 ? "-"
608 : ChessRules.CoordsToSquare(this.cmoves[L - 1].start) +
609 ChessRules.CoordsToSquare(this.cmoves[L - 1].end)
610 );
611 }
612
613 getStageFen() {
614 return (this.stage == 1 ? "1" : "2" + this.sideCheckered);
615 }
616
617 getFen() {
618 return (
619 super.getFen() + " " + this.getCmoveFen() + " " + this.getStageFen()
620 );
621 }
622
623 getFenForRepeat() {
624 return (
625 super.getFenForRepeat() + "_" +
626 this.getCmoveFen() + "_" + this.getStageFen()
627 );
628 }
629
630 getFlagsFen() {
631 let fen = super.getFlagsFen();
632 // Add pawns flags
633 for (let c of ["w", "b"])
634 for (let i = 0; i < 8; i++) fen += (this.pawnFlags[c][i] ? "1" : "0");
635 return fen;
636 }
637
638 static get SEARCH_DEPTH() {
639 return 2;
640 }
641
642 getComputerMove() {
643 // To simplify, prevent the bot from switching (TODO...)
644 return (
645 super.getComputerMove(
646 this.getAllValidMoves().filter(m => m.appear.length > 0)
647 )
648 );
649 }
650
651 getNotation(move) {
652 if (move.appear.length == 0 && move.vanish.length == 0) return "S";
653 if (move.appear.length == 2) {
654 // Castle
655 if (move.end.y < move.start.y) return "0-0-0";
656 return "0-0";
657 }
658
659 const finalSquare = V.CoordsToSquare(move.end);
660 const piece = this.getPiece(move.start.x, move.start.y);
661 let notation = "";
662 if (piece == V.PAWN) {
663 if (move.vanish.length > 1) {
664 const startColumn = V.CoordToColumn(move.start.y);
665 notation = startColumn + "x" + finalSquare;
666 } else notation = finalSquare;
667 } else {
668 // Piece movement
669 notation =
670 piece.toUpperCase() +
671 (move.vanish.length > 1 ? "x" : "") +
672 finalSquare;
673 }
674 if (move.appear[0].p != move.vanish[0].p)
675 notation += "=" + move.appear[0].p.toUpperCase();
676 return notation;
677 }
678 };