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