Advance on Chakart
[vchess.git] / client / src / variants / Chakart.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { SuicideRules } from "@/variants/Suicide";
3
4 export class ChakartRules extends ChessRules {
5 static get PawnSpecs() {
6 return SuicideRules.PawnSpecs;
7 }
8
9 static get HasCastle() {
10 return false;
11 }
12
13 static get CorrConfirm() {
14 // Because of bonus effects
15 return false;
16 }
17
18 static get CanAnalyze() {
19 return false;
20 }
21
22 hoverHighlight(x, y) {
23 if (this.subTurn == 1) return false;
24 const L = this.firstMove.length;
25 const fm = this.firstMove[L-1];
26 if (fm.end.effect != 0) return false;
27 const deltaX = Math.abs(fm.end.x - x);
28 const deltaY = Math.abs(fm.end.y - y);
29 return (
30 (deltaX == 0 && deltaY == 0) ||
31 (
32 this.board[x][y] == V.EMPTY &&
33 (
34 (fm.vanish[0].p == V.ROOK && deltaX == 1 && deltaY == 1) ||
35 (fm.vanish[0].p == V.BISHOP && deltaX + deltaY == 1)
36 )
37 )
38 );
39 }
40
41 static get IMMOBILIZE_CODE() {
42 return {
43 'p': 's',
44 'r': 'u',
45 'n': 'o',
46 'b': 'c',
47 'q': 't',
48 'k': 'l'
49 };
50 }
51
52 static get IMMOBILIZE_DECODE() {
53 return {
54 's': 'p',
55 'u': 'r',
56 'o': 'n',
57 'c': 'b',
58 't': 'q',
59 'l': 'k'
60 };
61 }
62
63 static get INVISIBLE_QUEEN() {
64 return 'i';
65 }
66
67 // Fictive color 'a', bomb banana mushroom egg
68 static get BOMB() {
69 // Doesn't collide with bishop because color 'a'
70 return 'b';
71 }
72 static get BANANA() {
73 return 'n';
74 }
75 static get EGG() {
76 return 'e';
77 }
78 static get MUSHROOM() {
79 return 'm';
80 }
81
82 static get PIECES() {
83 return (
84 ChessRules.PIECES.concat(
85 Object.keys(V.IMMOBILIZE_DECODE)).concat(
86 [V.BANANA, V.BOMB, V.EGG, V.MUSHROOM, V.INVISIBLE_QUEEN])
87 );
88 }
89
90 getPpath(b) {
91 let prefix = "";
92 if (
93 b[0] == 'a' ||
94 b[1] == V.INVISIBLE_QUEEN ||
95 Object.keys(V.IMMOBILIZE_DECODE).includes(b[1])
96 ) {
97 prefix = "Chakart/";
98 }
99 return prefix + b;
100 }
101
102 static ParseFen(fen) {
103 const fenParts = fen.split(" ");
104 return Object.assign(
105 ChessRules.ParseFen(fen),
106 { captured: fenParts[5] }
107 );
108 }
109
110 // King can be l or L (immobilized) --> similar to Alice variant
111 static IsGoodPosition(position) {
112 if (position.length == 0) return false;
113 const rows = position.split("/");
114 if (rows.length != V.size.x) return false;
115 let kings = { "k": 0, "K": 0, 'l': 0, 'L': 0 };
116 for (let row of rows) {
117 let sumElts = 0;
118 for (let i = 0; i < row.length; i++) {
119 if (['K','k','L','l'].includes(row[i])) kings[row[i]]++;
120 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
121 else {
122 const num = parseInt(row[i]);
123 if (isNaN(num)) return false;
124 sumElts += num;
125 }
126 }
127 if (sumElts != V.size.y) return false;
128 }
129 if (kings['k'] + kings['l'] != 1 || kings['K'] + kings['L'] != 1)
130 return false;
131 return true;
132 }
133
134 static IsGoodFlags(flags) {
135 // 4 for Peach + Mario w, b
136 return !!flags.match(/^[01]{4,4}$/);
137 }
138
139 setFlags(fenflags) {
140 // King can send shell? Queen can be invisible?
141 this.powerFlags = {
142 w: [{ 'k': false, 'q': false }],
143 b: [{ 'k': false, 'q': false }]
144 };
145 for (let c of ["w", "b"]) {
146 for (let p of ['k', 'q']) {
147 this.powerFlags[c][p] =
148 fenFlags.charAt((c == "w" ? 0 : 2) + (p == 'k' ? 0 : 1)) == "1";
149 }
150 }
151 }
152
153 aggregateFlags() {
154 return this.powerFlags;
155 }
156
157 disaggregateFlags(flags) {
158 this.powerFlags = flags;
159 }
160
161 getFen() {
162 return super.getFen() + " " + this.getCapturedFen();
163 }
164
165 getFenForRepeat() {
166 return super.getFenForRepeat() + "_" + this.getCapturedFen();
167 }
168
169 getCapturedFen() {
170 let counts = [...Array(10).fill(0)];
171 let i = 0;
172 for (let p of [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.PAWN]) {
173 counts[i] = this.captured["w"][p];
174 counts[5 + i] = this.captured["b"][p];
175 i++;
176 }
177 return counts.join("");
178 }
179
180 setOtherVariables(fen) {
181 const fenParsed = V.ParseFen(fen);
182 // Initialize captured pieces' counts from FEN
183 this.captured = {
184 w: {
185 [V.ROOK]: parseInt(fenParsed.captured[0]),
186 [V.KNIGHT]: parseInt(fenParsed.captured[1]),
187 [V.BISHOP]: parseInt(fenParsed.captured[2]),
188 [V.QUEEN]: parseInt(fenParsed.captured[3]),
189 [V.PAWN]: parseInt(fenParsed.captured[4]),
190 },
191 b: {
192 [V.ROOK]: parseInt(fenParsed.captured[5]),
193 [V.KNIGHT]: parseInt(fenParsed.captured[6]),
194 [V.BISHOP]: parseInt(fenParsed.captured[7]),
195 [V.QUEEN]: parseInt(fenParsed.captured[8]),
196 [V.PAWN]: parseInt(fenParsed.captured[9]),
197 }
198 };
199 this.firstMove = [];
200 this.subTurn = 1;
201 }
202
203 getFlagsFen() {
204 let fen = "";
205 // Add power flags
206 for (let c of ["w", "b"])
207 for (let p of ['k', 'q']) fen += (this.powerFlags[c][p] ? "1" : "0");
208 return fen;
209 }
210
211 static get RESERVE_PIECES() {
212 return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
213 }
214
215 getReserveMoves([x, y]) {
216 const color = this.turn;
217 const p = V.RESERVE_PIECES[y];
218 if (this.reserve[color][p] == 0) return [];
219 let moves = [];
220 const start = (color == 'w' && p == V.PAWN ? 1 : 0);
221 const end = (color == 'b' && p == V.PAWN ? 7 : 8);
222 for (let i = start; i < end; i++) {
223 for (let j = 0; j < V.size.y; j++) {
224 if (this.board[i][j] == V.EMPTY) {
225 let mv = new Move({
226 appear: [
227 new PiPo({
228 x: i,
229 y: j,
230 c: color,
231 p: p
232 })
233 ],
234 vanish: [],
235 start: { x: x, y: y }, //a bit artificial...
236 end: { x: i, y: j }
237 });
238 moves.push(mv);
239 }
240 }
241 }
242 return moves;
243 }
244
245 getPotentialMovesFrom([x, y]) {
246 if (this.subTurn == 1) return super.getPotentialMovesFrom([x, y]);
247 if (this.subTurn == 2) {
248 let moves = [];
249 const L = this.firstMove.length;
250 const fm = this.firstMove[L-1];
251 switch (fm.end.effect) {
252 // case 0: a click is required (banana or bomb)
253 case 1:
254 // Exchange position with any piece
255 for (let i=0; i<8; i++) {
256 for (let j=0; j<8; j++) {
257 const colIJ = this.getColor(i, j);
258 if (
259 i != x &&
260 j != y &&
261 this.board[i][j] != V.EMPTY &&
262 colIJ != 'a'
263 ) {
264 const movedUnit = new PiPo({
265 x: x,
266 y: y,
267 c: colIJ,
268 p: this.getPiece(i, j)
269 });
270 let mMove = this.getBasicMove([x, y], [i, j]);
271 mMove.appear.push(movedUnit);
272 moves.push(mMove);
273 }
274 }
275 }
276 break;
277 case 2:
278 // Resurrect a captured piece
279 if (x >= V.size.x) moves = this.getReserveMoves([x, y]);
280 break;
281 case 3:
282 // Play again with the same piece
283 if (fm.end.x == x && fm.end.y == y)
284 moves = super.getPotentialMovesFrom([x, y]);
285 break;
286 }
287 return moves;
288 }
289 }
290
291 getBasicMove([x1, y1], [x2, y2], tr) {
292 // TODO: if this.subTurn == 2 :: no mushroom effect
293 // (first, transformation. then:)
294 // Apply mushroom, bomb or banana effect (hidden to the player).
295 // Determine egg effect, too, and apply its first part if possible.
296 // add egg + add mushroom for pawns.
297 let move = super.getBasicMove([x1, y1], [x2, y2]);
298 // TODO
299 return move;
300 // Infer move type based on its effects (used to decide subTurn 1 --> 2)
301 // --> impossible étant donné juste first part (egg --> effect?)
302 // => stocker l'effet (i, ii ou iii) dans le coup directement,
303 // Pas terrible, mais y'aura pas 36 variantes comme ça. Disons end.effect == 0, 1, 2 ou 3
304 // 0 => tour ou fou, pose potentielle.
305 // If queen can be invisible, add move same start + end but final type changes
306 // set move.end.effect (if subTurn --> 2)
307 }
308
309 getEnpassantCaptures([x, y], shiftX) {
310 const Lep = this.epSquares.length;
311 const epSquare = this.epSquares[Lep - 1]; //always at least one element
312 let enpassantMove = null;
313 if (
314 !!epSquare &&
315 epSquare.x == x + shiftX &&
316 Math.abs(epSquare.y - y) == 1
317 ) {
318 // Not using this.getBasicMove() because the mushroom has no effect
319 enpassantMove = super.getBasicMove([x, y], [epSquare.x, epSquare.y]);
320 enpassantMove.vanish.push({
321 x: x,
322 y: epSquare.y,
323 p: V.PAWN,
324 c: this.getColor(x, epSquare.y)
325 });
326 }
327 return !!enpassantMove ? [enpassantMove] : [];
328 }
329
330 getPotentialQueenMoves(sq) {
331 const normalMoves = super.getPotentialQueenMoves(sq);
332 // If flag allows it, add 'invisible movements'
333 let invisibleMoves = [];
334 if (this.powerFlags[this.turn][V.QUEEN]) {
335 normalMoves.forEach(m => {
336 if (m.vanish.length == 1) {
337 let im = JSON.parse(JSON.stringify(m));
338 m.appear[0].p = V.INVISIBLE_QUEEN;
339 invisibleMoves.push(im);
340 }
341 });
342 }
343 return normalMoves.concat(invisibleMoves);
344 }
345
346 getPotentialKingMoves([x, y]) {
347 let moves = super.getPotentialKingMoves([x, y]);
348 const color = this.turn;
349 // If flag allows it, add 'remote shell captures'
350 if (this.powerFlags[this.turn][V.KING]) {
351 V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
352 let [i, j] = [x + 2 * step[0], y + 2 * step[1]];
353 while (
354 V.OnBoard(i, j) &&
355 (
356 this.board[i][j] == V.EMPTY ||
357 (
358 this.getColor(i, j) == 'a' &&
359 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
360 )
361 )
362 ) {
363 i += step[0];
364 j += step[1];
365 }
366 if (V.OnBoard(i, j) && this.getColor(i, j) != color)
367 // May just destroy a bomb or banana:
368 moves.push(this.getBasicMove([x, y], [i, j]));
369 });
370 }
371 return moves;
372 }
373
374 getSlideNJumpMoves([x, y], steps, oneStep) {
375 let moves = [];
376 outerLoop: for (let step of steps) {
377 let i = x + step[0];
378 let j = y + step[1];
379 while (
380 V.OnBoard(i, j) &&
381 (
382 this.board[i][j] == V.EMPTY ||
383 (
384 this.getColor(i, j) == 'a' &&
385 [V.EGG, V.MUSHROOM].includes(this.getPiece(i, j))
386 )
387 )
388 ) {
389 moves.push(this.getBasicMove([x, y], [i, j]));
390 if (oneStep) continue outerLoop;
391 i += step[0];
392 j += step[1];
393 }
394 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
395 moves.push(this.getBasicMove([x, y], [i, j]));
396 }
397 return moves;
398 }
399
400 getAllPotentialMoves() {
401 if (this.subTurn == 1) return super.getAllPotentialMoves();
402 let moves = [];
403 const L = this.firstMove.length;
404 const fm = this.firstMove[L-1];
405 //switch (fm.end.effect) {
406 // case 0: //...
407 }
408
409 doClick(square) {
410 if (isNaN(square[0])) return null;
411 if (this.subTurn == 1) return null;
412 const L = this.firstMove.length;
413 const fm = this.firstMove[L-1];
414 if (fm.end.effect != 0) return null;
415 const [x, y] = [square[0], square[1]];
416 const deltaX = Math.abs(fm.end.x - x);
417 const deltaY = Math.abs(fm.end.y - y);
418 if (deltaX == 0 && deltaY == 0) {
419 // Empty move:
420 return {
421 start: { x: -1, y: -1 },
422 end: { x: -1, y: -1 },
423 appear: [],
424 vanish: []
425 };
426 }
427 if (
428 this.board[x][y] == V.EMPTY &&
429 (
430 (fm.vanish[0].p == V.ROOK && deltaX == 1 && deltaY == 1) ||
431 (fm.vanish[0].p == V.BISHOP && deltaX + deltaY == 1)
432 )
433 ) {
434 return new Move({
435 start: { x: -1, y: -1 },
436 end: { x: x, y: y },
437 appear: [
438 new PiPo({
439 x: x,
440 y: y,
441 c: 'a',
442 p: (fm.vanish[0].p == V.ROOK ? V.BANANA : V.BOMB)
443 })
444 ],
445 vanish: []
446 });
447 }
448 return null;
449 }
450
451 play(move) {
452 move.flags = JSON.stringify(this.aggregateFlags());
453 this.epSquares.push(this.getEpSquare(move));
454 V.PlayOnBoard(this.board, move);
455 if (move.end.effect !== undefined) {
456 this.firstMove.push(move);
457 this.subTurn = 2;
458 if (move.end.effect == 2) this.reserve = this.captured;
459 }
460 else {
461 this.turn = V.GetOppCol(this.turn);
462 this.subTurn = 1;
463 this.reserve = null;
464 }
465 }
466
467 postPlay(move) {
468 if (move.vanish[0].p == V.KING) { }
469 //si roi et delta >= 2 ou dame et appear invisible queen : turn flag off
470 if (move.vanish.length == 2 && move.vanish[1].c != 'a')
471 // Capture: update this.captured
472 this.captured[move.vanish[1].c][move.vanish[1].p]++;
473 else if (move.vanish.length == 0) {
474 // A piece is back on board
475 this.captured[move.vanish[1].c][move.vanish[1].p]++;
476 this.reserve = null;
477 }
478 // si pièce immobilisée de ma couleur : elle redevient utilisable (changer status fin de play)
479 // TODO: un-immobilize my formerly immobilized piece, if any.
480 // Make invisible queen visible again, if any opponent invisible queen.
481 }
482
483 undo(move) {
484 // TODO: should be easy once end.effect is set in getBasicMove()
485 if (move.end.effect !== undefined)
486 this.firstMove.pop();
487 }
488
489 postUndo(move) {
490 if (move.vanish.length == 2 && move.vanish[1].c != 'a')
491 this.captured[move.vanish[1].c][move.vanish[1].p]--;
492 }
493
494 getCheckSquares() {
495 return [];
496 }
497
498 getCurrentScore() {
499 // Find kings (not tracked in this variant)
500 let kingThere = { w: false, b: false };
501 for (let i=0; i<8; i++) {
502 for (let j=0; j<8; j++) {
503 if (this.board[i][j] != V.EMPTY && this.getPiece(i, j) == V.KING)
504 kingThere[this.getColor(i, j)] = true;
505 }
506 }
507 if (!kingThere['w']) return "0-1";
508 if (!kingThere['b']) return "1-0";
509 return "*";
510 }
511
512 static GenRandInitFen(randomness) {
513 return (
514 SuicideRules.GenRandInitFen(randomness).slice(0, -1) +
515 // Add Peach + Mario flags, re-add en-passant + capture counts
516 "0000 - 0000000000"
517 );
518 }
519
520 filterValid(moves) {
521 return moves;
522 }
523
524 getComputerMove() {
525 // Random mover:
526 const moves = this.getAllValidMoves();
527 let move1 = moves[randInt(movs.length)];
528 this.play(move1);
529 let move2 = undefined;
530 if (this.subTurn == 2) {
531 const moves2 = this.getAllValidMoves();
532 move2 = moves2[randInt(moves2.length)];
533 }
534 this.undo(move1);
535 if (!move2) return move1;
536 return [move1, move2];
537 }
538
539 getNotation(move) {
540 // TODO: invisibility used => move notation Q??
541 // Also, bonus should be clearly indicated + bomb/bananas locations
542 return super.getNotation(move);
543 }
544 };