Fix crazyhouse
[xogo.git] / variants / Chakart / Chakart.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { SuicideRules } from "@/variants/Suicide";
3 import { ArrayFun } from "@/utils/array";
4 import { randInt } from "@/utils/alea";
5
6 export class ChakartRules extends ChessRules {
7
8 static get Options() {
9 return {
10 select: [
11 {
12 label: "Randomness",
13 variable: "randomness",
14 defaut: 2,
15 options: [
16 { label: "Deterministic", value: 0 },
17 { label: "Symmetric random", value: 1 },
18 { label: "Asymmetric random", value: 2 }
19 ]
20 }
21 ]
22 };
23 }
24
25 static get PawnSpecs() {
26 return SuicideRules.PawnSpecs;
27 }
28
29 static get HasCastle() {
30 return false;
31 }
32
33 static get HasEnpassant() {
34 return false;
35 }
36
37 static get CorrConfirm() {
38 // Because of bonus effects
39 return false;
40 }
41
42 static get CanAnalyze() {
43 return false;
44 }
45
46 static get SomeHiddenMoves() {
47 return true;
48 }
49
50 static get IMMOBILIZE_CODE() {
51 return {
52 'p': 's',
53 'r': 'u',
54 'n': 'o',
55 'b': 'c',
56 'q': 't',
57 'k': 'l'
58 };
59 }
60
61 static get IMMOBILIZE_DECODE() {
62 return {
63 's': 'p',
64 'u': 'r',
65 'o': 'n',
66 'c': 'b',
67 't': 'q',
68 'l': 'k'
69 };
70 }
71
72 static get INVISIBLE_QUEEN() {
73 return 'i';
74 }
75
76 // Fictive color 'a', bomb banana mushroom egg
77 static get BOMB() {
78 return 'w'; //"Wario"
79 }
80 static get BANANA() {
81 return 'd'; //"Donkey"
82 }
83 static get EGG() {
84 return 'e';
85 }
86 static get MUSHROOM() {
87 return 'm';
88 }
89
90 static fen2board(f) {
91 return (
92 f.charCodeAt() <= 90
93 ? "w" + f.toLowerCase()
94 : (['w', 'd', 'e', 'm'].includes(f) ? "a" : "b") + f
95 );
96 }
97
98 static get PIECES() {
99 return (
100 ChessRules.PIECES.concat(
101 Object.keys(V.IMMOBILIZE_DECODE)).concat(
102 [V.BANANA, V.BOMB, V.EGG, V.MUSHROOM, V.INVISIBLE_QUEEN])
103 );
104 }
105
106 getPpath(b) {
107 let prefix = "";
108 if (
109 b[0] == 'a' ||
110 b[1] == V.INVISIBLE_QUEEN ||
111 Object.keys(V.IMMOBILIZE_DECODE).includes(b[1])
112 ) {
113 prefix = "Chakart/";
114 }
115 return prefix + b;
116 }
117
118 getPPpath(m) {
119 if (!!m.promoteInto) return m.promoteInto;
120 if (m.appear.length == 0 && m.vanish.length == 1)
121 // King 'remote shell capture', on an adjacent square:
122 return this.getPpath(m.vanish[0].c + m.vanish[0].p);
123 let piece = m.appear[0].p;
124 if (Object.keys(V.IMMOBILIZE_DECODE).includes(piece))
125 // Promotion by capture into immobilized piece: do not reveal!
126 piece = V.IMMOBILIZE_DECODE[piece];
127 return this.getPpath(m.appear[0].c + piece);
128 }
129
130 static ParseFen(fen) {
131 const fenParts = fen.split(" ");
132 return Object.assign(
133 ChessRules.ParseFen(fen),
134 { captured: fenParts[4] }
135 );
136 }
137
138 static IsGoodFen(fen) {
139 if (!ChessRules.IsGoodFen(fen)) return false;
140 const captured = V.ParseFen(fen).captured;
141 if (!captured || !captured.match(/^[0-9]{12,12}$/)) return false;
142 return true;
143 }
144
145 // King can be l or L (immobilized) --> similar to Alice variant
146 static IsGoodPosition(position) {
147 if (position.length == 0) return false;
148 const rows = position.split("/");
149 if (rows.length != V.size.x) return false;
150 let kings = { "k": 0, "K": 0, 'l': 0, 'L': 0 };
151 for (let row of rows) {
152 let sumElts = 0;
153 for (let i = 0; i < row.length; i++) {
154 if (['K', 'k', 'L', 'l'].includes(row[i])) kings[row[i]]++;
155 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
156 else {
157 const num = parseInt(row[i], 10);
158 if (isNaN(num)) return false;
159 sumElts += num;
160 }
161 }
162 if (sumElts != V.size.y) return false;
163 }
164 if (kings['k'] + kings['l'] == 0 || kings['K'] + kings['L'] == 0)
165 return false;
166 return true;
167 }
168
169 static IsGoodFlags(flags) {
170 // 4 for Peach + Mario w, b
171 return !!flags.match(/^[01]{4,4}$/);
172 }
173
174 setFlags(fenflags) {
175 // King can send shell? Queen can be invisible?
176 this.powerFlags = {
177 w: { 'k': false, 'q': false },
178 b: { 'k': false, 'q': false }
179 };
180 for (let c of ["w", "b"]) {
181 for (let p of ['k', 'q']) {
182 this.powerFlags[c][p] =
183 fenflags.charAt((c == "w" ? 0 : 2) + (p == 'k' ? 0 : 1)) == "1";
184 }
185 }
186 }
187
188 aggregateFlags() {
189 return this.powerFlags;
190 }
191
192 disaggregateFlags(flags) {
193 this.powerFlags = flags;
194 }
195
196 getFen() {
197 return super.getFen() + " " + this.getCapturedFen();
198 }
199
200 getFenForRepeat() {
201 return super.getFenForRepeat() + "_" + this.getCapturedFen();
202 }
203
204 getCapturedFen() {
205 let counts = [...Array(12).fill(0)];
206 let i = 0;
207 for (let p of V.RESERVE_PIECES) {
208 counts[i] = this.captured["w"][p];
209 counts[6 + i] = this.captured["b"][p];
210 i++;
211 }
212 return counts.join("");
213 }
214
215 scanKings() {}
216
217 setOtherVariables(fen) {
218 super.setOtherVariables(fen);
219 // Initialize captured pieces' counts from FEN
220 const captured =
221 V.ParseFen(fen).captured.split("").map(x => parseInt(x, 10));
222 this.captured = {
223 w: {
224 [V.PAWN]: captured[0],
225 [V.ROOK]: captured[1],
226 [V.KNIGHT]: captured[2],
227 [V.BISHOP]: captured[3],
228 [V.QUEEN]: captured[4],
229 [V.KING]: captured[5]
230 },
231 b: {
232 [V.PAWN]: captured[6],
233 [V.ROOK]: captured[7],
234 [V.KNIGHT]: captured[8],
235 [V.BISHOP]: captured[9],
236 [V.QUEEN]: captured[10],
237 [V.KING]: captured[11]
238 }
239 };
240 this.effects = [];
241 this.subTurn = 1;
242 }
243
244 getFlagsFen() {
245 let fen = "";
246 // Add power flags
247 for (let c of ["w", "b"])
248 for (let p of ['k', 'q']) fen += (this.powerFlags[c][p] ? "1" : "0");
249 return fen;
250 }
251
252 getColor(i, j) {
253 if (i >= V.size.x) return i == V.size.x ? "w" : "b";
254 return this.board[i][j].charAt(0);
255 }
256
257 getPiece(i, j) {
258 if (i >= V.size.x) return V.RESERVE_PIECES[j];
259 return this.board[i][j].charAt(1);
260 }
261
262 getReservePpath(index, color) {
263 return color + V.RESERVE_PIECES[index];
264 }
265
266 static get RESERVE_PIECES() {
267 return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.KING];
268 }
269
270 getReserveMoves([x, y]) {
271 const color = this.turn;
272 const p = V.RESERVE_PIECES[y];
273 if (this.reserve[color][p] == 0) return [];
274 let moves = [];
275 const start = (color == 'w' && p == V.PAWN ? 1 : 0);
276 const end = (color == 'b' && p == V.PAWN ? 7 : 8);
277 for (let i = start; i < end; i++) {
278 for (let j = 0; j < V.size.y; j++) {
279 if (
280 this.board[i][j] == V.EMPTY ||
281 this.getColor(i, j) == 'a' ||
282 this.getPiece(i, j) == V.INVISIBLE_QUEEN
283 ) {
284 let m = this.getBasicMove({ p: p, x: i, y: j});
285 m.start = { x: x, y: y };
286 moves.push(m);
287 }
288 }
289 }
290 return moves;
291 }
292
293 getPotentialMovesFrom([x, y]) {
294 let moves = [];
295 if (this.subTurn == 1) {
296 moves = super.getPotentialMovesFrom([x, y]);
297 const finalPieces = V.PawnSpecs.promotions;
298 const color = this.turn;
299 const lastRank = (color == "w" ? 0 : 7);
300 let pMoves = [];
301 moves.forEach(m => {
302 if (
303 m.appear.length > 0 &&
304 ['p', 's'].includes(m.appear[0].p) &&
305 m.appear[0].x == lastRank
306 ) {
307 for (let i = 1; i < finalPieces.length; i++) {
308 const piece = finalPieces[i];
309 let otherM = JSON.parse(JSON.stringify(m));
310 otherM.appear[0].p =
311 m.appear[0].p == V.PAWN
312 ? finalPieces[i]
313 : V.IMMOBILIZE_CODE[finalPieces[i]];
314 pMoves.push(otherM);
315 }
316 // Finally alter m itself:
317 m.appear[0].p =
318 m.appear[0].p == V.PAWN
319 ? finalPieces[0]
320 : V.IMMOBILIZE_CODE[finalPieces[0]];
321 }
322 });
323 Array.prototype.push.apply(moves, pMoves);
324 }
325 else {
326 // Subturn == 2
327 const L = this.effects.length;
328 switch (this.effects[L-1]) {
329 case "kingboo":
330 // Exchange position with any visible piece,
331 // except pawns if arriving on last rank.
332 const lastRank = { 'w': 0, 'b': 7 };
333 const color = this.turn;
334 const allowLastRank = (this.getPiece(x, y) != V.PAWN);
335 for (let i=0; i<8; i++) {
336 for (let j=0; j<8; j++) {
337 const colIJ = this.getColor(i, j);
338 const pieceIJ = this.getPiece(i, j);
339 if (
340 (i != x || j != y) &&
341 this.board[i][j] != V.EMPTY &&
342 pieceIJ != V.INVISIBLE_QUEEN &&
343 colIJ != 'a'
344 ) {
345 if (
346 (pieceIJ != V.PAWN || x != lastRank[colIJ]) &&
347 (allowLastRank || i != lastRank[color])
348 ) {
349 const movedUnit = new PiPo({
350 x: x,
351 y: y,
352 c: colIJ,
353 p: this.getPiece(i, j)
354 });
355 let mMove = this.getBasicMove({ x: x, y: y }, [i, j]);
356 mMove.appear.push(movedUnit);
357 moves.push(mMove);
358 }
359 }
360 }
361 }
362 break;
363 case "toadette":
364 // Resurrect a captured piece
365 if (x >= V.size.x) moves = this.getReserveMoves([x, y]);
366 break;
367 case "daisy":
368 // Play again with any piece
369 moves = super.getPotentialMovesFrom([x, y]);
370 break;
371 }
372 }
373 return moves;
374 }
375
376 // Helper for getBasicMove(): banana/bomb effect
377 getRandomSquare([x, y], steps) {
378 const validSteps = steps.filter(s => V.OnBoard(x + s[0], y + s[1]));
379 const step = validSteps[randInt(validSteps.length)];
380 return [x + step[0], y + step[1]];
381 }
382
383 // Apply mushroom, bomb or banana effect (hidden to the player).
384 // Determine egg effect, too, and apply its first part if possible.
385 getBasicMove_aux(psq1, sq2, tr, initMove) {
386 const [x1, y1] = [psq1.x, psq1.y];
387 const color1 = this.turn;
388 const piece1 = (!!tr ? tr.p : (psq1.p || this.getPiece(x1, y1)));
389 const oppCol = V.GetOppCol(color1);
390 if (!sq2) {
391 let move = {
392 appear: [],
393 vanish: []
394 };
395 // banana or bomb defines next square, or the move ends there
396 move.appear = [
397 new PiPo({
398 x: x1,
399 y: y1,
400 c: color1,
401 p: piece1
402 })
403 ];
404 if (this.board[x1][y1] != V.EMPTY) {
405 const initP1 = this.getPiece(x1, y1);
406 move.vanish = [
407 new PiPo({
408 x: x1,
409 y: y1,
410 c: this.getColor(x1, y1),
411 p: initP1
412 })
413 ];
414 if ([V.BANANA, V.BOMB].includes(initP1)) {
415 const steps = V.steps[initP1 == V.BANANA ? V.ROOK : V.BISHOP];
416 move.next = this.getRandomSquare([x1, y1], steps);
417 }
418 }
419 move.end = { x: x1, y: y1 };
420 return move;
421 }
422 const [x2, y2] = [sq2[0], sq2[1]];
423 // The move starts normally, on board:
424 let move = super.getBasicMove([x1, y1], [x2, y2], tr);
425 if (!!tr) move.promoteInto = tr.c + tr.p; //in case of (chomped...)
426 const L = this.effects.length;
427 if (
428 [V.PAWN, V.KNIGHT].includes(piece1) &&
429 !!initMove &&
430 (this.subTurn == 1 || this.effects[L-1] == "daisy")
431 ) {
432 switch (piece1) {
433 case V.PAWN: {
434 const twoSquaresMove = (Math.abs(x2 - x1) == 2);
435 const mushroomX = x1 + (twoSquaresMove ? (x2 - x1) / 2 : 0);
436 move.appear.push(
437 new PiPo({
438 x: mushroomX,
439 y: y1,
440 c: 'a',
441 p: V.MUSHROOM
442 })
443 );
444 if (this.getColor(mushroomX, y1) == 'a') {
445 move.vanish.push(
446 new PiPo({
447 x: mushroomX,
448 y: y1,
449 c: 'a',
450 p: this.getPiece(mushroomX, y1)
451 })
452 );
453 }
454 break;
455 }
456 case V.KNIGHT: {
457 const deltaX = Math.abs(x2 - x1);
458 const deltaY = Math.abs(y2 - y1);
459 let eggSquare = [
460 x1 + (deltaX == 2 ? (x2 - x1) / 2 : 0),
461 y1 + (deltaY == 2 ? (y2 - y1) / 2 : 0)
462 ];
463 if (
464 this.board[eggSquare[0]][eggSquare[1]] != V.EMPTY &&
465 this.getColor(eggSquare[0], eggSquare[1]) != 'a'
466 ) {
467 eggSquare[0] = x1;
468 eggSquare[1] = y1;
469 }
470 move.appear.push(
471 new PiPo({
472 x: eggSquare[0],
473 y: eggSquare[1],
474 c: 'a',
475 p: V.EGG
476 })
477 );
478 if (this.getColor(eggSquare[0], eggSquare[1]) == 'a') {
479 move.vanish.push(
480 new PiPo({
481 x: eggSquare[0],
482 y: eggSquare[1],
483 c: 'a',
484 p: this.getPiece(eggSquare[0], eggSquare[1])
485 })
486 );
487 }
488 break;
489 }
490 }
491 }
492 // For (wa)luigi effect:
493 const changePieceColor = (color) => {
494 let pieces = [];
495 const oppLastRank = (color == 'w' ? 7 : 0);
496 for (let i=0; i<8; i++) {
497 for (let j=0; j<8; j++) {
498 const piece = this.getPiece(i, j);
499 if (
500 (i != move.vanish[0].x || j != move.vanish[0].y) &&
501 this.board[i][j] != V.EMPTY &&
502 piece != V.INVISIBLE_QUEEN &&
503 this.getColor(i, j) == color
504 ) {
505 if (piece != V.KING && (piece != V.PAWN || i != oppLastRank))
506 pieces.push({ x: i, y: j, p: piece });
507 }
508 }
509 }
510 // Special case of the current piece (still at its initial position)
511 if (color == color1)
512 pieces.push({ x: move.appear[0].x, y: move.appear[0].y, p: piece1 });
513 const cp = pieces[randInt(pieces.length)];
514 if (move.appear[0].x != cp.x || move.appear[0].y != cp.y) {
515 move.vanish.push(
516 new PiPo({
517 x: cp.x,
518 y: cp.y,
519 c: color,
520 p: cp.p
521 })
522 );
523 }
524 else move.appear.shift();
525 move.appear.push(
526 new PiPo({
527 x: cp.x,
528 y: cp.y,
529 c: V.GetOppCol(color),
530 p: cp.p
531 })
532 );
533 };
534 const applyEggEffect = () => {
535 if (this.subTurn == 2)
536 // No egg effects at subTurn 2
537 return;
538 // 1) Determine the effect (some may be impossible)
539 let effects = ["kingboo", "koopa", "chomp", "bowser", "daisy"];
540 if (Object.values(this.captured[color1]).some(c => c >= 1))
541 effects.push("toadette");
542 const lastRank = { 'w': 0, 'b': 7 };
543 if (
544 this.board.some((b,i) =>
545 b.some(cell => {
546 return (
547 cell[0] == oppCol &&
548 cell[1] != V.KING &&
549 (cell[1] != V.PAWN || i != lastRank[color1])
550 );
551 })
552 )
553 ) {
554 effects.push("luigi");
555 }
556 if (
557 (
558 piece1 != V.KING &&
559 (piece1 != V.PAWN || move.appear[0].x != lastRank[oppCol])
560 ) ||
561 this.board.some((b,i) =>
562 b.some(cell => {
563 return (
564 cell[0] == color1 &&
565 cell[1] != V.KING &&
566 (cell[1] != V.PAWN || i != lastRank[oppCol])
567 );
568 })
569 )
570 ) {
571 effects.push("waluigi");
572 }
573 const effect = effects[randInt(effects.length)];
574 move.end.effect = effect;
575 // 2) Apply it if possible
576 if (!(["kingboo", "toadette", "daisy"].includes(effect))) {
577 switch (effect) {
578 case "koopa":
579 move.appear = [];
580 // Maybe egg effect was applied after others,
581 // so just shift vanish array:
582 move.vanish.shift();
583 break;
584 case "chomp":
585 move.appear = [];
586 break;
587 case "bowser":
588 move.appear[0].p = V.IMMOBILIZE_CODE[piece1];
589 break;
590 case "luigi":
591 changePieceColor(oppCol);
592 break;
593 case "waluigi":
594 changePieceColor(color1);
595 break;
596 }
597 }
598 };
599 const applyMushroomEffect = () => {
600 if ([V.PAWN, V.KING, V.KNIGHT].includes(piece1)) {
601 // Just make another similar step, if possible (non-capturing)
602 const [i, j] = [
603 move.appear[0].x + (x2 - x1),
604 move.appear[0].y + (y2 - y1)
605 ];
606 if (
607 V.OnBoard(i, j) &&
608 (
609 this.board[i][j] == V.EMPTY ||
610 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
611 this.getColor(i, j) == 'a'
612 )
613 ) {
614 move.appear[0].x = i;
615 move.appear[0].y = j;
616 if (this.board[i][j] != V.EMPTY) {
617 const object = this.getPiece(i, j);
618 const color = this.getColor(i, j);
619 move.vanish.push(
620 new PiPo({
621 x: i,
622 y: j,
623 c: color,
624 p: object
625 })
626 );
627 switch (object) {
628 case V.BANANA:
629 case V.BOMB:
630 const steps = V.steps[object == V.BANANA ? V.ROOK : V.BISHOP];
631 move.next = this.getRandomSquare([i, j], steps);
632 break;
633 case V.EGG:
634 applyEggEffect();
635 break;
636 case V.MUSHROOM:
637 applyMushroomEffect();
638 break;
639 }
640 }
641 }
642 }
643 else {
644 // Queen, bishop or rook:
645 const step = [
646 (x2 - x1) / Math.abs(x2 - x1) || 0,
647 (y2 - y1) / Math.abs(y2 - y1) || 0
648 ];
649 const next = [move.appear[0].x + step[0], move.appear[0].y + step[1]];
650 if (
651 V.OnBoard(next[0], next[1]) &&
652 this.board[next[0]][next[1]] != V.EMPTY &&
653 this.getPiece(next[0], next[1]) != V.INVISIBLE_QUEEN &&
654 this.getColor(next[0], next[1]) != 'a'
655 ) {
656 const afterNext = [next[0] + step[0], next[1] + step[1]];
657 if (V.OnBoard(afterNext[0], afterNext[1])) {
658 const afterColor = this.getColor(afterNext[0], afterNext[1]);
659 if (
660 this.board[afterNext[0]][afterNext[1]] == V.EMPTY ||
661 afterColor == 'a'
662 ) {
663 move.appear[0].x = afterNext[0];
664 move.appear[0].y = afterNext[1];
665 if (this.board[afterNext[0]][afterNext[1]] != V.EMPTY) {
666 // object = banana, bomb, mushroom or egg
667 const object = this.getPiece(afterNext[0], afterNext[1]);
668 move.vanish.push(
669 new PiPo({
670 x: afterNext[0],
671 y: afterNext[1],
672 c: afterColor,
673 p: object
674 })
675 );
676 switch (object) {
677 case V.BANANA:
678 case V.BOMB:
679 const steps =
680 V.steps[object == V.BANANA ? V.ROOK : V.BISHOP];
681 move.next = this.getRandomSquare(
682 [afterNext[0], afterNext[1]], steps);
683 break;
684 case V.EGG:
685 applyEggEffect();
686 break;
687 case V.MUSHROOM:
688 applyMushroomEffect();
689 break;
690 }
691 }
692 }
693 }
694 }
695 }
696 };
697 const color2 = this.getColor(x2, y2);
698 const piece2 = this.getPiece(x2, y2);
699 if (color2 == 'a') {
700 switch (piece2) {
701 case V.BANANA:
702 case V.BOMB:
703 const steps = V.steps[piece2 == V.BANANA ? V.ROOK : V.BISHOP];
704 move.next = this.getRandomSquare([x2, y2], steps);
705 break;
706 case V.MUSHROOM:
707 applyMushroomEffect();
708 break;
709 case V.EGG:
710 if (this.subTurn == 1)
711 // No egg effect at subTurn 2
712 applyEggEffect();
713 break;
714 }
715 }
716 if (
717 this.subTurn == 1 &&
718 !move.next &&
719 move.appear.length > 0 &&
720 [V.ROOK, V.BISHOP].includes(piece1)
721 ) {
722 const finalSquare = [move.appear[0].x, move.appear[0].y];
723 if (
724 color2 != 'a' ||
725 this.getColor(finalSquare[0], finalSquare[1]) != 'a' ||
726 this.getPiece(finalSquare[0], finalSquare[1]) != V.EGG
727 ) {
728 const validSteps =
729 V.steps[piece1 == V.ROOK ? V.BISHOP : V.ROOK].filter(s => {
730 const [i, j] = [finalSquare[0] + s[0], finalSquare[1] + s[1]];
731 return (
732 V.OnBoard(i, j) &&
733 // NOTE: do not place a bomb or banana on the invisible queen!
734 (this.board[i][j] == V.EMPTY || this.getColor(i, j) == 'a')
735 );
736 });
737 if (validSteps.length >= 1) {
738 const randIdx = randInt(validSteps.length);
739 const [x, y] = [
740 finalSquare[0] + validSteps[randIdx][0],
741 finalSquare[1] + validSteps[randIdx][1]
742 ];
743 move.appear.push(
744 new PiPo({
745 x: x,
746 y: y,
747 c: 'a',
748 p: (piece1 == V.ROOK ? V.BANANA : V.BOMB)
749 })
750 );
751 if (this.board[x][y] != V.EMPTY) {
752 move.vanish.push(
753 new PiPo({ x: x, y: y, c: 'a', p: this.getPiece(x, y) }));
754 }
755 }
756 }
757 }
758 return move;
759 }
760
761 getBasicMove(psq1, sq2, tr) {
762 let moves = [];
763 if (Array.isArray(psq1)) psq1 = { x: psq1[0], y: psq1[1] };
764 let m = this.getBasicMove_aux(psq1, sq2, tr, "initMove");
765 while (!!m.next) {
766 // Last move ended on bomb or banana, direction change
767 V.PlayOnBoard(this.board, m);
768 moves.push(m);
769 m = this.getBasicMove_aux(
770 { x: m.appear[0].x, y: m.appear[0].y }, m.next);
771 }
772 for (let i=moves.length-1; i>=0; i--) V.UndoOnBoard(this.board, moves[i]);
773 moves.push(m);
774 // Now merge moves into one
775 let move = {};
776 // start is wrong for Toadette moves --> it's fixed later
777 move.start = { x: psq1.x, y: psq1.y };
778 move.end = !!sq2 ? { x: sq2[0], y: sq2[1] } : { x: psq1.x, y: psq1.y };
779 if (!!tr) move.promoteInto = moves[0].promoteInto;
780 let lm = moves[moves.length-1];
781 if (this.subTurn == 1 && !!lm.end.effect)
782 move.end.effect = lm.end.effect;
783 if (moves.length == 1) {
784 move.appear = moves[0].appear;
785 move.vanish = moves[0].vanish;
786 }
787 else {
788 // Keep first vanish and last appear (if any)
789 move.appear = lm.appear;
790 move.vanish = moves[0].vanish;
791 if (
792 move.vanish.length >= 1 &&
793 move.appear.length >= 1 &&
794 move.vanish[0].x == move.appear[0].x &&
795 move.vanish[0].y == move.appear[0].y
796 ) {
797 // Loopback on initial square:
798 move.vanish.shift();
799 move.appear.shift();
800 }
801 for (let i=1; i < moves.length - 1; i++) {
802 for (let v of moves[i].vanish) {
803 // Only vanishing objects, not appearing at init move
804 if (
805 v.c == 'a' &&
806 (
807 moves[0].appear.length == 1 ||
808 moves[0].appear[1].x != v.x ||
809 moves[0].appear[1].y != v.y
810 )
811 ) {
812 move.vanish.push(v);
813 }
814 }
815 }
816 // Final vanish is our piece, but others might be relevant
817 // (for some egg bonuses at least).
818 for (let i=1; i < lm.vanish.length; i++) {
819 if (
820 lm.vanish[i].c != 'a' ||
821 moves[0].appear.length == 1 ||
822 moves[0].appear[1].x != lm.vanish[i].x ||
823 moves[0].appear[1].y != lm.vanish[i].y
824 ) {
825 move.vanish.push(lm.vanish[i]);
826 }
827 }
828 }
829 return move;
830 }
831
832 getPotentialPawnMoves([x, y]) {
833 const color = this.turn;
834 const oppCol = V.GetOppCol(color);
835 const [sizeX, sizeY] = [V.size.x, V.size.y];
836 const shiftX = V.PawnSpecs.directions[color];
837 const firstRank = (color == "w" ? sizeX - 1 : 0);
838 let moves = [];
839 if (
840 this.board[x + shiftX][y] == V.EMPTY ||
841 this.getColor(x + shiftX, y) == 'a' ||
842 this.getPiece(x + shiftX, y) == V.INVISIBLE_QUEEN
843 ) {
844 this.addPawnMoves([x, y], [x + shiftX, y], moves);
845 if (
846 [firstRank, firstRank + shiftX].includes(x) &&
847 (
848 this.board[x + 2 * shiftX][y] == V.EMPTY ||
849 this.getColor(x + 2 * shiftX, y) == 'a' ||
850 this.getPiece(x + 2 * shiftX, y) == V.INVISIBLE_QUEEN
851 )
852 ) {
853 moves.push(this.getBasicMove({ x: x, y: y }, [x + 2 * shiftX, y]));
854 }
855 }
856 for (let shiftY of [-1, 1]) {
857 if (
858 y + shiftY >= 0 &&
859 y + shiftY < sizeY &&
860 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
861 // Pawns cannot capture invisible queen this way!
862 this.getPiece(x + shiftX, y + shiftY) != V.INVISIBLE_QUEEN &&
863 ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
864 ) {
865 this.addPawnMoves([x, y], [x + shiftX, y + shiftY], moves);
866 }
867 }
868 return moves;
869 }
870
871 getPotentialQueenMoves(sq) {
872 const normalMoves = super.getPotentialQueenMoves(sq);
873 // If flag allows it, add 'invisible movements'
874 let invisibleMoves = [];
875 if (this.powerFlags[this.turn][V.QUEEN]) {
876 normalMoves.forEach(m => {
877 if (
878 m.appear.length == 1 &&
879 m.vanish.length == 1 &&
880 // Only simple non-capturing moves:
881 m.vanish[0].c != 'a'
882 ) {
883 let im = JSON.parse(JSON.stringify(m));
884 im.appear[0].p = V.INVISIBLE_QUEEN;
885 im.end.noHighlight = true;
886 invisibleMoves.push(im);
887 }
888 });
889 }
890 return normalMoves.concat(invisibleMoves);
891 }
892
893 getPotentialKingMoves([x, y]) {
894 let moves = super.getPotentialKingMoves([x, y]);
895 const color = this.turn;
896 // If flag allows it, add 'remote shell captures'
897 if (this.powerFlags[this.turn][V.KING]) {
898 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
899 let [i, j] = [x + step[0], y + step[1]];
900 while (
901 V.OnBoard(i, j) &&
902 (
903 this.board[i][j] == V.EMPTY ||
904 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
905 (
906 this.getColor(i, j) == 'a' &&
907 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
908 )
909 )
910 ) {
911 i += step[0];
912 j += step[1];
913 }
914 if (V.OnBoard(i, j)) {
915 const colIJ = this.getColor(i, j);
916 if (colIJ != color) {
917 // May just destroy a bomb or banana:
918 moves.push(
919 new Move({
920 start: { x: x, y: y},
921 end: { x: i, y: j },
922 appear: [],
923 vanish: [
924 new PiPo({
925 x: i, y: j, c: colIJ, p: this.getPiece(i, j)
926 })
927 ]
928 })
929 );
930 }
931 }
932 });
933 }
934 return moves;
935 }
936
937 getSlideNJumpMoves([x, y], steps, oneStep) {
938 let moves = [];
939 outerLoop: for (let step of steps) {
940 let i = x + step[0];
941 let j = y + step[1];
942 while (
943 V.OnBoard(i, j) &&
944 (
945 this.board[i][j] == V.EMPTY ||
946 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
947 (
948 this.getColor(i, j) == 'a' &&
949 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
950 )
951 )
952 ) {
953 moves.push(this.getBasicMove({ x: x, y: y }, [i, j]));
954 if (oneStep) continue outerLoop;
955 i += step[0];
956 j += step[1];
957 }
958 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
959 moves.push(this.getBasicMove({ x: x, y: y }, [i, j]));
960 }
961 return moves;
962 }
963
964 getAllPotentialMoves() {
965 if (this.subTurn == 1) return super.getAllPotentialMoves();
966 let moves = [];
967 const color = this.turn;
968 const L = this.effects.length;
969 switch (this.effects[L-1]) {
970 case "kingboo": {
971 let allPieces = [];
972 for (let i=0; i<8; i++) {
973 for (let j=0; j<8; j++) {
974 const colIJ = this.getColor(i, j);
975 const pieceIJ = this.getPiece(i, j);
976 if (
977 i != x && j != y &&
978 this.board[i][j] != V.EMPTY &&
979 colIJ != 'a' &&
980 pieceIJ != V.INVISIBLE_QUEEN
981 ) {
982 allPieces.push({ x: i, y: j, c: colIJ, p: pieceIJ });
983 }
984 }
985 }
986 for (let x=0; x<8; x++) {
987 for (let y=0; y<8; y++) {
988 if (this.getColor(i, j) == color) {
989 // Add exchange with something
990 allPieces.forEach(pp => {
991 if (pp.x != i || pp.y != j) {
992 const movedUnit = new PiPo({
993 x: x,
994 y: y,
995 c: pp.c,
996 p: pp.p
997 });
998 let mMove = this.getBasicMove({ x: x, y: y }, [pp.x, pp.y]);
999 mMove.appear.push(movedUnit);
1000 moves.push(mMove);
1001 }
1002 });
1003 }
1004 }
1005 }
1006 break;
1007 }
1008 case "toadette": {
1009 const x = V.size.x + (this.turn == 'w' ? 0 : 1);
1010 for (let y = 0; y < 8; y++)
1011 Array.prototype.push.apply(moves, this.getReserveMoves([x, y]));
1012 break;
1013 }
1014 case "daisy":
1015 moves = super.getAllPotentialMoves();
1016 break;
1017 }
1018 return moves;
1019 }
1020
1021 play(move) {
1022 // if (!this.states) this.states = [];
1023 // const stateFen = this.getFen();
1024 // this.states.push(stateFen);
1025
1026 move.flags = JSON.stringify(this.aggregateFlags());
1027 V.PlayOnBoard(this.board, move);
1028 move.turn = [this.turn, this.subTurn];
1029 if (["kingboo", "toadette", "daisy"].includes(move.end.effect)) {
1030 this.effects.push(move.end.effect);
1031 this.subTurn = 2;
1032 }
1033 else {
1034 this.turn = V.GetOppCol(this.turn);
1035 this.movesCount++;
1036 this.subTurn = 1;
1037 }
1038 this.postPlay(move);
1039 }
1040
1041 postPlay(move) {
1042 if (move.end.effect == "toadette") this.reserve = this.captured;
1043 else this.reserve = undefined;
1044 const color = move.turn[0];
1045 if (
1046 move.vanish.length == 2 &&
1047 move.vanish[1].c != 'a' &&
1048 move.appear.length == 1 //avoid king Boo!
1049 ) {
1050 // Capture: update this.captured
1051 let capturedPiece = move.vanish[1].p;
1052 if (capturedPiece == V.INVISIBLE_QUEEN) capturedPiece = V.QUEEN;
1053 else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
1054 capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
1055 this.captured[move.vanish[1].c][capturedPiece]++;
1056 }
1057 else if (move.vanish.length == 0) {
1058 if (move.appear.length == 0 || move.appear[0].c == 'a') return;
1059 // A piece is back on board
1060 this.captured[move.appear[0].c][move.appear[0].p]--;
1061 }
1062 if (move.appear.length == 0) {
1063 // Three cases: king "shell capture", Chomp or Koopa
1064 if (this.getPiece(move.start.x, move.start.y) == V.KING)
1065 // King remote capture:
1066 this.powerFlags[color][V.KING] = false;
1067 else if (move.end.effect == "chomp")
1068 this.captured[color][move.vanish[0].p]++;
1069 }
1070 else if (move.appear[0].p == V.INVISIBLE_QUEEN)
1071 this.powerFlags[move.appear[0].c][V.QUEEN] = false;
1072 if (this.subTurn == 2) return;
1073 if (
1074 move.turn[1] == 1 &&
1075 move.appear.length == 0 ||
1076 !(Object.keys(V.IMMOBILIZE_DECODE).includes(move.appear[0].p))
1077 ) {
1078 // Look for an immobilized piece of my color: it can now move
1079 for (let i=0; i<8; i++) {
1080 for (let j=0; j<8; j++) {
1081 if (this.board[i][j] != V.EMPTY) {
1082 const piece = this.getPiece(i, j);
1083 if (
1084 this.getColor(i, j) == color &&
1085 Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
1086 ) {
1087 this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
1088 move.wasImmobilized = [i, j];
1089 }
1090 }
1091 }
1092 }
1093 }
1094 // Also make opponent invisible queen visible again, if any
1095 const oppCol = V.GetOppCol(color);
1096 for (let i=0; i<8; i++) {
1097 for (let j=0; j<8; j++) {
1098 if (
1099 this.board[i][j] != V.EMPTY &&
1100 this.getColor(i, j) == oppCol &&
1101 this.getPiece(i, j) == V.INVISIBLE_QUEEN
1102 ) {
1103 this.board[i][j] = oppCol + V.QUEEN;
1104 move.wasInvisible = [i, j];
1105 }
1106 }
1107 }
1108 }
1109
1110 undo(move) {
1111 this.disaggregateFlags(JSON.parse(move.flags));
1112 V.UndoOnBoard(this.board, move);
1113 if (["kingboo", "toadette", "daisy"].includes(move.end.effect))
1114 this.effects.pop();
1115 else this.movesCount--;
1116 this.turn = move.turn[0];
1117 this.subTurn = move.turn[1];
1118 this.postUndo(move);
1119
1120 // const stateFen = this.getFen();
1121 // if (stateFen != this.states[this.states.length-1]) debugger;
1122 // this.states.pop();
1123 }
1124
1125 postUndo(move) {
1126 if (!!move.wasImmobilized) {
1127 const [i, j] = move.wasImmobilized;
1128 this.board[i][j] =
1129 this.getColor(i, j) + V.IMMOBILIZE_CODE[this.getPiece(i, j)];
1130 }
1131 if (!!move.wasInvisible) {
1132 const [i, j] = move.wasInvisible;
1133 this.board[i][j] = this.getColor(i, j) + V.INVISIBLE_QUEEN;
1134 }
1135 if (move.vanish.length == 2 && move.vanish[1].c != 'a') {
1136 let capturedPiece = move.vanish[1].p;
1137 if (capturedPiece == V.INVISIBLE_QUEEN) capturedPiece = V.QUEEN;
1138 else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
1139 capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
1140 this.captured[move.vanish[1].c][capturedPiece]--;
1141 }
1142 else if (move.vanish.length == 0) {
1143 if (move.appear.length == 0 || move.appear[0].c == 'a') return;
1144 // A piece was back on board
1145 this.captured[move.appear[0].c][move.appear[0].p]++;
1146 }
1147 else if (move.appear.length == 0 && move.end.effect == "chomp")
1148 this.captured[move.vanish[0].c][move.vanish[0].p]--;
1149 if (move.vanish.length == 0) this.reserve = this.captured;
1150 else this.reserve = undefined;
1151 }
1152
1153 getCheckSquares() {
1154 return [];
1155 }
1156
1157 getCurrentScore() {
1158 // Find kings (not tracked in this variant)
1159 let kingThere = { w: false, b: false };
1160 for (let i=0; i<8; i++) {
1161 for (let j=0; j<8; j++) {
1162 if (
1163 this.board[i][j] != V.EMPTY &&
1164 ['k', 'l'].includes(this.getPiece(i, j))
1165 ) {
1166 kingThere[this.getColor(i, j)] = true;
1167 }
1168 }
1169 }
1170 if (!kingThere['w']) return "0-1";
1171 if (!kingThere['b']) return "1-0";
1172 if (!this.atLeastOneMove()) return (this.turn == 'w' ? "0-1" : "1-0");
1173 return "*";
1174 }
1175
1176 static GenRandInitFen(options) {
1177 return (
1178 SuicideRules.GenRandInitFen(options).slice(0, -1) +
1179 // Add Peach + Mario flags + capture counts
1180 "1111 000000000000"
1181 );
1182 }
1183
1184 filterValid(moves) {
1185 return moves;
1186 }
1187
1188 static get VALUES() {
1189 return Object.assign(
1190 {},
1191 ChessRules.VALUES,
1192 {
1193 s: 1,
1194 u: 5,
1195 o: 3,
1196 c: 3,
1197 t: 9,
1198 l: 1000,
1199 e: 0,
1200 d: 0,
1201 w: 0,
1202 m: 0
1203 }
1204 );
1205 }
1206
1207 static get SEARCH_DEPTH() {
1208 return 1;
1209 }
1210
1211 getComputerMove() {
1212 const moves = this.getAllValidMoves();
1213 // Split into "normal" and "random" moves:
1214 // (Next splitting condition is OK because cannot take self object
1215 // without a banana or bomb on the way).
1216 const deterministicMoves = moves.filter(m => {
1217 return m.vanish.every(a => a.c != 'a' || a.p == V.MUSHROOM);
1218 });
1219 const randomMoves = moves.filter(m => {
1220 return m.vanish.some(a => a.c == 'a' && a.p != V.MUSHROOM);
1221 });
1222 if (Math.random() < deterministicMoves.length / randomMoves.length)
1223 // Play a deterministic one: capture king or material if possible
1224 return super.getComputerMove(deterministicMoves);
1225 // Play a random effect move, at random:
1226 let move1 = randomMoves[randInt(randomMoves.length)];
1227 this.play(move1);
1228 let move2 = undefined;
1229 if (this.subTurn == 2) {
1230 const moves2 = this.getAllValidMoves();
1231 move2 = moves2[randInt(moves2.length)];
1232 }
1233 this.undo(move1);
1234 if (!move2) return move1;
1235 return [move1, move2];
1236 }
1237
1238 getNotation(move) {
1239 if (move.vanish.length == 0 && move.appear.length == 0) return "-";
1240 if (
1241 !move.end.effect &&
1242 move.appear.length > 0 &&
1243 move.appear[0].p == V.INVISIBLE_QUEEN
1244 ) {
1245 return "Q??";
1246 }
1247 const finalSquare = V.CoordsToSquare(move.end);
1248 // Next condition also includes Toadette placements:
1249 if (move.appear.length > 0 && move.vanish.every(a => a.c == 'a')) {
1250 const piece =
1251 move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
1252 return piece + "@" + finalSquare;
1253 }
1254 else if (move.appear.length == 0) {
1255 const piece = this.getPiece(move.start.x, move.start.y);
1256 if (piece == V.KING && !move.end.effect)
1257 // King remote capture
1258 return "Kx" + finalSquare;
1259 // Koopa or Chomp, or loopback after bananas, bombs & mushrooms:
1260 return (
1261 piece.toUpperCase() + "x" + finalSquare +
1262 (
1263 !!move.end.effect
1264 ? "*" + (move.end.effect == "koopa" ? "K" : "C")
1265 : ""
1266 )
1267 );
1268 }
1269 if (move.appear.length == 1 && move.vanish.length == 1) {
1270 const moveStart = move.appear[0].p.toUpperCase() + "@";
1271 if (move.appear[0].c == 'a' && move.vanish[0].c == 'a')
1272 // Bonus replacement:
1273 return moveStart + finalSquare;
1274 if (
1275 move.vanish[0].p == V.INVISIBLE_QUEEN &&
1276 move.appear[0].x == move.vanish[0].x &&
1277 move.appear[0].y == move.vanish[0].y
1278 ) {
1279 // Toadette takes invisible queen
1280 return moveStart + "Q" + finalSquare;
1281 }
1282 }
1283 if (
1284 move.appear.length == 2 &&
1285 move.vanish.length == 2 &&
1286 move.appear.every(a => a.c != 'a') &&
1287 move.vanish.every(v => v.c != 'a')
1288 ) {
1289 // King Boo exchange
1290 return V.CoordsToSquare(move.start) + finalSquare;
1291 }
1292 const piece = move.vanish[0].p;
1293 let notation = undefined;
1294 if (piece == V.PAWN) {
1295 // Pawn move
1296 if (this.board[move.end.x][move.end.y] != V.EMPTY) {
1297 // Capture
1298 const startColumn = V.CoordToColumn(move.start.y);
1299 notation = startColumn + "x" + finalSquare;
1300 }
1301 else notation = finalSquare;
1302 if (move.appear[0].p != V.PAWN)
1303 // Promotion
1304 notation += "=" + move.appear[0].p.toUpperCase();
1305 }
1306 else {
1307 notation =
1308 piece.toUpperCase() +
1309 (this.board[move.end.x][move.end.y] != V.EMPTY ? "x" : "") +
1310 finalSquare;
1311 }
1312 if (!!move.end.effect) {
1313 switch (move.end.effect) {
1314 case "kingboo":
1315 notation += "*B";
1316 break;
1317 case "toadette":
1318 notation += "*T";
1319 break;
1320 case "daisy":
1321 notation += "*D";
1322 break;
1323 case "bowser":
1324 notation += "*M";
1325 break;
1326 case "luigi":
1327 case "waluigi":
1328 const lastAppear = move.appear[move.appear.length - 1];
1329 const effectOn =
1330 V.CoordsToSquare({ x: lastAppear.x, y : lastAppear.y });
1331 notation += "*" + move.end.effect[0].toUpperCase() + effectOn;
1332 break;
1333 }
1334 }
1335 return notation;
1336 }
1337
1338 };