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