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