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