1 import { ChessRules
, Move
, PiPo
} from "@/base_rules";
2 import { randInt
} from "@/utils/alea";
4 export class PandemoniumRules
extends ChessRules
{
6 static get PawnSpecs() {
12 promotions: [V
.GILDING
]
17 static get GILDING() {
21 static get SCEPTER() {
33 static get CARDINAL() {
41 static get MARSHAL() {
45 static get APRICOT() {
51 ChessRules
.PIECES
.concat([
52 V
.GILDING
, V
.SCEPTER
, V
.HORSE
, V
.DRAGON
,
53 V
.CARDINAL
, V
.WHOLE
, V
.MARSHAL
, V
.APRICOT
])
58 const prefix
= (ChessRules
.PIECES
.includes(b
[1]) ? "" : "Pandemonium/");
63 return { x: 10, y: 10};
67 if (i
>= V
.size
.x
) return i
== V
.size
.x
? "w" : "b";
68 return this.board
[i
][j
].charAt(0);
72 if (i
>= V
.size
.x
) return V
.RESERVE_PIECES
[j
];
73 return this.board
[i
][j
].charAt(1);
76 setOtherVariables(fen
) {
77 super.setOtherVariables(fen
);
78 // Sub-turn is useful only at first move...
80 // Also init reserves (used by the interface to show landable pieces)
82 V
.ParseFen(fen
).reserve
.split("").map(x
=> parseInt(x
, 10));
87 [V
.KNIGHT
]: reserve
[2],
88 [V
.BISHOP
]: reserve
[3],
89 [V
.QUEEN
]: reserve
[4],
90 [V
.CARDINAL
]: reserve
[5],
91 [V
.MARSHAL
]: reserve
[6],
96 [V
.KNIGHT
]: reserve
[9],
97 [V
.BISHOP
]: reserve
[10],
98 [V
.QUEEN
]: reserve
[11],
99 [V
.CARDINAL
]: reserve
[12],
100 [V
.MARSHAL
]: reserve
[13]
105 static IsGoodEnpassant(enpassant
) {
106 if (enpassant
!= "-") {
107 const squares
= enpassant
.split(",");
108 if (squares
.length
> 2) return false;
109 for (let sq
of squares
) {
110 if (!sq
.match(/[a-j0-9]/)) return false;
116 static IsGoodFen(fen
) {
117 if (!ChessRules
.IsGoodFen(fen
)) return false;
118 const fenParsed
= V
.ParseFen(fen
);
120 if (!fenParsed
.reserve
|| !fenParsed
.reserve
.match(/^[0-9]{14,14}$/))
125 static ParseFen(fen
) {
126 const fenParts
= fen
.split(" ");
127 return Object
.assign(
128 ChessRules
.ParseFen(fen
),
129 { reserve: fenParts
[5] }
134 return super.getFen() + " " + this.getReserveFen();
138 return super.getFenForRepeat() + "_" + this.getReserveFen();
142 let counts
= new Array(14);
143 for (let i
= 0; i
< V
.RESERVE_PIECES
.length
; i
++) {
144 counts
[i
] = this.reserve
["w"][V
.RESERVE_PIECES
[i
]];
145 counts
[7 + i
] = this.reserve
["b"][V
.RESERVE_PIECES
[i
]];
147 return counts
.join("");
151 // white a-castle, h-castle, king pos, then same for black.
152 this.castleFlags
= { w: [-1, -1, -1], b: [-1, -1, -1] };
153 for (let i
= 0; i
< 6; i
++) {
154 this.castleFlags
[i
< 3 ? "w" : "b"][i
% 3] =
155 V
.ColumnToCoord(fenflags
.charAt(i
));
159 static GenRandInitFen(randomness
) {
160 // No randomization here for now (but initial setup choice)
162 "rnbqkmcbnr/pppppppppp/91/91/91/91/91/91/PPPPPPPPPP/RNBQKMCBNR " +
163 "w 0 ajeaje - 00000000000000"
165 // TODO later: randomization too --> 2 bishops, not next to each other.
166 // then knights next to bishops. Then other pieces (...).
170 const L
= this.epSquares
.length
;
171 if (!this.epSquares
[L
- 1]) return "-"; //no en-passant
173 this.epSquares
[L
- 1].forEach(sq
=> {
174 res
+= V
.CoordsToSquare(sq
) + ",";
176 return res
.slice(0, -1); //remove last comma
179 getEpSquare(moveOrSquare
) {
180 if (!moveOrSquare
) return undefined;
181 if (typeof moveOrSquare
=== "string") {
182 const square
= moveOrSquare
;
183 if (square
== "-") return undefined;
185 square
.split(",").forEach(sq
=> {
186 res
.push(V
.SquareToCoords(sq
));
190 // Argument is a move:
191 const move = moveOrSquare
;
192 const [sx
, sy
, ex
] = [move.start
.x
, move.start
.y
, move.end
.x
];
193 if (this.getPiece(sx
, sy
) == V
.PAWN
&& Math
.abs(sx
- ex
) >= 2) {
194 const step
= (ex
- sx
) / Math
.abs(ex
- sx
);
199 if (sx
+ 2 * step
!= ex
) {
208 return undefined; //default
211 getReservePpath(index
, color
) {
212 const p
= V
.RESERVE_PIECES
[index
];
213 const prefix
= (ChessRules
.PIECES
.includes(p
) ? "" : "Pandemonium/");
214 return prefix
+ color
+ p
;;
217 // Ordering on reserve pieces
218 static get RESERVE_PIECES() {
220 [V
.PAWN
, V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.CARDINAL
, V
.MARSHAL
]
224 getReserveMoves([x
, y
]) {
225 const color
= this.turn
;
226 const oppCol
= V
.GetOppCol(color
);
227 const p
= V
.RESERVE_PIECES
[y
];
228 if (this.reserve
[color
][p
] == 0) return [];
229 const bounds
= (p
== V
.PAWN
? [1, V
.size
.x
- 1] : [0, V
.size
.x
]);
231 for (let i
= bounds
[0]; i
< bounds
[1]; i
++) {
232 for (let j
= 0; j
< V
.size
.y
; j
++) {
233 if (this.board
[i
][j
] == V
.EMPTY
) {
244 start: { x: x
, y: y
}, //a bit artificial...
248 // Do not drop on checkmate:
251 this.underCheck(oppCol
) && !this.atLeastOneMove("noReserve")
263 static get PromoteMap() {
273 getPotentialMovesFrom([x
, y
]) {
274 const c
= this.getColor(x
, y
);
275 const oppCol
= V
.GetOppCol(c
);
276 if (this.movesCount
<= 1) {
277 if (this.kingPos
[c
][0] == x
&& this.kingPos
[c
][1] == y
) {
278 // Pass (if setup is ok)
283 start: { x: this.kingPos
[c
][0], y: this.kingPos
[c
][1] },
284 end: { x: this.kingPos
[oppCol
][0], y: this.kingPos
[oppCol
][1] }
288 const firstRank
= (this.movesCount
== 0 ? 9 : 0);
289 // TODO: initDestFile currently hardcoded for deterministic setup
290 const initDestFile
= new Map([[1, 2], [8, 7]]);
291 // Only option is knight --> bishop swap:
294 !!initDestFile
.get(y
) &&
295 this.getPiece(x
, y
) == V
.KNIGHT
297 const destFile
= initDestFile
.get(y
);
328 start: { x: x
, y: y
},
329 end: { x: x
, y: destFile
}
335 // Normal move (after initial setup)
336 if (x
>= V
.size
.x
) return this.getReserveMoves(x
, y
);
337 const p
= this.getPiece(x
, y
);
340 if (ChessRules
.PIECES
.includes(p
))
341 moves
= super.getPotentialMovesFrom(sq
);
342 if ([V
.GILDING
, V
.APRICOT
, V
.WHOLE
].includes(p
))
343 moves
= super.getPotentialQueenMoves(sq
);
346 moves
= this.getPotentialScepterMoves(sq
);
349 moves
= this.getPotentialHorseMoves(sq
);
352 moves
= this.getPotentialDragonMoves(sq
);
355 moves
= this.getPotentialCardinalMoves(sq
);
358 moves
= this.getPotentialMarshalMoves(sq
);
361 // Maybe apply promotions:
362 if (Object
.keys(V
.PromoteMap
).includes(p
)) {
363 const promoted
= V
.PromoteMap
[p
];
364 const lastRank
= (c
== 'w' ? 0 : 9);
367 if (m
.start
.x
== lastRank
|| m
.end
.x
== lastRank
) {
368 let pMove
= JSON
.parse(JSON
.stringify(m
));
369 pMove
.appear
[0].p
= promoted
;
370 promotions
.push(pMove
);
373 Array
.prototype.push
.apply(moves
, promotions
);
378 getPotentialPawnMoves([x
, y
]) {
379 const color
= this.turn
;
380 const shiftX
= V
.PawnSpecs
.directions
[color
];
382 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
383 this.addPawnMoves([x
, y
], [x
+ shiftX
, y
], moves
);
384 if ((color
== 'w' && x
>= V
.size
.x
- 3) || (color
== 'b' && x
<= 3)) {
385 if (this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
) {
386 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
389 (color
== 'w' && x
>= V
.size
.x
- 2) ||
390 (color
== 'b' && x
<= 2)
393 this.board
[x
+ 3 * shiftX
][y
] == V
.EMPTY
395 moves
.push(this.getBasicMove([x
, y
], [x
+ 3 * shiftX
, y
]));
400 for (let shiftY
of [-1, 1]) {
401 if (y
+ shiftY
>= 0 && y
+ shiftY
< V
.size
.y
) {
403 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
404 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
406 this.addPawnMoves([x
, y
], [x
+ shiftX
, y
+ shiftY
], moves
);
410 Array
.prototype.push
.apply(
412 this.getEnpassantCaptures([x
, y
], shiftX
)
417 getPotentialMarshalMoves(sq
) {
418 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.ROOK
]).concat(
419 this.getSlideNJumpMoves(sq
, V
.steps
[V
.KNIGHT
], "oneStep")
423 getPotentialCardinalMoves(sq
) {
424 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.BISHOP
]).concat(
425 this.getSlideNJumpMoves(sq
, V
.steps
[V
.KNIGHT
], "oneStep")
429 getPotentialScepterMoves(sq
) {
431 V
.steps
[V
.KNIGHT
].concat(V
.steps
[V
.BISHOP
]).concat(V
.steps
[V
.ROOK
]);
432 return this.getSlideNJumpMoves(sq
, steps
, "oneStep");
435 getPotentialHorseMoves(sq
) {
436 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.BISHOP
]).concat(
437 this.getSlideNJumpMoves(sq
, V
.steps
[V
.ROOK
], "oneStep"));
440 getPotentialDragonMoves(sq
) {
441 return this.getSlideNJumpMoves(sq
, V
.steps
[V
.ROOK
]).concat(
442 this.getSlideNJumpMoves(sq
, V
.steps
[V
.BISHOP
], "oneStep"));
445 getEnpassantCaptures([x
, y
], shiftX
) {
446 const Lep
= this.epSquares
.length
;
447 const epSquare
= this.epSquares
[Lep
- 1];
450 for (let epsq
of epSquare
) {
451 // TODO: some redundant checks
452 if (epsq
.x
== x
+ shiftX
&& Math
.abs(epsq
.y
- y
) == 1) {
453 let enpassantMove
= this.getBasicMove([x
, y
], [epsq
.x
, epsq
.y
]);
454 // WARNING: the captured pawn may be diagonally behind us,
455 // if it's a 3-squares jump and we take on 1st passing square
456 const px
= this.board
[x
][epsq
.y
] != V
.EMPTY
? x : x
- shiftX
;
457 enpassantMove
.vanish
.push({
461 c: this.getColor(px
, epsq
.y
)
463 moves
.push(enpassantMove
);
470 getPotentialKingMoves(sq
) {
471 // Initialize with normal moves
472 let moves
= this.getSlideNJumpMoves(
474 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
479 this.castleFlags
[c
][0] < V
.size
.y
||
480 this.castleFlags
[c
][1] < V
.size
.y
482 moves
= moves
.concat(this.getCastleMoves(sq
));
487 getCastleMoves([x
, y
]) {
488 const c
= this.getColor(x
, y
);
490 ((c
== 'w' && x
== 9) || (c
== 'b' && x
== 0)) &&
491 y
== this.castleFlags
[c
][2]
493 const finalSquares
= [
497 return super.getCastleMoves([x
, y
], finalSquares
, false, [V
.ROOK
]);
502 isAttacked(sq
, color
) {
504 this.isAttackedByPawn(sq
, color
) ||
505 this.isAttackedByRook(sq
, color
) ||
506 this.isAttackedByKnight(sq
, color
) ||
507 this.isAttackedByBishop(sq
, color
) ||
508 this.isAttackedByKing(sq
, color
) ||
509 this.isAttackedByQueens(sq
, color
) ||
510 this.isAttackedByScepter(sq
, color
) ||
511 this.isAttackedByDragon(sq
, color
) ||
512 this.isAttackedByHorse(sq
, color
) ||
513 this.isAttackedByMarshal(sq
, color
) ||
514 this.isAttackedByCardinal(sq
, color
)
518 isAttackedByQueens([x
, y
], color
) {
519 // pieces: because queen = gilding = whole = apricot
520 const pieces
= [V
.QUEEN
, V
.GILDING
, V
.WHOLE
, V
.APRICOT
];
521 const steps
= V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]);
522 for (let step
of steps
) {
523 let rx
= x
+ step
[0],
525 while (V
.OnBoard(rx
, ry
) && this.board
[rx
][ry
] == V
.EMPTY
) {
531 this.board
[rx
][ry
] != V
.EMPTY
&&
532 pieces
.includes(this.getPiece(rx
, ry
)) &&
533 this.getColor(rx
, ry
) == color
541 isAttackedByScepter(sq
, color
) {
543 V
.steps
[V
.KNIGHT
].concat(V
.steps
[V
.ROOK
]).concat(V
.steps
[V
.BISHOP
]);
545 super.isAttackedBySlideNJump(sq
, color
, steps
, V
.SCEPTER
, "oneStep")
549 isAttackedByHorse(sq
, color
) {
551 super.isAttackedBySlideNJump(sq
, color
, V
.steps
[V
.BISHOP
], V
.HORSE
) ||
552 super.isAttackedBySlideNJump(
553 sq
, color
, V
.steps
[V
.ROOK
], V
.HORSE
, "oneStep")
557 isAttackedByDragon(sq
, color
) {
559 super.isAttackedBySlideNJump(sq
, color
, V
.steps
[V
.ROOK
], V
.DRAGON
) ||
560 super.isAttackedBySlideNJump(
561 sq
, color
, V
.steps
[V
.BISHOP
], V
.DRAGON
, "oneStep")
565 isAttackedByMarshal(sq
, color
) {
567 super.isAttackedBySlideNJump(sq
, color
, V
.MARSHAL
, V
.steps
[V
.ROOK
]) ||
568 super.isAttackedBySlideNJump(
578 isAttackedByCardinal(sq
, color
) {
580 super.isAttackedBySlideNJump(sq
, color
, V
.CARDINAL
, V
.steps
[V
.BISHOP
]) ||
581 super.isAttackedBySlideNJump(
592 let moves
= super.getAllPotentialMoves();
593 if (this.movesCount
>= 2) {
594 const color
= this.turn
;
595 for (let i
= 0; i
< V
.RESERVE_PIECES
.length
; i
++) {
596 moves
= moves
.concat(
597 this.getReserveMoves([V
.size
.x
+ (color
== "w" ? 0 : 1), i
])
601 return this.filterValid(moves
);
604 atLeastOneMove(noReserve
) {
605 if (!super.atLeastOneMove()) {
607 // Search one reserve move
608 for (let i
= 0; i
< V
.RESERVE_PIECES
.length
; i
++) {
609 let moves
= this.filterValid(
610 this.getReserveMoves([V
.size
.x
+ (this.turn
== "w" ? 0 : 1), i
])
612 if (moves
.length
> 0) return true;
620 // Reverse 'PromoteMap'
621 static get P_CORRESPONDANCES() {
631 static MayDecode(piece
) {
632 if (Object
.keys(V
.P_CORRESPONDANCES
).includes(piece
))
633 return V
.P_CORRESPONDANCES
[piece
];
638 move.subTurn
= this.subTurn
; //much easier
639 if (this.movesCount
>= 2 || this.subTurn
== 2 || move.vanish
.length
== 0) {
640 this.turn
= V
.GetOppCol(this.turn
);
644 else this.subTurn
= 2;
645 move.flags
= JSON
.stringify(this.aggregateFlags());
646 this.epSquares
.push(this.getEpSquare(move));
647 V
.PlayOnBoard(this.board
, move);
651 updateCastleFlags(move, piece
) {
652 if (piece
== V
.KING
&& move.appear
.length
== 2) {
653 // Castling (only move which disable flags)
654 this.castleFlags
[move.appear
[0].c
][0] = 10;
655 this.castleFlags
[move.appear
[0].c
][1] = 10;
660 if (move.vanish
.length
== 0 && move.appear
.length
== 0) return;
661 super.postPlay(move);
662 const color
= move.appear
[0].c
;
663 if (move.vanish
.length
== 0)
664 // Drop unpromoted piece:
665 this.reserve
[color
][move.appear
[0].p
]--;
666 else if (move.vanish
.length
== 2 && move.appear
.length
== 1)
667 // May capture a promoted piece:
668 this.reserve
[color
][V
.MayDecode(move.vanish
[1].p
)]++;
672 this.epSquares
.pop();
673 this.disaggregateFlags(JSON
.parse(move.flags
));
674 V
.UndoOnBoard(this.board
, move);
675 if (this.movesCount
>= 2 || this.subTurn
== 1 || move.vanish
.length
== 0) {
676 this.turn
= V
.GetOppCol(this.turn
);
679 this.subTurn
= move.subTurn
;
684 if (move.vanish
.length
== 0 && move.appear
.length
== 0) return;
685 super.postUndo(move);
686 const color
= move.appear
[0].c
;
687 if (move.vanish
.length
== 0)
688 this.reserve
[color
][move.appear
[0].p
]++;
689 else if (move.vanish
.length
== 2 && move.appear
.length
== 1)
690 this.reserve
[color
][V
.MayDecode(move.vanish
[1].p
)]--;
695 oppCol
= V
.GetOppCol(this.turn
);
696 let facingKings
= false;
698 this.kingPos
[c
][0] == this.kingPos
[oppCol
][0] ||
699 this.kingPos
[c
][1] == this.kingPos
[oppCol
][1]
703 this.kingPos
[oppCol
][0] - this.kingPos
[c
][0],
704 this.kingPos
[oppCol
][1] - this.kingPos
[c
][1]
706 if (step
[0] != 0) step
[0] /= Math
.abs(step
[0]);
707 else step
[1] /= Math
.abs(step
[1]);
709 [ this.kingPos
[c
][0] + step
[0], this.kingPos
[c
][1] + step
[1] ];
710 while (x
!= this.kingPos
[oppCol
][0] || y
!= this.kingPos
[oppCol
][1]) {
711 if (this.board
[x
][y
] != V
.EMPTY
) {
719 if (facingKings
) return (c
== "w" ? "1-0" : "0-1");
720 if (!this.atLeastOneMove()) return (c
== "w" ? "0-1" : "1-0");
724 static get VALUES() {
725 return Object
.assign(
729 n: 2.5, //knight is weaker
742 static get SEARCH_DEPTH() {
747 if (this.movesCount
<= 1) {
748 // Special case: swap and pass at random
749 const moves1
= this.getAllValidMoves();
750 const m1
= moves1
[randInt(moves1
.length
)];
752 if (m1
.vanish
.length
== 0) {
756 const moves2
= this.getAllValidMoves();
757 const m2
= moves2
[randInt(moves2
.length
)];
761 return super.getComputerMove();
765 let evaluation
= super.evalPosition();
767 for (let i
= 0; i
< V
.RESERVE_PIECES
.length
; i
++) {
768 const p
= V
.RESERVE_PIECES
[i
];
769 evaluation
+= this.reserve
["w"][p
] * V
.VALUES
[p
];
770 evaluation
-= this.reserve
["b"][p
] * V
.VALUES
[p
];
776 if (move.vanish
.length
== 0) {
777 if (move.appear
.length
== 0) return "pass";
779 (move.appear
[0].p
== V
.PAWN
? "" : move.appear
[0].p
.toUpperCase());
780 return pieceName
+ "@" + V
.CoordsToSquare(move.end
);
782 if (move.appear
.length
== 2) {
783 if (move.appear
[0].p
!= V
.KING
)
784 return V
.CoordsToSquare(move.start
) + "S" + V
.CoordsToSquare(move.end
);
785 return (move.end
.y
< move.start
.y
? "0-0" : "0-0-0");
787 let notation
= super.getNotation(move);
788 if (move.vanish
[0].p
!= V
.PAWN
&& move.appear
[0].p
!= move.vanish
[0].p
)
789 // Add promotion indication:
790 notation
+= "=" + move.appear
[0].p
.toUpperCase();