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