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