Fix Pandemonium pawn moves
[vchess.git] / client / src / variants / Pandemonium.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class PandemoniumRules extends ChessRules {
5
6 static get PawnSpecs() {
7 return Object.assign(
8 {},
9 ChessRules.PawnSpecs,
10 { promotions: [V.GILDING] }
11 );
12 }
13
14 loseOnRepetition() {
15 // If current side is under check: lost
16 return this.underCheck(this.turn);
17 }
18
19 static get GILDING() {
20 return "g";
21 }
22
23 static get SCEPTER() {
24 return "s";
25 }
26
27 static get HORSE() {
28 return "h";
29 }
30
31 static get DRAGON() {
32 return "d";
33 }
34
35 static get CARDINAL() {
36 return "c";
37 }
38
39 static get WHOLE() {
40 return "w";
41 }
42
43 static get MARSHAL() {
44 return "m";
45 }
46
47 static get APRICOT() {
48 return "a";
49 }
50
51 static get PIECES() {
52 return (
53 ChessRules.PIECES.concat([
54 V.GILDING, V.SCEPTER, V.HORSE, V.DRAGON,
55 V.CARDINAL, V.WHOLE, V.MARSHAL, V.APRICOT])
56 );
57 }
58
59 getPpath(b) {
60 const prefix = (ChessRules.PIECES.includes(b[1]) ? "" : "Pandemonium/");
61 return prefix + b;
62 }
63
64 static get size() {
65 return { x: 10, y: 10};
66 }
67
68 getColor(i, j) {
69 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
70 return this.board[i][j].charAt(0);
71 }
72
73 getPiece(i, j) {
74 if (i >= V.size.x) return V.RESERVE_PIECES[j];
75 return this.board[i][j].charAt(1);
76 }
77
78 setOtherVariables(fen) {
79 super.setOtherVariables(fen);
80 // Sub-turn is useful only at first move...
81 this.subTurn = 1;
82 // Also init reserves (used by the interface to show landable pieces)
83 const reserve =
84 V.ParseFen(fen).reserve.split("").map(x => parseInt(x, 10));
85 this.reserve = {
86 w: {
87 [V.PAWN]: reserve[0],
88 [V.ROOK]: reserve[1],
89 [V.KNIGHT]: reserve[2],
90 [V.BISHOP]: reserve[3],
91 [V.QUEEN]: reserve[4],
92 [V.CARDINAL]: reserve[5],
93 [V.MARSHAL]: reserve[6],
94 },
95 b: {
96 [V.PAWN]: reserve[7],
97 [V.ROOK]: reserve[8],
98 [V.KNIGHT]: reserve[9],
99 [V.BISHOP]: reserve[10],
100 [V.QUEEN]: reserve[11],
101 [V.CARDINAL]: reserve[12],
102 [V.MARSHAL]: reserve[13]
103 }
104 };
105 }
106
107 static IsGoodEnpassant(enpassant) {
108 if (enpassant != "-") {
109 const squares = enpassant.split(",");
110 if (squares.length > 2) return false;
111 for (let sq of squares) {
112 if (!sq.match(/[a-j0-9]/)) return false;
113 }
114 }
115 return true;
116 }
117
118 static IsGoodFen(fen) {
119 if (!ChessRules.IsGoodFen(fen)) return false;
120 const fenParsed = V.ParseFen(fen);
121 // Check reserves
122 if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{14,14}$/))
123 return false;
124 return true;
125 }
126
127 static ParseFen(fen) {
128 const fenParts = fen.split(" ");
129 return Object.assign(
130 ChessRules.ParseFen(fen),
131 { reserve: fenParts[5] }
132 );
133 }
134
135 getFen() {
136 return super.getFen() + " " + this.getReserveFen();
137 }
138
139 getFenForRepeat() {
140 return super.getFenForRepeat() + "_" + this.getReserveFen();
141 }
142
143 getReserveFen() {
144 let counts = new Array(14);
145 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
146 counts[i] = this.reserve["w"][V.RESERVE_PIECES[i]];
147 counts[7 + i] = this.reserve["b"][V.RESERVE_PIECES[i]];
148 }
149 return counts.join("");
150 }
151
152 static GenRandInitFen(randomness) {
153 // No randomization here for now (but initial setup choice)
154 return (
155 "rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR " +
156 "w 0 ajaj - 00000000000000"
157 );
158 // TODO later: randomization too --> 2 bishops, not next to each other.
159 // then knights next to bishops. Then other pieces (...).
160 }
161
162 getEnpassantFen() {
163 const L = this.epSquares.length;
164 if (!this.epSquares[L - 1]) return "-"; //no en-passant
165 let res = "";
166 this.epSquares[L - 1].forEach(sq => {
167 res += V.CoordsToSquare(sq) + ",";
168 });
169 return res.slice(0, -1); //remove last comma
170 }
171
172 getEpSquare(moveOrSquare) {
173 if (!moveOrSquare) return undefined;
174 if (typeof moveOrSquare === "string") {
175 const square = moveOrSquare;
176 if (square == "-") return undefined;
177 let res = [];
178 square.split(",").forEach(sq => {
179 res.push(V.SquareToCoords(sq));
180 });
181 return res;
182 }
183 // Argument is a move:
184 const move = moveOrSquare;
185 const [sx, sy, ex] = [move.start.x, move.start.y, move.end.x];
186 if (this.getPiece(sx, sy) == V.PAWN && Math.abs(sx - ex) >= 2) {
187 const step = (ex - sx) / Math.abs(ex - sx);
188 let res = [{
189 x: sx + step,
190 y: sy
191 }];
192 if (sx + 2 * step != ex) {
193 // 3-squares jump
194 res.push({
195 x: sx + 2 * step,
196 y: sy
197 });
198 }
199 return res;
200 }
201 return undefined; //default
202 }
203
204 getReservePpath(index, color) {
205 const p = V.RESERVE_PIECES[index];
206 const prefix = (ChessRules.PIECES.includes(p) ? "" : "Pandemonium/");
207 return prefix + color + p;;
208 }
209
210 // Ordering on reserve pieces
211 static get RESERVE_PIECES() {
212 return (
213 [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.CARDINAL, V.MARSHAL]
214 );
215 }
216
217 getReserveMoves([x, y]) {
218 const color = this.turn;
219 const oppCol = V.GetOppCol(color);
220 const p = V.RESERVE_PIECES[y];
221 if (this.reserve[color][p] == 0) return [];
222 const bounds = (p == V.PAWN ? [1, V.size.x - 1] : [0, V.size.x]);
223 let moves = [];
224 for (let i = bounds[0]; i < bounds[1]; i++) {
225 for (let j = 0; j < V.size.y; j++) {
226 if (this.board[i][j] == V.EMPTY) {
227 let mv = new Move({
228 appear: [
229 new PiPo({
230 x: i,
231 y: j,
232 c: color,
233 p: p
234 })
235 ],
236 vanish: [],
237 start: { x: x, y: y }, //a bit artificial...
238 end: { x: i, y: j }
239 });
240 if (p == V.PAWN) {
241 // Do not drop on checkmate:
242 this.play(mv);
243 const res = (
244 this.underCheck(oppCol) && !this.atLeastOneMove("noReserve")
245 );
246 this.undo(mv);
247 if (res) continue;
248 }
249 moves.push(mv);
250 }
251 }
252 }
253 return moves;
254 }
255
256 static get PromoteMap() {
257 return {
258 r: 'd',
259 n: 's',
260 b: 'h',
261 c: 'w',
262 m: 'a'
263 };
264 }
265
266 getPotentialMovesFrom([x, y]) {
267 const c = this.getColor(x, y);
268 const oppCol = V.GetOppCol(c);
269 if (this.movesCount <= 1) {
270 if (this.kingPos[c][0] == x && this.kingPos[c][1] == y) {
271 // Pass (if setup is ok)
272 return [
273 new Move({
274 appear: [],
275 vanish: [],
276 start: { x: this.kingPos[c][0], y: this.kingPos[c][1] },
277 end: { x: this.kingPos[oppCol][0], y: this.kingPos[oppCol][1] }
278 })
279 ];
280 }
281 const firstRank = (this.movesCount == 0 ? 9 : 0);
282 // TODO: initDestFile currently hardcoded for deterministic setup
283 const initDestFile = new Map([[1, 2], [8, 7]]);
284 // Only option is knight --> bishop swap:
285 if (
286 x == firstRank &&
287 !!initDestFile.get(y) &&
288 this.getPiece(x, y) == V.KNIGHT
289 ) {
290 const destFile = initDestFile.get(y);
291 return [
292 new Move({
293 appear: [
294 new PiPo({
295 x: x,
296 y: destFile,
297 c: c,
298 p: V.KNIGHT
299 }),
300 new PiPo({
301 x: x,
302 y: y,
303 c: c,
304 p: V.BISHOP
305 })
306 ],
307 vanish: [
308 new PiPo({
309 x: x,
310 y: y,
311 c: c,
312 p: V.KNIGHT
313 }),
314 new PiPo({
315 x: x,
316 y: destFile,
317 c: c,
318 p: V.BISHOP
319 })
320 ],
321 start: { x: x, y: y },
322 end: { x: x, y: destFile }
323 })
324 ];
325 }
326 return [];
327 }
328 // Normal move (after initial setup)
329 if (x >= V.size.x) return this.getReserveMoves([x, y]);
330 const p = this.getPiece(x, y);
331 const sq = [x, y];
332 let moves = [];
333 if (ChessRules.PIECES.includes(p))
334 moves = super.getPotentialMovesFrom(sq);
335 if ([V.GILDING, V.APRICOT, V.WHOLE].includes(p))
336 moves = super.getPotentialQueenMoves(sq);
337 switch (p) {
338 case V.SCEPTER:
339 moves = this.getPotentialScepterMoves(sq);
340 break;
341 case V.HORSE:
342 moves = this.getPotentialHorseMoves(sq);
343 break;
344 case V.DRAGON:
345 moves = this.getPotentialDragonMoves(sq);
346 break;
347 case V.CARDINAL:
348 moves = this.getPotentialCardinalMoves(sq);
349 break;
350 case V.MARSHAL:
351 moves = this.getPotentialMarshalMoves(sq);
352 break;
353 }
354 // Maybe apply promotions:
355 if (Object.keys(V.PromoteMap).includes(p)) {
356 const promoted = V.PromoteMap[p];
357 const lastRank = (c == 'w' ? 0 : 9);
358 let promotions = [];
359 moves.forEach(m => {
360 if (m.start.x == lastRank || m.end.x == lastRank) {
361 let pMove = JSON.parse(JSON.stringify(m));
362 pMove.appear[0].p = promoted;
363 promotions.push(pMove);
364 }
365 });
366 Array.prototype.push.apply(moves, promotions);
367 }
368 return moves;
369 }
370
371 getPotentialPawnMoves([x, y]) {
372 const color = this.turn;
373 const shiftX = (color == 'w' ? -1 : 1);
374 let moves = [];
375 if (this.board[x + shiftX][y] == V.EMPTY) {
376 this.addPawnMoves([x, y], [x + shiftX, y], moves);
377 if ((color == 'w' && x >= V.size.x - 3) || (color == 'b' && x <= 2)) {
378 if (this.board[x + 2 * shiftX][y] == V.EMPTY) {
379 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
380 if (
381 (
382 (color == 'w' && x == V.size.x - 2) ||
383 (color == 'b' && x == 1)
384 )
385 &&
386 this.board[x + 3 * shiftX][y] == V.EMPTY
387 ) {
388 moves.push(this.getBasicMove([x, y], [x + 3 * shiftX, y]));
389 }
390 }
391 }
392 }
393 for (let shiftY of [-1, 1]) {
394 if (y + shiftY >= 0 && y + shiftY < V.size.y) {
395 if (
396 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
397 this.canTake([x, y], [x + shiftX, y + shiftY])
398 ) {
399 this.addPawnMoves([x, y], [x + shiftX, y + shiftY], moves);
400 }
401 }
402 }
403 Array.prototype.push.apply(
404 moves,
405 this.getEnpassantCaptures([x, y], shiftX)
406 );
407 return moves;
408 }
409
410 getPotentialMarshalMoves(sq) {
411 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
412 this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")
413 );
414 }
415
416 getPotentialCardinalMoves(sq) {
417 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
418 this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep")
419 );
420 }
421
422 getPotentialScepterMoves(sq) {
423 const steps =
424 V.steps[V.KNIGHT].concat(V.steps[V.BISHOP]).concat(V.steps[V.ROOK]);
425 return this.getSlideNJumpMoves(sq, steps, "oneStep");
426 }
427
428 getPotentialHorseMoves(sq) {
429 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]).concat(
430 this.getSlideNJumpMoves(sq, V.steps[V.ROOK], "oneStep"));
431 }
432
433 getPotentialDragonMoves(sq) {
434 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]).concat(
435 this.getSlideNJumpMoves(sq, V.steps[V.BISHOP], "oneStep"));
436 }
437
438 getEnpassantCaptures([x, y], shiftX) {
439 const Lep = this.epSquares.length;
440 const epSquare = this.epSquares[Lep - 1];
441 let moves = [];
442 if (!!epSquare) {
443 for (let epsq of epSquare) {
444 // TODO: some redundant checks
445 if (epsq.x == x + shiftX && Math.abs(epsq.y - y) == 1) {
446 let enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]);
447 // WARNING: the captured pawn may be diagonally behind us,
448 // if it's a 3-squares jump and we take on 1st passing square
449 const px = this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX;
450 enpassantMove.vanish.push({
451 x: px,
452 y: epsq.y,
453 p: "p",
454 c: this.getColor(px, epsq.y)
455 });
456 moves.push(enpassantMove);
457 }
458 }
459 }
460 return moves;
461 }
462
463 getPotentialKingMoves(sq) {
464 // Initialize with normal moves
465 let moves = this.getSlideNJumpMoves(
466 sq,
467 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
468 "oneStep"
469 );
470 const c = this.turn;
471 if (
472 this.castleFlags[c][0] < V.size.y ||
473 this.castleFlags[c][1] < V.size.y
474 ) {
475 const finalSquares = [
476 [1, 2],
477 [7, 6]
478 ];
479 moves = moves.concat(super.getCastleMoves(sq, finalSquares));
480 }
481 return moves;
482 }
483
484 isAttacked(sq, color) {
485 return (
486 this.isAttackedByPawn(sq, color) ||
487 this.isAttackedByRook(sq, color) ||
488 this.isAttackedByKnight(sq, color) ||
489 this.isAttackedByBishop(sq, color) ||
490 this.isAttackedByKing(sq, color) ||
491 this.isAttackedByQueens(sq, color) ||
492 this.isAttackedByScepter(sq, color) ||
493 this.isAttackedByDragon(sq, color) ||
494 this.isAttackedByHorse(sq, color) ||
495 this.isAttackedByMarshal(sq, color) ||
496 this.isAttackedByCardinal(sq, color)
497 );
498 }
499
500 isAttackedByQueens([x, y], color) {
501 // pieces: because queen = gilding = whole = apricot
502 const pieces = [V.QUEEN, V.GILDING, V.WHOLE, V.APRICOT];
503 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
504 for (let step of steps) {
505 let rx = x + step[0],
506 ry = y + step[1];
507 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY) {
508 rx += step[0];
509 ry += step[1];
510 }
511 if (
512 V.OnBoard(rx, ry) &&
513 this.board[rx][ry] != V.EMPTY &&
514 pieces.includes(this.getPiece(rx, ry)) &&
515 this.getColor(rx, ry) == color
516 ) {
517 return true;
518 }
519 }
520 return false;
521 }
522
523 isAttackedByScepter(sq, color) {
524 const steps =
525 V.steps[V.KNIGHT].concat(V.steps[V.ROOK]).concat(V.steps[V.BISHOP]);
526 return (
527 super.isAttackedBySlideNJump(sq, color, V.SCEPTER, steps, "oneStep")
528 );
529 }
530
531 isAttackedByHorse(sq, color) {
532 return (
533 super.isAttackedBySlideNJump(sq, color, V.steps[V.BISHOP], V.HORSE) ||
534 super.isAttackedBySlideNJump(
535 sq, color, V.HORSE, V.steps[V.ROOK], "oneStep")
536 );
537 }
538
539 isAttackedByDragon(sq, color) {
540 return (
541 super.isAttackedBySlideNJump(sq, color, V.steps[V.ROOK], V.DRAGON) ||
542 super.isAttackedBySlideNJump(
543 sq, color, V.DRAGON, V.steps[V.BISHOP], "oneStep")
544 );
545 }
546
547 isAttackedByMarshal(sq, color) {
548 return (
549 super.isAttackedBySlideNJump(sq, color, V.MARSHAL, V.steps[V.ROOK]) ||
550 super.isAttackedBySlideNJump(
551 sq,
552 color,
553 V.MARSHAL,
554 V.steps[V.KNIGHT],
555 "oneStep"
556 )
557 );
558 }
559
560 isAttackedByCardinal(sq, color) {
561 return (
562 super.isAttackedBySlideNJump(sq, color, V.CARDINAL, V.steps[V.BISHOP]) ||
563 super.isAttackedBySlideNJump(
564 sq,
565 color,
566 V.CARDINAL,
567 V.steps[V.KNIGHT],
568 "oneStep"
569 )
570 );
571 }
572
573 getAllValidMoves() {
574 let moves = super.getAllPotentialMoves();
575 if (this.movesCount >= 2) {
576 const color = this.turn;
577 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
578 moves = moves.concat(
579 this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
580 );
581 }
582 }
583 return this.filterValid(moves);
584 }
585
586 atLeastOneMove(noReserve) {
587 if (!super.atLeastOneMove()) {
588 if (!noReserve) {
589 // Search one reserve move
590 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
591 let moves = this.filterValid(
592 this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
593 );
594 if (moves.length > 0) return true;
595 }
596 }
597 return false;
598 }
599 return true;
600 }
601
602 // Reverse 'PromoteMap'
603 static get P_CORRESPONDANCES() {
604 return {
605 d: 'r',
606 s: 'n',
607 h: 'b',
608 w: 'c',
609 a: 'm',
610 g: 'p'
611 };
612 }
613
614 static MayDecode(piece) {
615 if (Object.keys(V.P_CORRESPONDANCES).includes(piece))
616 return V.P_CORRESPONDANCES[piece];
617 return piece;
618 }
619
620 play(move) {
621 move.subTurn = this.subTurn; //much easier
622 if (this.movesCount >= 2 || this.subTurn == 2 || move.vanish.length == 0) {
623 this.turn = V.GetOppCol(this.turn);
624 this.subTurn = 1;
625 this.movesCount++;
626 }
627 else this.subTurn = 2;
628 move.flags = JSON.stringify(this.aggregateFlags());
629 this.epSquares.push(this.getEpSquare(move));
630 V.PlayOnBoard(this.board, move);
631 this.postPlay(move);
632 }
633
634 postPlay(move) {
635 if (move.vanish.length == 0 && move.appear.length == 0) return;
636 super.postPlay(move);
637 const color = move.appear[0].c;
638 if (move.vanish.length == 0)
639 // Drop unpromoted piece:
640 this.reserve[color][move.appear[0].p]--;
641 else if (move.vanish.length == 2 && move.appear.length == 1)
642 // May capture a promoted piece:
643 this.reserve[color][V.MayDecode(move.vanish[1].p)]++;
644 }
645
646 undo(move) {
647 this.epSquares.pop();
648 this.disaggregateFlags(JSON.parse(move.flags));
649 V.UndoOnBoard(this.board, move);
650 if (this.movesCount >= 2 || this.subTurn == 1 || move.vanish.length == 0) {
651 this.turn = V.GetOppCol(this.turn);
652 this.movesCount--;
653 }
654 this.subTurn = move.subTurn;
655 this.postUndo(move);
656 }
657
658 postUndo(move) {
659 if (move.vanish.length == 0 && move.appear.length == 0) return;
660 super.postUndo(move);
661 const color = move.appear[0].c;
662 if (move.vanish.length == 0)
663 this.reserve[color][move.appear[0].p]++;
664 else if (move.vanish.length == 2 && move.appear.length == 1)
665 this.reserve[color][V.MayDecode(move.vanish[1].p)]--;
666 }
667
668 static get VALUES() {
669 return Object.assign(
670 {},
671 ChessRules.VALUES,
672 {
673 n: 2.5, //knight is weaker
674 g: 9,
675 s: 5,
676 h: 6,
677 d: 7,
678 c: 7,
679 w: 9,
680 m: 8,
681 a: 9
682 }
683 );
684 }
685
686 static get SEARCH_DEPTH() {
687 return 2;
688 }
689
690 getComputerMove() {
691 if (this.movesCount <= 1) {
692 // Special case: swap and pass at random
693 const moves1 = this.getAllValidMoves();
694 const m1 = moves1[randInt(moves1.length)];
695 this.play(m1);
696 if (m1.vanish.length == 0) {
697 this.undo(m1);
698 return m1;
699 }
700 const moves2 = this.getAllValidMoves();
701 const m2 = moves2[randInt(moves2.length)];
702 this.undo(m1);
703 return [m1, m2];
704 }
705 return super.getComputerMove();
706 }
707
708 evalPosition() {
709 let evaluation = super.evalPosition();
710 // Add reserves:
711 for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
712 const p = V.RESERVE_PIECES[i];
713 evaluation += this.reserve["w"][p] * V.VALUES[p];
714 evaluation -= this.reserve["b"][p] * V.VALUES[p];
715 }
716 return evaluation;
717 }
718
719 getNotation(move) {
720 if (move.vanish.length == 0) {
721 if (move.appear.length == 0) return "pass";
722 const pieceName =
723 (move.appear[0].p == V.PAWN ? "" : move.appear[0].p.toUpperCase());
724 return pieceName + "@" + V.CoordsToSquare(move.end);
725 }
726 if (move.appear.length == 2) {
727 if (move.appear[0].p != V.KING)
728 return V.CoordsToSquare(move.start) + "S" + V.CoordsToSquare(move.end);
729 return (move.end.y < move.start.y ? "0-0" : "0-0-0");
730 }
731 let notation = super.getNotation(move);
732 if (move.vanish[0].p != V.PAWN && move.appear[0].p != move.vanish[0].p)
733 // Add promotion indication:
734 notation += "=" + move.appear[0].p.toUpperCase();
735 return notation;
736 }
737
738 };