1 import ChessRules
from "/base_rules.js";
2 import GiveawayRules
from "/variants/Giveaway/class.js";
3 import { ArrayFun
} from "/utils/array.js";
4 import { Random
} from "/utils/alea.js";
5 import PiPo
from "/utils/PiPo.js";
6 import Move
from "/utils/Move.js";
8 export default class ChakartRules
extends ChessRules
{
10 static get Options() {
15 variable: "randomness",
18 {label: "Deterministic", value: 0},
19 {label: "Symmetric random", value: 1},
20 {label: "Asymmetric random", value: 2}
28 get pawnPromotions() {
29 return ['q', 'r', 'n', 'b', 'k'];
48 static get IMMOBILIZE_CODE() {
59 static get IMMOBILIZE_DECODE() {
70 // Fictive color 'a', bomb banana mushroom egg
75 return 'd'; //"Donkey"
80 static get MUSHROOM() {
84 static get EGG_SURPRISE() {
86 "kingboo", "bowser", "daisy", "koopa",
87 "luigi", "waluigi", "toadette", "chomp"];
92 this.playerColor
!= this.turn
||
93 Object
.keys(V
.IMMOBILIZE_DECODE
).includes(this.getPiece(x
, y
))
97 return this.egg
== "kingboo" || this.getColor(x
, y
) == this.turn
;
100 pieces(color
, x
, y
) {
102 'i': {"class": "invisible"}, //queen
103 '?': {"class": "mystery"}, //...initial square
104 'e': {"class": "egg"},
105 'm': {"class": "mushroom"},
106 'd': {"class": "banana"},
107 'w': {"class": "bomb"},
108 'z': {"class": "remote-capture"}
111 's': {"class": ["immobilized", "pawn"]},
112 'u': {"class": ["immobilized", "rook"]},
113 'o': {"class": ["immobilized", "knight"]},
114 'c': {"class": ["immobilized", "bishop"]},
115 't': {"class": ["immobilized", "queen"]},
116 'l': {"class": ["immobilized", "king"]}
118 return Object
.assign(
121 // Virtual piece for "king remote shell captures"
126 [0, 1], [0, -1], [1, 0], [-1, 0],
127 [1, 1], [1, -1], [-1, 1], [-1, -1]
133 specials
, bowsered
, super.pieces(color
, x
, y
));
136 genRandInitFen(seed
) {
137 const options
= Object
.assign({mode: "suicide"}, this.options
);
138 const gr
= new GiveawayRules({options: options
, genFenOnly: true});
139 const baseFen
= gr
.genRandInitFen(seed
);
140 const fenParts
= baseFen
.split(" ");
141 let others
= JSON
.parse(fenParts
[3]);
142 delete others
["enpassant"];
143 others
["flags"] = "1111"; //Peach + Mario flags
144 return fenParts
.slice(0, 3).join(" ") + " " + JSON
.stringify(others
);
150 ? "w" + f
.toLowerCase()
151 : (['w', 'd', 'e', 'm'].includes(f
) ? "a" : "b") + f
156 // King can send shell? Queen can be invisible?
158 w: {k: false, q: false},
159 b: {k: false, q: false}
161 for (let c
of ['w', 'b']) {
162 for (let p
of ['k', 'q']) {
163 this.powerFlags
[c
][p
] =
164 fenflags
.charAt((c
== "w" ? 0 : 2) + (p
== 'k' ? 0 : 1)) == "1";
170 return this.powerFlags
;
173 disaggregateFlags(flags
) {
174 this.powerFlags
= flags
;
178 return ['w', 'b'].map(c
=> {
179 return ['k', 'q'].map(p
=> this.powerFlags
[c
][p
] ? "1" : "0").join("");
183 setOtherVariables(fenParsed
) {
184 super.setOtherVariables(fenParsed
);
186 // Change seed (after FEN generation!!)
187 // so that further calls differ between players:
188 Random
.setSeed(Math
.floor(19840 * Math
.random()));
192 this.reserve
= {}; //to be filled later
195 // For Toadette bonus
196 getDropMovesFrom([c
, p
]) {
197 if (typeof c
!= "string" || this.reserve
[c
][p
] == 0)
200 const start
= (c
== 'w' && p
== 'p' ? 1 : 0);
201 const end
= (c
== 'b' && p
== 'p' ? 7 : 8);
202 for (let i
= start
; i
< end
; i
++) {
203 for (let j
= 0; j
< this.size
.y
; j
++) {
204 const pieceIJ
= this.getPiece(i
, j
);
205 const colIJ
= this.getColor(i
, j
);
206 if (this.board
[i
][j
] == "" || colIJ
== 'a' || pieceIJ
== 'i') {
209 appear: [new PiPo({x: i
, y: j
, c: c
, p: p
})],
212 // A drop move may remove a bonus (or hidden queen!)
213 if (this.board
[i
][j
] != "")
214 m
.vanish
.push(new PiPo({x: i
, y: j
, c: colIJ
, p: pieceIJ
}));
222 getPotentialMovesFrom([x
, y
]) {
224 const piece
= this.getPiece(x
, y
);
225 if (this.egg
== "toadette")
226 moves
= this.getDropMovesFrom([x
, y
]);
227 else if (this.egg
== "kingboo") {
228 const color
= this.turn
;
229 const oppCol
= C
.GetOppCol(color
);
230 // Only allow to swap (non-immobilized!) pieces
231 for (let i
=0; i
<this.size
.x
; i
++) {
232 for (let j
=0; j
<this.size
.y
; j
++) {
233 const colIJ
= this.getColor(i
, j
);
234 const pieceIJ
= this.getPiece(i
, j
);
236 (i
!= x
|| j
!= y
) &&
237 ['w', 'b'].includes(colIJ
) &&
238 !Object
.keys(V
.IMMOBILIZE_DECODE
).includes(pieceIJ
) &&
239 // Next conditions = no pawn on last rank
243 (color
!= 'w' || i
!= 0) &&
244 (color
!= 'b' || i
!= this.size
.x
- 1)
251 (colIJ
!= 'w' || x
!= 0) &&
252 (colIJ
!= 'b' || x
!= this.size
.x
- 1)
256 let m
= this.getBasicMove([x
, y
], [i
, j
]);
257 m
.appear
.push(new PiPo({x: x
, y: y
, p: pieceIJ
, c: colIJ
}));
258 m
.kingboo
= true; //avoid some side effects (bananas/bombs)
265 // Normal case (including bonus daisy)
268 moves
= this.getPawnMovesFrom([x
, y
]); //apply promotions
271 moves
= this.getQueenMovesFrom([x
, y
]);
274 moves
= this.getKingMovesFrom([x
, y
]);
277 moves
= this.getKnightMovesFrom([x
, y
]);
281 // Explicitely listing types to avoid moving immobilized piece
282 moves
= this.getPotentialMovesOf(piece
, [x
, y
]);
291 this.board
[i
][j
] == "" ||
292 ['i', V
.EGG
, V
.MUSHROOM
].includes(this.getPiece(i
, j
))
296 getPawnMovesFrom([x
, y
]) {
297 const color
= this.turn
;
298 const oppCol
= C
.GetOppCol(color
);
299 const shiftX
= (color
== 'w' ? -1 : 1);
300 const firstRank
= (color
== "w" ? this.size
.x
- 1 : 0);
302 const frontPiece
= this.getPiece(x
+ shiftX
, y
);
304 this.board
[x
+ shiftX
][y
] == "" ||
305 this.getColor(x
+ shiftX
, y
) == 'a' ||
308 moves
.push(this.getBasicMove([x
, y
], [x
+ shiftX
, y
]));
310 [firstRank
, firstRank
+ shiftX
].includes(x
) &&
311 ![V
.BANANA
, V
.BOMB
].includes(frontPiece
) &&
313 this.board
[x
+ 2 * shiftX
][y
] == "" ||
314 this.getColor(x
+ 2 * shiftX
, y
) == 'a' ||
315 this.getPiece(x
+ 2 * shiftX
, y
) == 'i'
318 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
321 for (let shiftY
of [-1, 1]) {
322 const nextY
= this.getY(y
+ shiftY
);
325 nextY
< this.size
.y
&&
326 this.board
[x
+ shiftX
][nextY
] != "" &&
327 // Pawns cannot capture invisible queen this way!
328 this.getPiece(x
+ shiftX
, nextY
) != 'i' &&
329 ['a', oppCol
].includes(this.getColor(x
+ shiftX
, nextY
))
331 moves
.push(this.getBasicMove([x
, y
], [x
+ shiftX
, nextY
]));
334 this.pawnPostProcess(moves
, color
, oppCol
);
335 // Add mushroom on before-last square (+ potential segments)
337 let [mx
, my
] = [x
, y
];
338 if (Math
.abs(m
.end
.x
- m
.start
.x
) == 2)
339 mx
= (m
.start
.x
+ m
.end
.x
) / 2;
340 m
.appear
.push(new PiPo({x: mx
, y: my
, c: 'a', p: 'm'}));
341 if (mx
!= x
&& this.board
[mx
][my
] != "") {
342 m
.vanish
.push(new PiPo({
345 c: this.getColor(mx
, my
),
346 p: this.getPiece(mx
, my
)
349 if (Math
.abs(m
.end
.y
- m
.start
.y
) > 1) {
352 [[m
.end
.x
, m
.end
.y
], [m
.end
.x
, m
.end
.y
]]
359 getKnightMovesFrom([x
, y
]) {
360 // Add egg on initial square:
361 return this.getPotentialMovesOf('n', [x
, y
]).map(m
=> {
362 m
.appear
.push(new PiPo({p: "e", c: "a", x: x
, y: y
}));
367 getQueenMovesFrom(sq
) {
368 const normalMoves
= this.getPotentialMovesOf('q', sq
);
369 // If flag allows it, add 'invisible movements'
370 let invisibleMoves
= [];
371 if (this.powerFlags
[this.turn
]['q']) {
372 normalMoves
.forEach(m
=> {
374 m
.appear
.length
== 1 &&
375 m
.vanish
.length
== 1 &&
376 // Only simple non-capturing moves:
379 let im
= JSON
.parse(JSON
.stringify(m
));
380 im
.appear
[0].p
= 'i';
382 invisibleMoves
.push(im
);
386 return normalMoves
.concat(invisibleMoves
);
389 getKingMovesFrom([x
, y
]) {
390 let moves
= this.getPotentialMovesOf('k', [x
, y
]);
391 // If flag allows it, add 'remote shell captures'
392 if (this.powerFlags
[this.turn
]['k']) {
393 let shellCaptures
= this.getPotentialMovesOf('y', [x
, y
]);
394 shellCaptures
.forEach(sc
=> {
395 sc
.shell
= true; //easier play()
396 sc
.choice
= 'z'; //to display in showChoices()
397 // Fix move (Rifle style):
401 Array
.prototype.push
.apply(moves
, shellCaptures
);
407 const color
= this.turn
;
408 const oppCol
= C
.GetOppCol(color
);
410 move.appear
.length
> 0 &&
411 move.appear
[0].p
== 'p' &&
413 (color
== 'w' && move.end
.x
== 0) ||
414 (color
== 'b' && move.end
.x
== this.size
.x
- 1)
417 // "Forgotten" promotion, which occurred after some effect
419 super.pawnPostProcess(moves
, color
, oppCol
);
420 super.showChoices(moves
);
423 this.postPlay(move, color
, oppCol
);
427 postPlay(move, color
, oppCol
) {
429 if (move.egg
== "toadette") {
430 this.reserve
= { w: {}, b: {} };
431 // Randomly select a piece in pawnPromotions
433 move.toadette
= Random
.sample(this.pawnPromotions
);
434 this.reserve
[color
][move.toadette
] = 1;
435 this.re_drawReserve([color
]);
437 else if (Object
.keys(this.reserve
).length
> 0) {
439 this.re_drawReserve([color
]);
442 this.powerFlags
[color
]['k'] = false;
443 else if (move.appear
.length
> 0 && move.appear
[0].p
== 'i') {
444 this.powerFlags
[move.appear
[0].c
]['q'] = false;
445 if (color
== this.playerColor
) {
447 new PiPo({x: move.start
.x
, y: move.start
.y
, c: color
, p: '?'}));
450 if (color
== this.playerColor
) {
451 // Look for an immobilized piece of my color: it can now move
452 for (let i
=0; i
<8; i
++) {
453 for (let j
=0; j
<8; j
++) {
454 if ((i
!= move.end
.x
|| j
!= move.end
.y
) && this.board
[i
][j
] != "") {
455 const piece
= this.getPiece(i
, j
);
457 this.getColor(i
, j
) == color
&&
458 Object
.keys(V
.IMMOBILIZE_DECODE
).includes(piece
)
460 move.vanish
.push(new PiPo({
461 x: i
, y: j
, c: color
, p: piece
463 move.appear
.push(new PiPo({
464 x: i
, y: j
, c: color
, p: V
.IMMOBILIZE_DECODE
[piece
]
470 // Also make opponent invisible queen visible again, if any
471 for (let i
=0; i
<8; i
++) {
472 for (let j
=0; j
<8; j
++) {
474 this.board
[i
][j
] != "" &&
475 this.getColor(i
, j
) == oppCol
477 const pieceIJ
= this.getPiece(i
, j
);
478 if (pieceIJ
== 'i') {
479 move.vanish
.push(new PiPo({x: i
, y: j
, c: oppCol
, p: 'i'}));
480 move.appear
.push(new PiPo({x: i
, y: j
, c: oppCol
, p: 'q'}));
482 else if (pieceIJ
== '?')
483 move.vanish
.push(new PiPo({x: i
, y: j
, c: oppCol
, p: '?'}));
488 this.playOnBoard(move);
489 super.postPlay(move);
492 playVisual(move, r
) {
493 super.playVisual(move, r
);
495 this.displayBonus(move);
498 computeNextMove(move) {
499 // Set potential random effects, so that play() is deterministic
500 // from opponent viewpoint:
501 const endPiece
= this.getPiece(move.end
.x
, move.end
.y
);
504 move.egg
= Random
.sample(V
.EGG_SURPRISE
);
505 move.next
= this.getEggEffect(move);
508 move.next
= this.getMushroomEffect(move);
512 move.next
= this.getBombBananaEffect(move, endPiece
);
515 // NOTE: Chakart has also some side-effects:
517 !move.next
&& move.appear
.length
> 0 &&
518 !move.kingboo
&& !move.luigiEffect
520 const movingPiece
= move.appear
[0].p
;
521 if (['b', 'r'].includes(movingPiece
)) {
522 // Drop a banana or bomb:
524 this.getRandomSquare([move.end
.x
, move.end
.y
],
526 ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
527 : [[1, 0], [-1, 0], [0, 1], [0, -1]],
535 p: movingPiece
== 'r' ? 'd' : 'w'
538 if (this.board
[bs
[0]][bs
[1]] != "") {
543 c: this.getColor(bs
[0], bs
[1]),
544 p: this.getPiece(bs
[0], bs
[1])
554 return !move.next
&& !["daisy", "toadette", "kingboo"].includes(move.egg
);
557 // Helper to set and apply banana/bomb effect
558 getRandomSquare([x
, y
], steps
, freeSquare
) {
559 let validSteps
= steps
.filter(s
=> this.onBoard(x
+ s
[0], y
+ s
[1]));
561 // Square to put banana/bomb cannot be occupied by a piece
562 validSteps
= validSteps
.filter(s
=> {
563 return ["", 'a'].includes(this.getColor(x
+ s
[0], y
+ s
[1]))
566 if (validSteps
.length
== 0)
568 const step
= validSteps
[Random
.randInt(validSteps
.length
)];
569 return [x
+ step
[0], y
+ step
[1]];
573 const getRandomPiece
= (c
) => {
574 let bagOfPieces
= [];
575 for (let i
=0; i
<this.size
.x
; i
++) {
576 for (let j
=0; j
<this.size
.y
; j
++) {
577 if (this.getColor(i
, j
) == c
&& this.getPiece(i
, j
) != 'k')
578 bagOfPieces
.push([i
, j
]);
581 if (bagOfPieces
.length
>= 1)
582 return Random
.sample(bagOfPieces
);
585 const color
= this.turn
;
590 // Change color of friendly or enemy piece, king excepted
591 const oldColor
= (move.egg
== "waluigi" ? color : C
.GetOppCol(color
));
592 const newColor
= C
.GetOppCol(oldColor
);
593 const coords
= getRandomPiece(oldColor
);
595 const piece
= this.getPiece(coords
[0], coords
[1]);
598 new PiPo({x: coords
[0], y: coords
[1], c: newColor
, p: piece
})
601 new PiPo({x: coords
[0], y: coords
[1], c: oldColor
, p: piece
})
604 em
.luigiEffect
= true; //avoid dropping bomb/banana by mistake
614 p: V
.IMMOBILIZE_CODE
[move.appear
[0].p
]
632 x: move.start
.x
, y: move.start
.y
, c: color
, p: move.appear
[0].p
637 x: move.end
.x
, y: move.end
.y
, c: color
, p: move.appear
[0].p
641 if (this.board
[move.start
.x
][move.start
.y
] != "") {
642 // Pawn or knight let something on init square
643 em
.vanish
.push(new PiPo({
647 p: this.getPiece(move.start
.x
, move.start
.y
)
657 x: move.end
.x
, y: move.end
.y
, c: color
, p: move.appear
[0].p
660 end: {x: move.end
.x
, y: move.end
.y
}
664 if (em
&& move.egg
!= "koopa")
665 em
.noAnimate
= true; //static move
669 getMushroomEffect(move) {
671 typeof move.start
.x
== "string" || //drop move (toadette)
672 ['b', 'r', 'q'].includes(move.vanish
[0].p
) //slider
676 let step
= [move.end
.x
- move.start
.x
, move.end
.y
- move.start
.y
];
677 if (Math
.abs(step
[0]) == 2 && Math
.abs(step
[1]) == 0)
678 // Pawn initial 2-squares move: normalize step
680 const nextSquare
= [move.end
.x
+ step
[0], move.end
.y
+ step
[1]];
683 this.onBoard(nextSquare
[0], nextSquare
[1]) &&
685 this.board
[nextSquare
[0]][nextSquare
[1]] == "" ||
686 this.getColor(nextSquare
[0], nextSquare
[1]) == 'a'
689 this.playOnBoard(move); //HACK for getBasicMove()
690 nextMove
= this.getBasicMove([move.end
.x
, move.end
.y
], nextSquare
);
691 this.undoOnBoard(move);
696 getBombBananaEffect(move, item
) {
697 const steps
= item
== V
.BANANA
698 ? [[1, 0], [-1, 0], [0, 1], [0, -1]]
699 : [[1, 1], [1, -1], [-1, 1], [-1, -1]];
700 const nextSquare
= this.getRandomSquare([move.end
.x
, move.end
.y
], steps
);
701 this.playOnBoard(move); //HACK for getBasicMove()
702 const res
= this.getBasicMove([move.end
.x
, move.end
.y
], nextSquare
);
703 this.undoOnBoard(move);
708 let divBonus
= document
.createElement("div");
709 divBonus
.classList
.add("bonus-text");
710 divBonus
.innerHTML
= move.egg
;
711 let container
= document
.getElementById(this.containerId
);
712 container
.appendChild(divBonus
);
713 setTimeout(() => container
.removeChild(divBonus
), 2000);
724 // Kingboo bonus can be animated better:
725 customAnimate(move, segments
, cb
) {
728 super.animateMoving(move.end
, move.start
, null,
729 segments
.reverse().map(s
=> s
.reverse()), cb
);