cee02423af311fa7cfafea8383e7363984f45bab
[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 };
25 }
26
27 get pawnPromotions() {
28 return ['q', 'r', 'n', 'b', 'k'];
29 }
30
31 get hasCastle() {
32 return false;
33 }
34 get hasEnpassant() {
35 return false;
36 }
37 get hasReserve() {
38 return true;
39 }
40 get hasReserveFen() {
41 return false;
42 }
43
44 static get IMMOBILIZE_CODE() {
45 return {
46 'p': 's',
47 'r': 'u',
48 'n': 'o',
49 'b': 'c',
50 'q': 't',
51 'k': 'l'
52 };
53 }
54
55 static get IMMOBILIZE_DECODE() {
56 return {
57 's': 'p',
58 'u': 'r',
59 'o': 'n',
60 'c': 'b',
61 't': 'q',
62 'l': 'k'
63 };
64 }
65
66 static get INVISIBLE_QUEEN() {
67 return 'i';
68 }
69
70 // Fictive color 'a', bomb banana mushroom egg
71 static get BOMB() {
72 return 'w'; //"Wario"
73 }
74 static get BANANA() {
75 return 'd'; //"Donkey"
76 }
77 static get EGG() {
78 return 'e';
79 }
80 static get MUSHROOM() {
81 return 'm';
82 }
83
84 static get EGG_SURPRISE() {
85 return [
86 "kingboo", "bowser", "daisy", "koopa",
87 "luigi", "waluigi", "toadette", "chomp"];
88 }
89
90 pieces(color, x, y) {
91 let specials = {
92 'i': {"class": "invisible"}, //queen
93 'e': {"class": "egg"},
94 'm': {"class": "mushroom"},
95 'd': {"class": "banana"},
96 'w': {"class": "bomb"}
97 };
98 return Object.assign(specials, super.pieces(color, x, y));
99 }
100
101 genRandInitFen(seed) {
102 const gr = new GiveawayRules(
103 {mode: "suicide", options: {}, genFenOnly: true});
104 // Add Peach + mario flags
105 return gr.genRandInitFen(seed).slice(0, -17) + '{"flags":"1111"}';
106 }
107
108 fen2board(f) {
109 return (
110 f.charCodeAt() <= 90
111 ? "w" + f.toLowerCase()
112 : (['w', 'd', 'e', 'm'].includes(f) ? "a" : "b") + f
113 );
114 }
115
116 setFlags(fenflags) {
117 // King can send shell? Queen can be invisible?
118 this.powerFlags = {
119 w: {k: false, q: false},
120 b: {k: false, q: false}
121 };
122 for (let c of ['w', 'b']) {
123 for (let p of ['k', 'q']) {
124 this.powerFlags[c][p] =
125 fenflags.charAt((c == "w" ? 0 : 2) + (p == 'k' ? 0 : 1)) == "1";
126 }
127 }
128 }
129
130 aggregateFlags() {
131 return this.powerFlags;
132 }
133
134 disaggregateFlags(flags) {
135 this.powerFlags = flags;
136 }
137
138 getFlagsFen() {
139 return ['w', 'b'].map(c => {
140 return ['k', 'q'].map(p => this.powerFlags[c][p] ? "1" : "0").join("");
141 }).join("");
142 }
143
144 setOtherVariables(fenParsed) {
145 this.setFlags(fenParsed.flags);
146 this.reserve = {}; //to be filled later
147 this.egg = null;
148 this.moveStack = [];
149 }
150
151 // For Toadette bonus
152 getDropMovesFrom([c, p]) {
153 if (typeof c != "string" || this.reserve[c][p] == 0)
154 return [];
155 let moves = [];
156 const start = (c == 'w' && p == 'p' ? 1 : 0);
157 const end = (color == 'b' && p == 'p' ? 7 : 8);
158 for (let i = start; i < end; i++) {
159 for (let j = 0; j < this.size.y; j++) {
160 const pieceIJ = this.getPiece(i, j);
161 if (
162 this.board[i][j] == "" ||
163 this.getColor(i, j) == 'a' ||
164 pieceIJ == V.INVISIBLE_QUEEN
165 ) {
166 let m = new Move({
167 start: {x: c, y: p},
168 end: {x: i, y: j},
169 appear: [new PiPo({x: i, y: j, c: c, p: p})],
170 vanish: []
171 });
172 // A drop move may remove a bonus (or hidden queen!)
173 if (this.board[i][j] != "")
174 m.vanish.push(new PiPo({x: i, y: j, c: 'a', p: pieceIJ}));
175 moves.push(m);
176 }
177 }
178 }
179 return moves;
180 }
181
182 getPotentialMovesFrom([x, y]) {
183 let moves = [];
184 if (this.egg == "toadette")
185 moves = this.getDropMovesFrom([x, y]);
186 else if (this.egg == "kingboo") {
187 const initPiece = this.getPiece(x, y);
188 const color = this.getColor(x, y);
189 const oppCol = C.GetOppCol(color);
190 // Only allow to swap pieces
191 for (let i=0; i<this.size.x; i++) {
192 for (let j=0; j<this.size.y; j++) {
193 const colIJ = this.getColor(i, j);
194 const pieceIJ = this.getPiece(i, j);
195 if (
196 (i != x || j != y) &&
197 ['w', 'b'].includes(colIJ) &&
198 // Next conditions = no pawn on last rank
199 (
200 initPiece != 'p' ||
201 (
202 (color != 'w' || i != 0) &&
203 (color != 'b' || i != this.size.x - 1)
204 )
205 )
206 &&
207 (
208 pieceIJ != 'p' ||
209 (
210 (colIJ != 'w' || x != 0) &&
211 (colIJ != 'b' || x != this.size.x - 1)
212 )
213 )
214 ) {
215 let m = this.getBasicMove([x, y], [i, j]);
216 m.appear.push(
217 new PiPo({x: x, y: y, p: this.getPiece(i, j), c: oppCol}));
218 moves.push(m);
219 }
220 }
221 }
222 }
223 else {
224 // Normal case (including bonus daisy)
225 const piece = this.getPiece(x, y);
226 switch (piece) {
227 case 'p':
228 moves = this.getPawnMovesFrom([x, y]); //apply promotions
229 break;
230 case 'q':
231 moves = this.getQueenMovesFrom([x, y]);
232 break;
233 case 'k':
234 moves = this.getKingMovesFrom([x, y]);
235 break;
236 case 'n':
237 moves = this.getKnightMovesFrom([x, y]);
238 break;
239 case 'b':
240 case 'r':
241 // explicitely listing types to avoid moving immobilized piece
242 moves = this.getRookOrBishopMovesFrom([x, y], piece);
243 break;
244 }
245 }
246 // Set potential random effects, so that play() is deterministic
247 moves.forEach(m => {
248 switch (this.getPiece(m.end.x, m.end.y)) {
249 case V.EGG:
250 m.egg = Random.sample(V.EGG_SURPRISE);
251 m.next = this.getEggEffect(m);
252 break;
253 case V.BANANA:
254 m.next = this.getRandomSquare(
255 [m.end.x, m.end.y], [[1, 1], [1, -1], [-1, 1], [-1, -1]]);
256 break;
257 case V.BOMB:
258 m.next = this.getRandomSquare(
259 [m.end.x, m.end.y], [[1, 0], [-1, 0], [0, 1], [0, -1]]);
260 break;
261 }
262 });
263 return moves;
264 }
265
266 getEggEffect(move) {
267 const getRandomPiece = (c) => {
268 let bagOfPieces = [];
269 for (let i=0; i<this.size.x; i++) {
270 for (let j=0; j<this.size.y; j++) {
271 if (this.getColor(i, j) == c && this.getPiece(i, j) != 'k')
272 bagOfPieces.push([i, j]);
273 }
274 }
275 if (bagOfPieces.length >= 1)
276 return Random.sample(bagOfPieces);
277 return null;
278 };
279 const color = this.turn;
280 let em = null;
281 switch (move.egg) {
282 case "luigi":
283 case "waluigi":
284 // Change color of friendly or enemy piece, king excepted
285 const oldColor = (move.egg == "waluigi" ? color : C.GetOppCol(color));
286 const newColor = C.GetOppCol(oldColor);
287 const coords = getRandomPiece(oldColor);
288 if (coords) {
289 const piece = this.getPiece(coords[0], coords[1]);
290 em = new Move({
291 appear: [
292 new PiPo({x: coords[0], y: coords[1], c: newColor, p: piece})
293 ],
294 vanish: [
295 new PiPo({x: coords[0], y: coords[1], c: oldColor, p: piece})
296 ]
297 });
298 }
299 break;
300 case "bowser":
301 em = new Move({
302 appear: [
303 new PiPo({
304 x: move.end.x,
305 y: move.end.y,
306 c: color,
307 p: V.IMMOBILIZED_CODE[move.appear[0].p]
308 })
309 ],
310 vanish: [
311 new PiPo({
312 x: move.end.x,
313 y: move.end.y,
314 c: color,
315 p: move.appear[0].p
316 })
317 ]
318 });
319 break;
320 case "koopa":
321 // Reverse move
322 em = new Move({
323 appear: [
324 new PiPo({
325 x: move.start.x, y: move.start.y, c: color, p: move.appear[0].p
326 })
327 ],
328 vanish: [
329 new PiPo({
330 x: move.end.x, y: move.end.y, c: color, p: move.appear[0].p
331 })
332 ]
333 });
334 break;
335 case "chomp":
336 // Eat piece
337 em = new Move({
338 appear: [],
339 vanish: [
340 new PiPo({
341 x: move.end.x, y: move.end.y, c: color, p: move.appear[0].p
342 })
343 ],
344 end: {x: move.end.x, y: move.end.y}
345 });
346 break;
347 }
348 return em;
349 }
350
351 // Helper to set and apply banana/bomb effect
352 getRandomSquare([x, y], steps, freeSquare) {
353 let validSteps = steps.filter(s => this.onBoard(x + s[0], y + s[1]));
354 if (freeSquare) {
355 // Square to put banana/bomb cannot be occupied by a piece
356 validSteps = validSteps.filter(s => {
357 return ["", 'a'].includes(this.getColor(x + s[0], y + s[1]))
358 });
359 }
360 if (validSteps.length == 0)
361 return null;
362 const step = validSteps[Random.randInt(validSteps.length)];
363 return [x + step[0], y + step[1]];
364 }
365
366 getPotentialMovesOf(piece, [x, y]) {
367 const color = this.getColor(x, y);
368 const stepSpec = this.pieces(color, x, y)[piece];
369 let moves = [];
370 const findAddMoves = (type, stepArray) => {
371 for (let s of stepArray) {
372 outerLoop: for (let step of s.steps) {
373 let [i, j] = [x + step[0], y + step[1]];
374 let stepCounter = 1;
375 while (
376 this.onBoard(i, j) &&
377 (
378 this.board[i][j] == "" ||
379 [V.MUSHROOM, V.EGG].includes(this.getPiece(i, j))
380 )
381 ) {
382 if (type != "attack")
383 moves.push(this.getBasicMove([x, y], [i, j]));
384 if (s.range <= stepCounter++)
385 continue outerLoop;
386 [i, j] = [i + step[0], j + step[1]];
387 }
388 if (!this.onBoard(i, j))
389 continue;
390 const pieceIJ = this.getPiece(i, j);
391 if (type != "moveonly" && this.getColor(i, j) != color)
392 moves.push(this.getBasicMove([x, y], [i, j]));
393 }
394 }
395 };
396 const specialAttack = !!stepSpec.attack;
397 if (specialAttack)
398 findAddMoves("attack", stepSpec.attack);
399 findAddMoves(specialAttack ? "moveonly" : "all", stepSpec.moves);
400 return moves;
401 }
402
403 getPawnMovesFrom([x, y]) {
404 const color = this.turn;
405 const oppCol = C.GetOppCol(color);
406 const shiftX = (color == 'w' ? -1 : 1);
407 const firstRank = (color == "w" ? this.size.x - 1 : 0);
408 let moves = [];
409 if (
410 this.board[x + shiftX][y] == "" ||
411 this.getColor(x + shiftX, y) == 'a' ||
412 this.getPiece(x + shiftX, y) == V.INVISIBLE_QUEEN
413 ) {
414 moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
415 if (
416 [firstRank, firstRank + shiftX].includes(x) &&
417 (
418 this.board[x + 2 * shiftX][y] == "" ||
419 this.getColor(x + 2 * shiftX, y) == 'a' ||
420 this.getPiece(x + 2 * shiftX, y) == V.INVISIBLE_QUEEN
421 )
422 ) {
423 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
424 }
425 }
426 for (let shiftY of [-1, 1]) {
427 if (
428 y + shiftY >= 0 &&
429 y + shiftY < this.size.y &&
430 this.board[x + shiftX][y + shiftY] != "" &&
431 // Pawns cannot capture invisible queen this way!
432 this.getPiece(x + shiftX, y + shiftY) != V.INVISIBLE_QUEEN &&
433 ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
434 ) {
435 moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
436 }
437 }
438 this.pawnPostProcess(moves, color, oppCol);
439 // Add mushroom on initial square
440 moves.forEach(m => {
441 m.appear.push(new PiPo({x: m.start.x, y: m.start.y, c: 'a', p: 'm'}));
442 });
443 return moves;
444 }
445
446 getRookOrBishopMovesFrom([x, y], type) {
447 // Add banana if possible, diagonaly
448 return this.getPotentialMovesOf(type, [x, y]).map(m => {
449 const bs =
450 this.getRandomSquare([m.end.x, m.end.y],
451 type == 'r'
452 ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
453 : [[1, 0], [-1, 0], [0, 1], [0, -1]],
454 "freeSquare");
455 if (bs) {
456 m.appear.push(
457 new PiPo({
458 x: bs[0],
459 y: bs[1],
460 c: 'a',
461 p: type == 'r' ? 'd' : 'w'
462 })
463 );
464 if (this.board[bs[0]][bs[1]] != "") {
465 m.vanish.push(
466 new PiPo({
467 x: bs[0],
468 y: bs[1],
469 c: this.getColor(bs[0], bs[1]),
470 p: this.getPiece(bs[0], bs[1])
471 })
472 );
473 }
474 }
475 return m;
476 });
477 }
478
479 getKnightMovesFrom([x, y]) {
480 // Add egg on initial square:
481 return this.getPotentialMovesOf('n', [x, y]).map(m => {
482 m.appear.push(new PiPo({p: "e", c: "a", x: x, y: y}));
483 return m;
484 });
485 }
486
487 getQueenMovesFrom(sq) {
488 const normalMoves = this.getPotentialMovesOf('q', sq);
489 // If flag allows it, add 'invisible movements'
490 let invisibleMoves = [];
491 if (this.powerFlags[this.turn]['q']) {
492 normalMoves.forEach(m => {
493 if (
494 m.appear.length == 1 &&
495 m.vanish.length == 1 &&
496 // Only simple non-capturing moves:
497 m.vanish[0].c != 'a'
498 ) {
499 let im = JSON.parse(JSON.stringify(m));
500 im.appear[0].p = V.INVISIBLE_QUEEN;
501 im.noAnimate = true;
502 invisibleMoves.push(im);
503 }
504 });
505 }
506 return normalMoves.concat(invisibleMoves);
507 }
508
509 getKingMovesFrom([x, y]) {
510 let moves = this.getPotentialMovesOf('k', [x, y]);
511 // If flag allows it, add 'remote shell captures'
512 if (this.powerFlags[this.turn]['k']) {
513 super.pieces()['k'].moves[0].steps.forEach(step => {
514 let [i, j] = [x + step[0], y + step[1]];
515 while (
516 this.onBoard(i, j) &&
517 (
518 this.board[i][j] == "" ||
519 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
520 (
521 this.getColor(i, j) == 'a' &&
522 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
523 )
524 )
525 ) {
526 i += step[0];
527 j += step[1];
528 }
529 if (this.onBoard(i, j)) {
530 const colIJ = this.getColor(i, j);
531 if (colIJ != this.turn) {
532 // May just destroy a bomb or banana:
533 let shellCapture = new Move({
534 start: {x: x, y: y},
535 end: {x: i, y: j},
536 appear: [],
537 vanish: [
538 new PiPo({x: i, y: j, c: colIJ, p: this.getPiece(i, j)})
539 ]
540 });
541 shellCapture.shell = true; //easier play()
542 moves.push(shellCapture);
543 }
544 }
545 });
546 }
547 return moves;
548 }
549
550 play(move) {
551 this.egg = move.egg;
552 const color = this.turn;
553 if (move.egg == "toadette") {
554 this.reserve = { w: {}, b: {} };
555 // Randomly select a piece in pawnPromotions
556 this.reserve[color][Random.sample(this.pawnPromotions)] = 1;
557 this.re_drawReserve([color]);
558 }
559 else if (Object.keys(this.reserve).length > 0) {
560 this.reserve = {};
561 this.re_drawReserve([color]);
562 }
563 if (this.getPiece(move.end.x, move.end.y) == V.MUSHROOM)
564 move.next = this.getMushroomEffect(move);
565 if (move.shell)
566 this.powerFlags[color]['k'] = false;
567 else if (move.appear.length > 0 && move.appear[0].p == V.INVISIBLE_QUEEN)
568 this.powerFlags[move.appear[0].c]['q'] = false;
569 this.playOnBoard(move);
570 // Look for an immobilized piece of my color: it can now move
571 for (let i=0; i<8; i++) {
572 for (let j=0; j<8; j++) {
573 if ((i != move.end.x || j != move.end.y) && this.board[i][j] != "") {
574 const piece = this.getPiece(i, j);
575 if (
576 this.getColor(i, j) == color &&
577 Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
578 ) {
579 this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
580 }
581 }
582 }
583 }
584 // Also make opponent invisible queen visible again, if any
585 const oppCol = C.GetOppCol(color);
586 for (let i=0; i<8; i++) {
587 for (let j=0; j<8; j++) {
588 if (
589 this.board[i][j] != "" &&
590 this.getColor(i, j) == oppCol &&
591 this.getPiece(i, j) == V.INVISIBLE_QUEEN
592 ) {
593 this.board[i][j] = oppCol + V.QUEEN;
594 }
595 }
596 }
597 if (!move.next && !["daisy", "toadette", "kingboo"].includes(move.egg)) {
598 this.turn = oppCol;
599 this.movesCount++;
600 }
601 if (move.egg)
602 this.displayBonus(move.egg);
603 this.nextMove = move.next;
604 }
605
606 displayBonus(egg) {
607 alert(egg); //TODO: nicer display
608 }
609
610 filterValid(moves) {
611 return moves;
612 }
613
614 getMushroomEffect(move) {
615 let step = [move.end.x - move.start.x, move.end.y - move.start.y];
616 if ([0, 1].some(i => step[i] >= 2 && step[1-i] != 1)) {
617 // Slider, multi-squares: normalize step
618 for (let j of [0, 1])
619 step[j] = step[j] / Math.abs(step[j]) || 0;
620 }
621 const nextSquare = [move.end.x + step[0], move.end.y + step[1]];
622 const afterSquare =
623 [nextSquare[0] + step[0], nextSquare[1] + step[1]];
624 let nextMove = null;
625 if (
626 this.onBoard(nextSquare[0], nextSquare[1]) &&
627 this.board[nextSquare[0]][nextSquare[1]] == "" &&
628 ['k', 'p', 'n'].includes(move.vanish[0].p)
629 ) {
630 // Speed up non-sliders
631 nextMove = this.getBasicMove([move.end.x, move.end.y], nextSquare);
632 }
633 else if (
634 this.onBoard(afterSquare[0], afterSquare[1]) &&
635 this.board[nextSquare[0]][nextSquare[1]] != "" &&
636 this.getColor(nextSquare[0], nextSquare[1]) != 'a' &&
637 this.getColor(afterSquare[0], afterSquare[1]) != this.turn
638 ) {
639 nextMove = this.getBasicMove([move.end.x, move.end.y], afterSquare);
640 }
641 return nextMove;
642 }
643
644 playPlusVisual(move, r) {
645 this.moveStack.push(move);
646 this.play(move);
647 this.playVisual(move, r);
648 if (this.nextMove)
649 this.playPlusVisual(this.nextMove, r);
650 else
651 this.afterPlay(this.moveStack);
652 }
653
654 // TODO: set some special moves (effects) as noAnimate
655 // TODO: put bomb/banana only at final location of a move ? Seems more logical
656 // + fix bishop takes mushroom and jump
657 // Also improve showChoices for invisible queen + king shell
658 // + fix turn issues after multimove (like bishop put bomb)
659
660 };