A few bugs fixes
[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], noswitch) {
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 (
145 !noswitch &&
146 this.board.some(b => b.some(cell => cell[0] == 'c'))
147 ) {
148 const oppCol = V.GetOppCol(color);
149 moves.push(
150 new Move({
151 start: { x: x, y: y },
152 end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] },
153 appear: [],
154 vanish: []
155 })
156 );
157 }
158 return standardMoves.concat(moves);
159 }
160 standardMoves.forEach(m => {
161 if (m.vanish[0].p == V.PAWN) {
162 if (
163 Math.abs(m.end.x - m.start.x) == 2 &&
164 !this.pawnFlags[this.turn][m.start.y]
165 ) {
166 return; //skip forbidden 2-squares jumps
167 }
168 if (
169 this.board[m.end.x][m.end.y] == V.EMPTY &&
170 m.vanish.length == 2 &&
171 this.getColor(m.start.x, m.start.y) == "c"
172 ) {
173 return; //checkered pawns cannot take en-passant
174 }
175 }
176 if (m.vanish.length == 1)
177 // No capture
178 moves.push(m);
179 else {
180 // A capture occured (m.vanish.length == 2)
181 m.appear[0].c = "c";
182 moves.push(m);
183 if (
184 // Avoid promotions (already treated):
185 m.appear[0].p != m.vanish[1].p &&
186 (m.vanish[0].p != V.PAWN || m.end.x != lastRank)
187 ) {
188 // Add transformation into captured piece
189 let m2 = JSON.parse(JSON.stringify(m));
190 m2.appear[0].p = m.vanish[1].p;
191 moves.push(m2);
192 }
193 }
194 });
195 return moves;
196 }
197 return standardMoves;
198 }
199
200 getPotentialPawnMoves([x, y]) {
201 const color = this.getColor(x, y);
202 if (this.stage == 2) {
203 const saveTurn = this.turn;
204 if (this.sideCheckered == this.turn) {
205 // Cannot change PawnSpecs.bidirectional, so cheat a little:
206 this.turn = 'w';
207 const wMoves = super.getPotentialPawnMoves([x, y]);
208 this.turn = 'b';
209 const bMoves = super.getPotentialPawnMoves([x, y]);
210 this.turn = saveTurn;
211 return wMoves.concat(bMoves);
212 }
213 // Playing with both colors:
214 this.turn = color;
215 const moves = super.getPotentialPawnMoves([x, y]);
216 this.turn = saveTurn;
217 return moves;
218 }
219 let moves = super.getPotentialPawnMoves([x, y]);
220 // Post-process: set right color for checkered moves
221 if (color == 'c') {
222 moves.forEach(m => {
223 m.appear[0].c = 'c'; //may be done twice if capture
224 m.vanish[0].c = 'c';
225 });
226 }
227 return moves;
228 }
229
230 canIplay(side, [x, y]) {
231 if (this.stage == 2) {
232 const color = this.getColor(x, y);
233 return (
234 this.turn == this.sideCheckered
235 ? color == 'c'
236 : ['w', 'b'].includes(color)
237 );
238 }
239 return side == this.turn && [side, "c"].includes(this.getColor(x, y));
240 }
241
242 // Does m2 un-do m1 ? (to disallow undoing checkered moves)
243 oppositeMoves(m1, m2) {
244 return (
245 !!m1 &&
246 m2.appear[0].c == "c" &&
247 m2.appear.length == 1 &&
248 m2.vanish.length == 1 &&
249 m1.start.x == m2.end.x &&
250 m1.end.x == m2.start.x &&
251 m1.start.y == m2.end.y &&
252 m1.end.y == m2.start.y
253 );
254 }
255
256 filterValid(moves) {
257 if (moves.length == 0) return [];
258 const color = this.turn;
259 const oppCol = V.GetOppCol(color);
260 const L = this.cmoves.length; //at least 1: init from FEN
261 const stage = this.stage; //may change if switch
262 return moves.filter(m => {
263 // Checkered cannot be under check (no king)
264 if (stage == 2 && this.sideCheckered == color) return true;
265 this.play(m);
266 let res = true;
267 if (stage == 1) {
268 if (m.appear.length == 0 && m.vanish.length == 0) {
269 // Special "switch" move: kings must not be attacked by checkered.
270 // Not checking for oppositeMoves here: checkered are autonomous
271 res = (
272 !this.isAttacked(this.kingPos['w'], ['c']) &&
273 !this.isAttacked(this.kingPos['b'], ['c']) &&
274 this.getAllPotentialMoves().length > 0
275 );
276 }
277 else res = !this.oppositeMoves(this.cmoves[L - 1], m);
278 }
279 if (res && m.appear.length > 0) res = !this.underCheck(color);
280 // At stage 2, side with B & W can be undercheck with both kings:
281 if (res && stage == 2) res = !this.underCheck(oppCol);
282 this.undo(m);
283 return res;
284 });
285 }
286
287 getAllPotentialMoves() {
288 const color = this.turn;
289 const oppCol = V.GetOppCol(color);
290 let potentialMoves = [];
291 for (let i = 0; i < V.size.x; i++) {
292 for (let j = 0; j < V.size.y; j++) {
293 const colIJ = this.getColor(i, j);
294 if (
295 this.board[i][j] != V.EMPTY &&
296 (
297 (this.stage == 1 && colIJ != oppCol) ||
298 (this.stage == 2 &&
299 (
300 (this.sideCheckered == color && colIJ == 'c') ||
301 (this.sideCheckered != color && ['w', 'b'].includes(colIJ))
302 )
303 )
304 )
305 ) {
306 Array.prototype.push.apply(
307 potentialMoves,
308 this.getPotentialMovesFrom([i, j])
309 );
310 }
311 }
312 }
313 return potentialMoves;
314 }
315
316 atLeastOneMove() {
317 const color = this.turn;
318 const oppCol = V.GetOppCol(color);
319 for (let i = 0; i < V.size.x; i++) {
320 for (let j = 0; j < V.size.y; j++) {
321 const colIJ = this.getColor(i, j);
322 if (
323 this.board[i][j] != V.EMPTY &&
324 (
325 (this.stage == 1 && colIJ != oppCol) ||
326 (this.stage == 2 &&
327 (
328 (this.sideCheckered == color && colIJ == 'c') ||
329 (this.sideCheckered != color && ['w', 'b'].includes(colIJ))
330 )
331 )
332 )
333 ) {
334 const moves = this.getPotentialMovesFrom([i, j], "noswitch");
335 if (moves.length > 0) {
336 for (let k = 0; k < moves.length; k++)
337 if (this.filterValid([moves[k]]).length > 0) return true;
338 }
339 }
340 }
341 }
342 return false;
343 }
344
345 // colors: array, 'w' and 'c' or 'b' and 'c' at stage 1,
346 // just 'c' (or unused) at stage 2
347 isAttacked(sq, colors) {
348 if (!Array.isArray(colors)) colors = [colors];
349 return (
350 this.isAttackedByPawn(sq, colors) ||
351 this.isAttackedByRook(sq, colors) ||
352 this.isAttackedByKnight(sq, colors) ||
353 this.isAttackedByBishop(sq, colors) ||
354 this.isAttackedByQueen(sq, colors) ||
355 this.isAttackedByKing(sq, colors)
356 );
357 }
358
359 isAttackedByPawn([x, y], colors) {
360 for (let c of colors) {
361 let shifts = [];
362 if (this.stage == 1) {
363 const color = (c == "c" ? this.turn : c);
364 shifts = [color == "w" ? 1 : -1];
365 }
366 else {
367 // Stage 2: checkered pawns are bidirectional
368 if (c == 'c') shifts = [-1, 1];
369 else shifts = [c == "w" ? 1 : -1];
370 }
371 for (let pawnShift of shifts) {
372 if (x + pawnShift >= 0 && x + pawnShift < 8) {
373 for (let i of [-1, 1]) {
374 if (
375 y + i >= 0 &&
376 y + i < 8 &&
377 this.getPiece(x + pawnShift, y + i) == V.PAWN &&
378 this.getColor(x + pawnShift, y + i) == c
379 ) {
380 return true;
381 }
382 }
383 }
384 }
385 }
386 return false;
387 }
388
389 isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) {
390 for (let step of steps) {
391 let rx = x + step[0],
392 ry = y + step[1];
393 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
394 rx += step[0];
395 ry += step[1];
396 }
397 if (
398 V.OnBoard(rx, ry) &&
399 this.getPiece(rx, ry) === piece &&
400 colors.includes(this.getColor(rx, ry))
401 ) {
402 return true;
403 }
404 }
405 return false;
406 }
407
408 isAttackedByRook(sq, colors) {
409 return this.isAttackedBySlideNJump(sq, colors, V.ROOK, V.steps[V.ROOK]);
410 }
411
412 isAttackedByKnight(sq, colors) {
413 return this.isAttackedBySlideNJump(
414 sq,
415 colors,
416 V.KNIGHT,
417 V.steps[V.KNIGHT],
418 "oneStep"
419 );
420 }
421
422 isAttackedByBishop(sq, colors) {
423 return this.isAttackedBySlideNJump(
424 sq, colors, V.BISHOP, V.steps[V.BISHOP]);
425 }
426
427 isAttackedByQueen(sq, colors) {
428 return this.isAttackedBySlideNJump(
429 sq,
430 colors,
431 V.QUEEN,
432 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
433 );
434 }
435
436 isAttackedByKing(sq, colors) {
437 return this.isAttackedBySlideNJump(
438 sq,
439 colors,
440 V.KING,
441 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
442 "oneStep"
443 );
444 }
445
446 underCheck(color) {
447 if (this.stage == 1)
448 return this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"]);
449 if (color == this.sideCheckered) return false;
450 return (
451 this.isAttacked(this.kingPos['w'], ["c"]) ||
452 this.isAttacked(this.kingPos['b'], ["c"])
453 );
454 }
455
456 getCheckSquares() {
457 const color = this.turn;
458 if (this.stage == 1) {
459 // Artifically change turn, for checkered pawns
460 this.turn = V.GetOppCol(color);
461 const kingAttacked =
462 this.isAttacked(
463 this.kingPos[color],
464 [V.GetOppCol(color), "c"]
465 );
466 let res = kingAttacked
467 ? [JSON.parse(JSON.stringify(this.kingPos[color]))]
468 : [];
469 this.turn = color;
470 return res;
471 }
472 if (this.sideCheckered == color) return [];
473 let res = [];
474 for (let c of ['w', 'b']) {
475 if (this.isAttacked(this.kingPos[c], ['c']))
476 res.push(JSON.parse(JSON.stringify(this.kingPos[c])));
477 }
478 return res;
479 }
480
481 play(move) {
482 move.flags = JSON.stringify(this.aggregateFlags());
483 this.epSquares.push(this.getEpSquare(move));
484 V.PlayOnBoard(this.board, move);
485 if (move.appear.length > 0 || move.vanish.length > 0)
486 {
487 this.turn = V.GetOppCol(this.turn);
488 this.movesCount++;
489 }
490 this.postPlay(move);
491 }
492
493 postPlay(move) {
494 if (move.appear.length == 0 && move.vanish.length == 0) {
495 this.stage = 2;
496 this.sideCheckered = this.turn;
497 }
498 else {
499 const c = move.vanish[0].c;
500 const piece = move.vanish[0].p;
501 if (piece == V.KING) {
502 this.kingPos[c][0] = move.appear[0].x;
503 this.kingPos[c][1] = move.appear[0].y;
504 }
505 super.updateCastleFlags(move, piece);
506 // Does this move turn off a 2-squares pawn flag?
507 if ([1, 6].includes(move.start.x) && move.vanish[0].p == V.PAWN)
508 this.pawnFlags[move.start.x == 6 ? "w" : "b"][move.start.y] = false;
509 }
510 this.cmoves.push(this.getCmove(move));
511 }
512
513 undo(move) {
514 this.epSquares.pop();
515 this.disaggregateFlags(JSON.parse(move.flags));
516 V.UndoOnBoard(this.board, move);
517 if (move.appear.length > 0 || move.vanish.length > 0)
518 {
519 this.turn = V.GetOppCol(this.turn);
520 this.movesCount--;
521 }
522 this.postUndo(move);
523 }
524
525 postUndo(move) {
526 if (move.appear.length == 0 && move.vanish.length == 0) this.stage = 1;
527 else super.postUndo(move);
528 this.cmoves.pop();
529 }
530
531 getCurrentScore() {
532 const color = this.turn;
533 if (this.stage == 1) {
534 if (this.atLeastOneMove()) return "*";
535 // Artifically change turn, for checkered pawns
536 this.turn = V.GetOppCol(this.turn);
537 const res =
538 this.isAttacked(this.kingPos[color], [V.GetOppCol(color), "c"])
539 ? color == "w"
540 ? "0-1"
541 : "1-0"
542 : "1/2";
543 this.turn = V.GetOppCol(this.turn);
544 return res;
545 }
546 // Stage == 2:
547 if (this.sideCheckered == this.turn) {
548 // Check if remaining checkered pieces: if none, I lost
549 if (this.board.some(b => b.some(cell => cell[0] == 'c'))) {
550 if (!this.atLeastOneMove()) return "1/2";
551 return "*";
552 }
553 return color == 'w' ? "0-1" : "1-0";
554 }
555 if (this.atLeastOneMove()) return "*";
556 let res = this.isAttacked(this.kingPos['w'], ["c"]);
557 if (!res) res = this.isAttacked(this.kingPos['b'], ["c"]);
558 if (res) return color == 'w' ? "0-1" : "1-0";
559 return "1/2";
560 }
561
562 evalPosition() {
563 let evaluation = 0;
564 // Just count material for now, considering checkered neutral at stage 1.
565 const baseSign = (this.turn == 'w' ? 1 : -1);
566 for (let i = 0; i < V.size.x; i++) {
567 for (let j = 0; j < V.size.y; j++) {
568 if (this.board[i][j] != V.EMPTY) {
569 const sqColor = this.getColor(i, j);
570 if (this.stage == 1) {
571 if (["w", "b"].includes(sqColor)) {
572 const sign = sqColor == "w" ? 1 : -1;
573 evaluation += sign * V.VALUES[this.getPiece(i, j)];
574 }
575 }
576 else {
577 const sign =
578 this.sideCheckered == this.turn
579 ? (sqColor == 'c' ? 1 : -1) * baseSign
580 : (sqColor == 'c' ? -1 : 1) * baseSign;
581 evaluation += sign * V.VALUES[this.getPiece(i, j)];
582 }
583 }
584 }
585 }
586 return evaluation;
587 }
588
589 static GenRandInitFen(randomness) {
590 // Add 16 pawns flags + empty cmove + stage == 1:
591 return ChessRules.GenRandInitFen(randomness)
592 .slice(0, -2) + "1111111111111111 - - 1";
593 }
594
595 static ParseFen(fen) {
596 const fenParts = fen.split(" ");
597 return Object.assign(
598 ChessRules.ParseFen(fen),
599 {
600 cmove: fenParts[5],
601 stage: fenParts[6]
602 }
603 );
604 }
605
606 getCmoveFen() {
607 const L = this.cmoves.length;
608 return (
609 !this.cmoves[L - 1]
610 ? "-"
611 : ChessRules.CoordsToSquare(this.cmoves[L - 1].start) +
612 ChessRules.CoordsToSquare(this.cmoves[L - 1].end)
613 );
614 }
615
616 getStageFen() {
617 return (this.stage == 1 ? "1" : "2" + this.sideCheckered);
618 }
619
620 getFen() {
621 return (
622 super.getFen() + " " + this.getCmoveFen() + " " + this.getStageFen()
623 );
624 }
625
626 getFenForRepeat() {
627 return (
628 super.getFenForRepeat() + "_" +
629 this.getCmoveFen() + "_" + this.getStageFen()
630 );
631 }
632
633 getFlagsFen() {
634 let fen = super.getFlagsFen();
635 // Add pawns flags
636 for (let c of ["w", "b"])
637 for (let i = 0; i < 8; i++) fen += (this.pawnFlags[c][i] ? "1" : "0");
638 return fen;
639 }
640
641 static get SEARCH_DEPTH() {
642 return 2;
643 }
644
645 getComputerMove() {
646 // To simplify, prevent the bot from switching (TODO...)
647 return (
648 super.getComputerMove(
649 this.getAllValidMoves().filter(m => m.appear.length > 0)
650 )
651 );
652 }
653
654 getNotation(move) {
655 if (move.appear.length == 0 && move.vanish.length == 0) return "S";
656 if (move.appear.length == 2) {
657 // Castle
658 if (move.end.y < move.start.y) return "0-0-0";
659 return "0-0";
660 }
661
662 const finalSquare = V.CoordsToSquare(move.end);
663 const piece = this.getPiece(move.start.x, move.start.y);
664 let notation = "";
665 if (piece == V.PAWN) {
666 if (move.vanish.length > 1) {
667 const startColumn = V.CoordToColumn(move.start.y);
668 notation = startColumn + "x" + finalSquare;
669 } else notation = finalSquare;
670 } else {
671 // Piece movement
672 notation =
673 piece.toUpperCase() +
674 (move.vanish.length > 1 ? "x" : "") +
675 finalSquare;
676 }
677 if (move.appear[0].p != move.vanish[0].p)
678 notation += "=" + move.appear[0].p.toUpperCase();
679 return notation;
680 }
681 };