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