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