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