02dd16e9425ce926f542beeef72bcd9621006f80
[xogo.git] / variants / Chakart / class.js
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";
7
8 export default class ChakartRules extends ChessRules {
9
10 static get Options() {
11 return {
12 select: [
13 {
14 label: "Randomness",
15 variable: "randomness",
16 defaut: 2,
17 options: [
18 {label: "Deterministic", value: 0},
19 {label: "Symmetric random", value: 1},
20 {label: "Asymmetric random", value: 2}
21 ]
22 }
23 ],
24 styles: ["cylinder"]
25 };
26 }
27
28 get pawnPromotions() {
29 return ['q', 'r', 'n', 'b', 'k'];
30 }
31
32 get hasCastle() {
33 return false;
34 }
35 get hasEnpassant() {
36 return false;
37 }
38 get hasReserve() {
39 return true;
40 }
41 get hasReserveFen() {
42 return false;
43 }
44
45 static get IMMOBILIZE_CODE() {
46 return {
47 'p': 's',
48 'r': 'u',
49 'n': 'o',
50 'b': 'c',
51 'q': 't',
52 'k': 'l'
53 };
54 }
55
56 static get IMMOBILIZE_DECODE() {
57 return {
58 's': 'p',
59 'u': 'r',
60 'o': 'n',
61 'c': 'b',
62 't': 'q',
63 'l': 'k'
64 };
65 }
66
67 // Fictive color 'a', bomb banana mushroom egg
68 static get BOMB() {
69 return 'w'; //"Wario"
70 }
71 static get BANANA() {
72 return 'd'; //"Donkey"
73 }
74 static get EGG() {
75 return 'e';
76 }
77 static get MUSHROOM() {
78 return 'm';
79 }
80
81 static get EGG_SURPRISE() {
82 return [
83 "kingboo", "bowser", "daisy", "koopa",
84 "luigi", "waluigi", "toadette", "chomp"];
85 }
86
87 canIplay(x, y) {
88 if (
89 this.playerColor != this.turn ||
90 Object.keys(V.IMMOBILIZE_DECODE).includes(this.getPiece(x, y))
91 ) {
92 return false;
93 }
94 return this.egg == "kingboo" || this.getColor(x, y) == this.turn;
95 }
96
97 pieces(color, x, y) {
98 const specials = {
99 'i': {"class": "invisible"}, //queen
100 '?': {"class": "mystery"}, //...initial square
101 'e': {"class": "egg"},
102 'm': {"class": "mushroom"},
103 'd': {"class": "banana"},
104 'w': {"class": "bomb"},
105 'z': {"class": "remote-capture"}
106 };
107 const bowsered = {
108 's': {"class": ["immobilized", "pawn"]},
109 'u': {"class": ["immobilized", "rook"]},
110 'o': {"class": ["immobilized", "knight"]},
111 'c': {"class": ["immobilized", "bishop"]},
112 't': {"class": ["immobilized", "queen"]},
113 'l': {"class": ["immobilized", "king"]}
114 };
115 return Object.assign(
116 {
117 'y': {
118 // Virtual piece for "king remote shell captures"
119 moves: [],
120 attack: [
121 {
122 steps: [
123 [0, 1], [0, -1], [1, 0], [-1, 0],
124 [1, 1], [1, -1], [-1, 1], [-1, -1]
125 ]
126 }
127 ]
128 }
129 },
130 specials, bowsered, super.pieces(color, x, y)
131 );
132 }
133
134 genRandInitFen(seed) {
135 const options = Object.assign({mode: "suicide"}, this.options);
136 const gr = new GiveawayRules({options: options, genFenOnly: true});
137 const baseFen = gr.genRandInitFen(seed);
138 const fenParts = baseFen.split(" ");
139 let others = JSON.parse(fenParts[3]);
140 delete others["enpassant"];
141 others["flags"] = "1111"; //Peach + Mario flags
142 return fenParts.slice(0, 3).join(" ") + " " + JSON.stringify(others);
143 }
144
145 fen2board(f) {
146 return (
147 f.charCodeAt() <= 90
148 ? "w" + f.toLowerCase()
149 : (['w', 'd', 'e', 'm'].includes(f) ? "a" : "b") + f
150 );
151 }
152
153 setFlags(fenflags) {
154 // King can send shell? Queen can be invisible?
155 this.powerFlags = {
156 w: {k: false, q: false},
157 b: {k: false, q: false}
158 };
159 for (let c of ['w', 'b']) {
160 for (let p of ['k', 'q']) {
161 this.powerFlags[c][p] =
162 fenflags.charAt((c == "w" ? 0 : 2) + (p == 'k' ? 0 : 1)) == "1";
163 }
164 }
165 }
166
167 aggregateFlags() {
168 return this.powerFlags;
169 }
170
171 disaggregateFlags(flags) {
172 this.powerFlags = flags;
173 }
174
175 getFlagsFen() {
176 return ['w', 'b'].map(c => {
177 return ['k', 'q'].map(p => this.powerFlags[c][p] ? "1" : "0").join("");
178 }).join("");
179 }
180
181 setOtherVariables(fenParsed) {
182 super.setOtherVariables(fenParsed);
183 this.egg = null;
184 // Change seed (after FEN generation!!)
185 // so that further calls differ between players:
186 Random.setSeed(Math.floor(19840 * Math.random()));
187 }
188
189 initReserves() {
190 this.reserve = {}; //to be filled later
191 }
192
193 canStepOver(i, j) {
194 return (
195 this.board[i][j] == "" ||
196 ['i', V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
197 );
198 }
199
200 // For Toadette bonus
201 canDrop([c, p], [i, j]) {
202 return (
203 (
204 this.board[i][j] == "" ||
205 this.getColor(i, j) == 'a' ||
206 this.getPiece(i, j) == 'i'
207 )
208 &&
209 (p != "p" || (c == 'w' && i < this.size.x - 1) || (c == 'b' && i > 0))
210 );
211 }
212
213 getPotentialMovesFrom([x, y]) {
214 let moves = [];
215 const piece = this.getPiece(x, y);
216 if (this.egg == "toadette")
217 moves = this.getDropMovesFrom([x, y]);
218 else if (this.egg == "kingboo") {
219 const color = this.turn;
220 const oppCol = C.GetOppCol(color);
221 // Only allow to swap (non-immobilized!) pieces
222 for (let i=0; i<this.size.x; i++) {
223 for (let j=0; j<this.size.y; j++) {
224 const colIJ = this.getColor(i, j);
225 const pieceIJ = this.getPiece(i, j);
226 if (
227 (i != x || j != y) &&
228 ['w', 'b'].includes(colIJ) &&
229 !Object.keys(V.IMMOBILIZE_DECODE).includes(pieceIJ) &&
230 // Next conditions = no pawn on last rank
231 (
232 piece != 'p' ||
233 (
234 (color != 'w' || i != 0) &&
235 (color != 'b' || i != this.size.x - 1)
236 )
237 )
238 &&
239 (
240 pieceIJ != 'p' ||
241 (
242 (colIJ != 'w' || x != 0) &&
243 (colIJ != 'b' || x != this.size.x - 1)
244 )
245 )
246 ) {
247 let m = this.getBasicMove([x, y], [i, j]);
248 m.appear.push(new PiPo({x: x, y: y, p: pieceIJ, c: colIJ}));
249 m.kingboo = true; //avoid some side effects (bananas/bombs)
250 moves.push(m);
251 }
252 }
253 }
254 }
255 else {
256 // Normal case (including bonus daisy)
257 switch (piece) {
258 case 'p':
259 moves = this.getPawnMovesFrom([x, y]); //apply promotions
260 break;
261 case 'q':
262 moves = this.getQueenMovesFrom([x, y]);
263 break;
264 case 'k':
265 moves = this.getKingMovesFrom([x, y]);
266 break;
267 case 'n':
268 moves = this.getKnightMovesFrom([x, y]);
269 break;
270 case 'b':
271 case 'r':
272 // Explicitely listing types to avoid moving immobilized piece
273 moves = super.getPotentialMovesOf(piece, [x, y]);
274 break;
275 }
276 }
277 return moves;
278 }
279
280 getPawnMovesFrom([x, y]) {
281 const color = this.turn;
282 const oppCol = C.GetOppCol(color);
283 const shiftX = (color == 'w' ? -1 : 1);
284 const firstRank = (color == "w" ? this.size.x - 1 : 0);
285 let moves = [];
286 const frontPiece = this.getPiece(x + shiftX, y);
287 if (
288 this.board[x + shiftX][y] == "" ||
289 this.getColor(x + shiftX, y) == 'a' ||
290 frontPiece == 'i'
291 ) {
292 moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
293 if (
294 [firstRank, firstRank + shiftX].includes(x) &&
295 ![V.BANANA, V.BOMB].includes(frontPiece) &&
296 (
297 this.board[x + 2 * shiftX][y] == "" ||
298 this.getColor(x + 2 * shiftX, y) == 'a' ||
299 this.getPiece(x + 2 * shiftX, y) == 'i'
300 )
301 ) {
302 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
303 }
304 }
305 for (let shiftY of [-1, 1]) {
306 const nextY = this.getY(y + shiftY);
307 if (
308 nextY >= 0 &&
309 nextY < this.size.y &&
310 this.board[x + shiftX][nextY] != "" &&
311 // Pawns cannot capture invisible queen this way!
312 this.getPiece(x + shiftX, nextY) != 'i' &&
313 ['a', oppCol].includes(this.getColor(x + shiftX, nextY))
314 ) {
315 moves.push(this.getBasicMove([x, y], [x + shiftX, nextY]));
316 }
317 }
318 this.pawnPostProcess(moves, color, oppCol);
319 // Add mushroom on before-last square (+ potential segments)
320 moves.forEach(m => {
321 let [mx, my] = [x, y];
322 if (Math.abs(m.end.x - m.start.x) == 2)
323 mx = (m.start.x + m.end.x) / 2;
324 m.appear.push(new PiPo({x: mx, y: my, c: 'a', p: 'm'}));
325 if (mx != x && this.board[mx][my] != "") {
326 m.vanish.push(new PiPo({
327 x: mx,
328 y: my,
329 c: this.getColor(mx, my),
330 p: this.getPiece(mx, my)
331 }));
332 }
333 if (Math.abs(m.end.y - m.start.y) > 1) {
334 m.segments = [
335 [[x, y], [x, y]],
336 [[m.end.x, m.end.y], [m.end.x, m.end.y]]
337 ];
338 }
339 });
340 return moves;
341 }
342
343 getKnightMovesFrom([x, y]) {
344 // Add egg on initial square:
345 return super.getPotentialMovesOf('n', [x, y]).map(m => {
346 m.appear.push(new PiPo({p: "e", c: "a", x: x, y: y}));
347 return m;
348 });
349 }
350
351 getQueenMovesFrom(sq) {
352 const normalMoves = super.getPotentialMovesOf('q', sq);
353 // If flag allows it, add 'invisible movements'
354 let invisibleMoves = [];
355 if (this.powerFlags[this.turn]['q']) {
356 normalMoves.forEach(m => {
357 if (
358 m.appear.length == 1 &&
359 m.vanish.length == 1 &&
360 // Only simple non-capturing moves:
361 m.vanish[0].c != 'a'
362 ) {
363 let im = JSON.parse(JSON.stringify(m));
364 im.appear[0].p = 'i';
365 im.noAnimate = true;
366 invisibleMoves.push(im);
367 }
368 });
369 }
370 return normalMoves.concat(invisibleMoves);
371 }
372
373 getKingMovesFrom([x, y]) {
374 let moves = super.getPotentialMovesOf('k', [x, y]);
375 // If flag allows it, add 'remote shell captures'
376 if (this.powerFlags[this.turn]['k']) {
377 let shellCaptures = super.getPotentialMovesOf('y', [x, y]);
378 shellCaptures.forEach(sc => {
379 sc.shell = true; //easier play()
380 sc.choice = 'z'; //to display in showChoices()
381 // Fix move (Rifle style):
382 sc.vanish.shift();
383 sc.appear.shift();
384 });
385 Array.prototype.push.apply(moves, shellCaptures);
386 }
387 return moves;
388 }
389
390 play(move) {
391 const color = this.turn;
392 const oppCol = C.GetOppCol(color);
393 if (
394 move.appear.length > 0 &&
395 move.appear[0].p == 'p' &&
396 (
397 (color == 'w' && move.end.x == 0) ||
398 (color == 'b' && move.end.x == this.size.x - 1)
399 )
400 ) {
401 // "Forgotten" promotion, which occurred after some effect
402 let moves = [move];
403 super.pawnPostProcess(moves, color, oppCol);
404 super.showChoices(moves);
405 return false;
406 }
407 this.postPlay(move, color, oppCol);
408 return true;
409 }
410
411 postPlay(move, color, oppCol) {
412 this.egg = move.egg;
413 if (move.egg == "toadette") {
414 this.reserve = { w: {}, b: {} };
415 // Randomly select a piece in pawnPromotions
416 if (!move.toadette)
417 move.toadette = Random.sample(this.pawnPromotions);
418 this.reserve[color][move.toadette] = 1;
419 this.re_drawReserve([color]);
420 }
421 else if (Object.keys(this.reserve).length > 0) {
422 this.reserve = {};
423 this.re_drawReserve([color]);
424 }
425 if (move.shell)
426 this.powerFlags[color]['k'] = false;
427 else if (move.appear.length > 0 && move.appear[0].p == 'i') {
428 this.powerFlags[move.appear[0].c]['q'] = false;
429 if (color == this.playerColor) {
430 move.appear.push(
431 new PiPo({x: move.start.x, y: move.start.y, c: color, p: '?'}));
432 }
433 }
434 if (color == this.playerColor) {
435 // Look for an immobilized piece of my color: it can now move
436 for (let i=0; i<8; i++) {
437 for (let j=0; j<8; j++) {
438 if ((i != move.end.x || j != move.end.y) && this.board[i][j] != "") {
439 const piece = this.getPiece(i, j);
440 if (
441 this.getColor(i, j) == color &&
442 Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
443 ) {
444 move.vanish.push(new PiPo({
445 x: i, y: j, c: color, p: piece
446 }));
447 move.appear.push(new PiPo({
448 x: i, y: j, c: color, p: V.IMMOBILIZE_DECODE[piece]
449 }));
450 }
451 }
452 }
453 }
454 // Also make opponent invisible queen visible again, if any
455 for (let i=0; i<8; i++) {
456 for (let j=0; j<8; j++) {
457 if (
458 this.board[i][j] != "" &&
459 this.getColor(i, j) == oppCol
460 ) {
461 const pieceIJ = this.getPiece(i, j);
462 if (
463 pieceIJ == 'i' &&
464 // Ensure that current move doesn't erase invisible queen
465 move.appear.every(a => a.x != i || a.y != j)
466 ) {
467 move.vanish.push(new PiPo({x: i, y: j, c: oppCol, p: 'i'}));
468 move.appear.push(new PiPo({x: i, y: j, c: oppCol, p: 'q'}));
469 }
470 else if (pieceIJ == '?')
471 move.vanish.push(new PiPo({x: i, y: j, c: oppCol, p: '?'}));
472 }
473 }
474 }
475 }
476 this.playOnBoard(move);
477 super.postPlay(move);
478 }
479
480 playVisual(move, r) {
481 super.playVisual(move, r);
482 if (move.egg)
483 this.displayBonus(move);
484 }
485
486 computeNextMove(move) {
487 // Set potential random effects, so that play() is deterministic
488 // from opponent viewpoint:
489 const endPiece = this.getPiece(move.end.x, move.end.y);
490 switch (endPiece) {
491 case V.EGG:
492 move.egg = Random.sample(V.EGG_SURPRISE);
493 move.next = this.getEggEffect(move);
494 break;
495 case V.MUSHROOM:
496 move.next = this.getMushroomEffect(move);
497 break;
498 case V.BANANA:
499 case V.BOMB:
500 move.next = this.getBombBananaEffect(move, endPiece);
501 break;
502 }
503 // NOTE: Chakart has also some side-effects:
504 if (
505 !move.next && move.appear.length > 0 &&
506 !move.kingboo && !move.luigiEffect
507 ) {
508 const movingPiece = move.appear[0].p;
509 if (['b', 'r'].includes(movingPiece)) {
510 // Drop a banana or bomb:
511 const bs =
512 this.getRandomSquare([move.end.x, move.end.y],
513 movingPiece == 'r'
514 ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
515 : [[1, 0], [-1, 0], [0, 1], [0, -1]],
516 "freeSquare");
517 if (bs) {
518 move.appear.push(
519 new PiPo({
520 x: bs[0],
521 y: bs[1],
522 c: 'a',
523 p: movingPiece == 'r' ? 'd' : 'w'
524 })
525 );
526 if (this.board[bs[0]][bs[1]] != "") {
527 move.vanish.push(
528 new PiPo({
529 x: bs[0],
530 y: bs[1],
531 c: this.getColor(bs[0], bs[1]),
532 p: this.getPiece(bs[0], bs[1])
533 })
534 );
535 }
536 }
537 }
538 }
539 }
540
541 isLastMove(move) {
542 return !move.next && !["daisy", "toadette", "kingboo"].includes(move.egg);
543 }
544
545 // Helper to set and apply banana/bomb effect
546 getRandomSquare([x, y], steps, freeSquare) {
547 let validSteps = steps.filter(s => this.onBoard(x + s[0], y + s[1]));
548 if (freeSquare) {
549 // Square to put banana/bomb cannot be occupied by a piece
550 validSteps = validSteps.filter(s => {
551 return ["", 'a'].includes(this.getColor(x + s[0], y + s[1]))
552 });
553 }
554 if (validSteps.length == 0)
555 return null;
556 const step = validSteps[Random.randInt(validSteps.length)];
557 return [x + step[0], y + step[1]];
558 }
559
560 getEggEffect(move) {
561 const getRandomPiece = (c) => {
562 let bagOfPieces = [];
563 for (let i=0; i<this.size.x; i++) {
564 for (let j=0; j<this.size.y; j++) {
565 if (this.getColor(i, j) == c && this.getPiece(i, j) != 'k')
566 bagOfPieces.push([i, j]);
567 }
568 }
569 if (bagOfPieces.length >= 1)
570 return Random.sample(bagOfPieces);
571 return null;
572 };
573 const color = this.turn;
574 let em = null;
575 switch (move.egg) {
576 case "luigi":
577 case "waluigi":
578 // Change color of friendly or enemy piece, king excepted
579 const oldColor = (move.egg == "waluigi" ? color : C.GetOppCol(color));
580 const newColor = C.GetOppCol(oldColor);
581 const coords = getRandomPiece(oldColor);
582 if (coords) {
583 const piece = this.getPiece(coords[0], coords[1]);
584 em = new Move({
585 appear: [
586 new PiPo({x: coords[0], y: coords[1], c: newColor, p: piece})
587 ],
588 vanish: [
589 new PiPo({x: coords[0], y: coords[1], c: oldColor, p: piece})
590 ]
591 });
592 em.luigiEffect = true; //avoid dropping bomb/banana by mistake
593 }
594 break;
595 case "bowser":
596 em = new Move({
597 appear: [
598 new PiPo({
599 x: move.end.x,
600 y: move.end.y,
601 c: color,
602 p: V.IMMOBILIZE_CODE[move.appear[0].p]
603 })
604 ],
605 vanish: [
606 new PiPo({
607 x: move.end.x,
608 y: move.end.y,
609 c: color,
610 p: move.appear[0].p
611 })
612 ]
613 });
614 break;
615 case "koopa":
616 // Reverse move
617 em = new Move({
618 appear: [
619 new PiPo({
620 x: move.start.x, y: move.start.y, c: color, p: move.appear[0].p
621 })
622 ],
623 vanish: [
624 new PiPo({
625 x: move.end.x, y: move.end.y, c: color, p: move.appear[0].p
626 })
627 ]
628 });
629 if (this.board[move.start.x][move.start.y] != "") {
630 // Pawn or knight let something on init square
631 em.vanish.push(new PiPo({
632 x: move.start.x,
633 y: move.start.y,
634 c: 'a',
635 p: this.getPiece(move.start.x, move.start.y)
636 }));
637 }
638 break;
639 case "chomp":
640 // Eat piece
641 em = new Move({
642 appear: [],
643 vanish: [
644 new PiPo({
645 x: move.end.x, y: move.end.y, c: color, p: move.appear[0].p
646 })
647 ],
648 end: {x: move.end.x, y: move.end.y}
649 });
650 break;
651 }
652 if (em && move.egg != "koopa")
653 em.noAnimate = true; //static move
654 return em;
655 }
656
657 getMushroomEffect(move) {
658 if (
659 typeof move.start.x == "string" || //drop move (toadette)
660 ['b', 'r', 'q'].includes(move.vanish[0].p) //slider
661 ) {
662 return null;
663 }
664 let step = [move.end.x - move.start.x, move.end.y - move.start.y];
665 if (Math.abs(step[0]) == 2 && Math.abs(step[1]) == 0)
666 // Pawn initial 2-squares move: normalize step
667 step[0] /= 2;
668 const nextSquare = [move.end.x + step[0], move.end.y + step[1]];
669 let nextMove = null;
670 if (
671 this.onBoard(nextSquare[0], nextSquare[1]) &&
672 (
673 this.board[nextSquare[0]][nextSquare[1]] == "" ||
674 this.getColor(nextSquare[0], nextSquare[1]) == 'a'
675 )
676 ) {
677 this.playOnBoard(move); //HACK for getBasicMove()
678 nextMove = this.getBasicMove([move.end.x, move.end.y], nextSquare);
679 this.undoOnBoard(move);
680 }
681 return nextMove;
682 }
683
684 getBombBananaEffect(move, item) {
685 const steps = item == V.BANANA
686 ? [[1, 0], [-1, 0], [0, 1], [0, -1]]
687 : [[1, 1], [1, -1], [-1, 1], [-1, -1]];
688 const nextSquare = this.getRandomSquare([move.end.x, move.end.y], steps);
689 this.playOnBoard(move); //HACK for getBasicMove()
690 const res = this.getBasicMove([move.end.x, move.end.y], nextSquare);
691 this.undoOnBoard(move);
692 return res;
693 }
694
695 displayBonus(move) {
696 let divBonus = document.createElement("div");
697 divBonus.classList.add("bonus-text");
698 divBonus.innerHTML = move.egg;
699 let container = document.getElementById(this.containerId);
700 container.appendChild(divBonus);
701 setTimeout(() => container.removeChild(divBonus), 2000);
702 }
703
704 atLeastOneMove() {
705 return true;
706 }
707
708 filterValid(moves) {
709 return moves;
710 }
711
712 // Kingboo bonus can be animated better:
713 customAnimate(move, segments, cb) {
714 if (!move.kingboo)
715 return 0;
716 super.animateMoving(move.end, move.start, null,
717 segments.reverse().map(s => s.reverse()), cb);
718 return 1;
719 }
720
721 };