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