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