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