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