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