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