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