Chakart ready soon
[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
38 static get IMMOBILIZE_CODE() {
39 return {
40 'p': 's',
41 'r': 'u',
42 'n': 'o',
43 'b': 'c',
44 'q': 't',
45 'k': 'l'
46 };
47 }
48
49 static get IMMOBILIZE_DECODE() {
50 return {
51 's': 'p',
52 'u': 'r',
53 'o': 'n',
54 'c': 'b',
55 't': 'q',
56 'l': 'k'
57 };
58 }
59
60 static get INVISIBLE_QUEEN() {
61 return 'i';
62 }
63
64 // Fictive color 'a', bomb banana mushroom egg
65 static get BOMB() {
66 return 'w'; //"Wario"
67 }
68 static get BANANA() {
69 return 'd'; //"Donkey"
70 }
71 static get EGG() {
72 return 'e';
73 }
74 static get MUSHROOM() {
75 return 'm';
76 }
77
78 genRandInitFen(seed) {
79 const gr = new GiveawayRules(
80 {mode: "suicide", options: {}, genFenOnly: true});
81 return (
82 gr.genRandInitFen(seed).slice(0, -17) +
83 // Add Peach + Mario flags + capture counts
84 '{"flags":"1111","ccount":"000000000000"}'
85 );
86 }
87
88 fen2board(f) {
89 return (
90 f.charCodeAt() <= 90
91 ? "w" + f.toLowerCase()
92 : (['w', 'd', 'e', 'm'].includes(f) ? "a" : "b") + f
93 );
94 }
95
96 setFlags(fenflags) {
97 // King can send shell? Queen can be invisible?
98 this.powerFlags = {
99 w: {k: false, q: false},
100 b: {k: false, q: false}
101 };
102 for (let c of ['w', 'b']) {
103 for (let p of ['k', 'q']) {
104 this.powerFlags[c][p] =
105 fenflags.charAt((c == "w" ? 0 : 2) + (p == 'k' ? 0 : 1)) == "1";
106 }
107 }
108 }
109
110 aggregateFlags() {
111 return this.powerFlags;
112 }
113
114 disaggregateFlags(flags) {
115 this.powerFlags = flags;
116 }
117
118 getFen() {
119 return super.getFen() + " " + this.getCapturedFen();
120 }
121
122 getFlagsFen() {
123 return ['w', 'b'].map(c => {
124 return ['k', 'q'].map(p => this.powerFlags[c][p] ? "1" : "0").join("");
125 }).join("");
126 }
127
128 getCapturedFen() {
129 const res = ['w', 'b'].map(c => Object.values(this.captured[c]));
130 return res[0].concat(res[1]).join("");
131 }
132
133 setOtherVariables(fenParsed) {
134 super.setOtherVariables(fenParsed);
135 // Initialize captured pieces' counts from FEN
136 const allCapts = fenParsed.ccount.split("").map(x => parseInt(x, 10));
137 const pieces = ['p', 'r', 'n', 'b', 'q', 'k'];
138 this.captured = {
139 w: ArrayFun.toObject(pieces, allCapts.slice(0, 6)),
140 b: ArrayFun.toObject(pieces, allCapts.slice(6, 12))
141 };
142 this.reserve = { w: {}, b: {} }; //to be replaced by this.captured
143 this.moveStack = [];
144 this.egg = null;
145 }
146
147 // For Toadette bonus
148 getDropMovesFrom([c, p]) {
149 if (typeof c != "string" || this.reserve[c][p] == 0)
150 return [];
151 let moves = [];
152 const start = (c == 'w' && p == 'p' ? 1 : 0);
153 const end = (color == 'b' && p == 'p' ? 7 : 8);
154 for (let i = start; i < end; i++) {
155 for (let j = 0; j < this.size.y; j++) {
156 const pieceIJ = this.getPiece(i, j);
157 if (
158 this.board[i][j] == "" ||
159 this.getColor(i, j) == 'a' ||
160 pieceIJ == V.INVISIBLE_QUEEN
161 ) {
162 let m = new Move({
163 start: {x: c, y: p},
164 end: {x: i, y: j},
165 appear: [new PiPo({x: i, y: j, c: c, p: p})],
166 vanish: []
167 });
168 // A drop move may remove a bonus (or hidden queen!)
169 if (this.board[i][j] != "")
170 m.vanish.push(new PiPo({x: i, y: j, c: 'a', p: pieceIJ}));
171 moves.push(m);
172 }
173 }
174 }
175 return moves;
176 }
177
178 // Moving something. Potential effects resolved after playing
179 getPotentialMovesFrom([x, y]) {
180 let moves = [];
181 if (this.egg == "toadette")
182 return this.getDropMovesFrom([x, y]);
183 if (this.egg == "kingboo") {
184 const initPiece = this.getPiece(x, y);
185 const color = this.getColor(x, y);
186 const oppCol = C.GetOppCol(color);
187 // Only allow to swap pieces
188 for (let i=0; i<this.size.x; i++) {
189 for (let j=0; j<this.size.y; j++) {
190 const colIJ = this.getColor(i, j);
191 const pieceIJ = this.getPiece(i, j);
192 if (
193 (i != x || j != y) &&
194 ['w', 'b'].includes(colIJ) &&
195 // Next conditions = no pawn on last rank
196 (
197 initPiece != 'p' ||
198 (
199 (color != 'w' || i != 0) &&
200 (color != 'b' || i != this.size.x - 1)
201 )
202 )
203 &&
204 (
205 pieceIJ != 'p' ||
206 (
207 (colIJ != 'w' || x != 0) &&
208 (colIJ != 'b' || x != this.size.x - 1)
209 )
210 )
211 ) {
212 let m = this.getBasicMove([x, y], [i, j]);
213 m.appear.push(
214 new PiPo({x: x, y: y, p: this.getPiece(i, j), c: oppCol}));
215 moves.push(m);
216 }
217 }
218 }
219 return moves;
220 }
221 // Normal case (including bonus daisy)
222 switch (this.getPiece(x, y)) {
223 case 'p':
224 moves = this.getPawnMovesFrom([x, y]); //apply promotions
225 break;
226 case 'q':
227 moves = this.getQueenMovesFrom([x, y]);
228 break;
229 case 'k':
230 moves = this.getKingMovesFrom([x, y]);
231 break;
232 case 'n':
233 moves = this.getKnightMovesFrom([x, y]);
234 break;
235 case 'b':
236 case 'r':
237 // explicitely listing types to avoid moving immobilized piece
238 moves = super.getPotentialMovesFrom([x, y]);
239 }
240 return moves;
241 }
242
243 getPawnMovesFrom([x, y]) {
244 const color = this.turn;
245 const oppCol = C.GetOppCol(color);
246 const shiftX = (color == 'w' ? -1 : 1);
247 const firstRank = (color == "w" ? this.size.x - 1 : 0);
248 let moves = [];
249 if (
250 this.board[x + shiftX][y] == "" ||
251 this.getColor(x + shiftX, y) == 'a' ||
252 this.getPiece(x + shiftX, y) == V.INVISIBLE_QUEEN
253 ) {
254 moves.push(this.getBasicMove([x, y], [x + shiftX, y]));
255 if (
256 [firstRank, firstRank + shiftX].includes(x) &&
257 (
258 this.board[x + 2 * shiftX][y] == "" ||
259 this.getColor(x + 2 * shiftX, y) == 'a' ||
260 this.getPiece(x + 2 * shiftX, y) == V.INVISIBLE_QUEEN
261 )
262 ) {
263 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
264 }
265 }
266 for (let shiftY of [-1, 1]) {
267 if (
268 y + shiftY >= 0 &&
269 y + shiftY < this.size.y &&
270 this.board[x + shiftX][y + shiftY] != "" &&
271 // Pawns cannot capture invisible queen this way!
272 this.getPiece(x + shiftX, y + shiftY) != V.INVISIBLE_QUEEN &&
273 ['a', oppCol].includes(this.getColor(x + shiftX, y + shiftY))
274 ) {
275 moves.push(this.getBasicMove([x, y], [x + shiftX, y + shiftY]));
276 }
277 }
278 super.pawnPostProcess(moves, color, oppCol);
279 return moves;
280 }
281
282 getQueenMovesFrom(sq) {
283 const normalMoves = super.getPotentialMovesOf('q', sq);
284 // If flag allows it, add 'invisible movements'
285 let invisibleMoves = [];
286 if (this.powerFlags[this.turn]['q']) {
287 normalMoves.forEach(m => {
288 if (
289 m.appear.length == 1 &&
290 m.vanish.length == 1 &&
291 // Only simple non-capturing moves:
292 m.vanish[0].c != 'a'
293 ) {
294 let im = JSON.parse(JSON.stringify(m));
295 im.appear[0].p = V.INVISIBLE_QUEEN;
296 im.end.noHighlight = true;
297 invisibleMoves.push(im);
298 }
299 });
300 }
301 return normalMoves.concat(invisibleMoves);
302 }
303
304 getKingMovesFrom([x, y]) {
305 let moves = super.getPotentialMovesOf('k', [x, y]);
306 // If flag allows it, add 'remote shell captures'
307 if (this.powerFlags[this.turn]['k']) {
308 super.pieces()['k'].moves[0].steps.forEach(step => {
309 let [i, j] = [x + step[0], y + step[1]];
310 while (
311 this.onBoard(i, j) &&
312 (
313 this.board[i][j] == "" ||
314 this.getPiece(i, j) == V.INVISIBLE_QUEEN ||
315 (
316 this.getColor(i, j) == 'a' &&
317 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
318 )
319 )
320 ) {
321 i += step[0];
322 j += step[1];
323 }
324 if (this.onBoard(i, j)) {
325 const colIJ = this.getColor(i, j);
326 if (colIJ != this.turn) {
327 // May just destroy a bomb or banana:
328 moves.push(
329 new Move({
330 start: {x: x, y: y},
331 end: {x: i, y: j},
332 appear: [],
333 vanish: [
334 new PiPo({x: i, y: j, c: colIJ, p: this.getPiece(i, j)})
335 ]
336 })
337 );
338 }
339 }
340 });
341 }
342 return moves;
343 }
344
345 getKnightMovesFrom([x, y]) {
346 // Add egg on initial square:
347 return super.getPotentialMovesOf('n', [x, y]).map(m => {
348 m.appear.push(new PiPo({p: "e", c: "a", x: x, y: y}));
349 return m;
350 });
351 }
352
353 /// if any of my pieces was immobilized, it's not anymore.
354 //if play set a piece immobilized, then mark it
355 play(move) {
356 if (move.effect == "toadette") {
357 this.reserve = this.captured;
358 this.re_drawReserve([this.turn]);
359 }
360 else if (this.reserve) {
361 this.reserve = { w: {}, b: {} };
362 this.re_drawReserve([this.turn]);
363 }
364 const color = this.turn;
365 if (
366 move.vanish.length == 2 &&
367 move.vanish[1].c != 'a' &&
368 move.appear.length == 1 //avoid king Boo!
369 ) {
370 // Capture: update this.captured
371 let capturedPiece = move.vanish[1].p;
372 if (capturedPiece == V.INVISIBLE_QUEEN)
373 capturedPiece = V.QUEEN;
374 else if (Object.keys(V.IMMOBILIZE_DECODE).includes(capturedPiece))
375 capturedPiece = V.IMMOBILIZE_DECODE[capturedPiece];
376 this.captured[move.vanish[1].c][capturedPiece]++;
377 }
378 else if (move.vanish.length == 0) {
379 if (move.appear.length == 0 || move.appear[0].c == 'a')
380 return;
381 // A piece is back on board
382 this.captured[move.appear[0].c][move.appear[0].p]--;
383 }
384 if (move.appear.length == 0) {
385 // Three cases: king "shell capture", Chomp or Koopa
386 if (this.getPiece(move.start.x, move.start.y) == V.KING)
387 // King remote capture:
388 this.powerFlags[color][V.KING] = false;
389 else if (move.end.effect == "chomp")
390 this.captured[color][move.vanish[0].p]++;
391 }
392 else if (move.appear[0].p == V.INVISIBLE_QUEEN)
393 this.powerFlags[move.appear[0].c][V.QUEEN] = false;
394 if (this.subTurn == 2) return;
395 if (
396 move.turn[1] == 1 &&
397 move.appear.length == 0 ||
398 !(Object.keys(V.IMMOBILIZE_DECODE).includes(move.appear[0].p))
399 ) {
400 // Look for an immobilized piece of my color: it can now move
401 for (let i=0; i<8; i++) {
402 for (let j=0; j<8; j++) {
403 if (this.board[i][j] != V.EMPTY) {
404 const piece = this.getPiece(i, j);
405 if (
406 this.getColor(i, j) == color &&
407 Object.keys(V.IMMOBILIZE_DECODE).includes(piece)
408 ) {
409 this.board[i][j] = color + V.IMMOBILIZE_DECODE[piece];
410 move.wasImmobilized = [i, j];
411 }
412 }
413 }
414 }
415 }
416 // Also make opponent invisible queen visible again, if any
417 const oppCol = V.GetOppCol(color);
418 for (let i=0; i<8; i++) {
419 for (let j=0; j<8; j++) {
420 if (
421 this.board[i][j] != V.EMPTY &&
422 this.getColor(i, j) == oppCol &&
423 this.getPiece(i, j) == V.INVISIBLE_QUEEN
424 ) {
425 this.board[i][j] = oppCol + V.QUEEN;
426 move.wasInvisible = [i, j];
427 }
428 }
429 }
430 this.playOnBoard(move);
431 if (["kingboo", "toadette", "daisy"].includes(move.effect)) {
432 this.effect = move.effect;
433 this.subTurn = 2;
434 }
435 else {
436 this.turn = C.GetOppCol(this.turn);
437 this.movesCount++;
438 this.subTurn = 1;
439 }
440
441
442 if (move.egg)
443 this.displayBonus(move.egg);
444 else if (this.egg)
445 this.egg = null; //the egg is consumed
446 }
447
448 displayBonus(egg) {
449 alert(egg); //TODO: nicer display
450 }
451
452 filterValid(moves) {
453 return moves;
454 }
455
456 tryMoveFollowup(move, cb) {
457 // Warning: at this stage, the move is played
458 if (move.vanish.length == 2 && move.vanish[1].c == 'a') {
459 // effect, or bonus/malus
460 const endType = move.vanish[1].p;
461 switch (endType) {
462 case V.EGG:
463 this.applyRandomBonus(move, cb);
464 break;
465 case V.BANANA:
466 case V.BOMB: {
467 const dest =
468 this.getRandomSquare([m.end.x, m.end.y],
469 endType == V.BANANA
470 ? [[1, 1], [1, -1], [-1, 1], [-1, -1]]
471 : [[1, 0], [-1, 0], [0, 1], [0, -1]]);
472 cb(this.getBasicMove([move.end.x, move.end.y], dest));
473 break;
474 }
475 case V.MUSHROOM: {
476 let step = [move.end.x - move.start.x, move.end.y - move.start.y];
477 if ([0, 1].some(i => step[i] >= 2 && step[1-i] != 1)) {
478 // Slider, multi-squares: normalize step
479 for (let j of [0, 1])
480 step[j] = step[j] / Math.abs(step[j]) || 0;
481 }
482 const nextSquare = [move.end.x + step[0], move.end.y + step[1]];
483 if (this.onBoard(nextSquare[0], nextSquare[1])) {
484 if (
485 this.board[nextSquare[0]][nextSquare[1]] != "" &&
486 this.getColor(nextSquare[0], nextSquare[1]) != 'a'
487 ) {
488 // (try to) jump
489 const afterSquare =
490 [nextSquare[0] + step[0], nextSquare[1] + step[1]];
491 if (
492 this.onBoard(afterSquare[0], afterSquare[1]) &&
493 this.getColor(afterSquare[0], afterSquare[1]) != this.turn
494 ) {
495 cb(this.getBasicMove([move.end.x, move.end.y], afterSquare));
496 }
497 }
498 else if (!['b', 'r', 'q'].includes(move.vanish[0].p))
499 // Take another step forward if not slider move
500 cb(this.getBasicMove([move.end.x, move.end.y], nextSquare));
501 }
502 break;
503 }
504 }
505 }
506 }
507
508 applyRandomBonus(move, cb) {
509 // TODO: determine bonus/malus, and then ...
510 // if toadette, daisy or kingboo : do not call cb
511 this.egg = "daisy"; //not calling cb in this case
512 this.displayBonus(this.egg);
513 move.egg = this.egg; //for play() by opponent
514 }
515
516 // Helper to apply banana/bomb effect
517 getRandomSquare([x, y], steps) {
518 const validSteps = steps.filter(s => this.onBoard(x + s[0], y + s[1]));
519 const step = validSteps[Random.randInt(validSteps.length)];
520 return [x + step[0], y + step[1]];
521 }
522
523 // Warning: if play() is called, then move.end changed.
524 playPlusVisual(move, r) {
525 this.moveStack.push(move);
526 this.play(move);
527 this.playVisual(move, r);
528 this.tryMoveFollowup(move, (nextMove) => {
529 if (nextMove)
530 this.playPlusVisual(nextMove, r);
531 else
532 this.afterPlay(this.moveStack);
533 });
534 }
535
536 };