Almost added TitanChess + EvolutionChess
[vchess.git] / client / src / variants / Baroque.js
1 import { ChessRules, PiPo, Move } from "@/base_rules";
2 import { ArrayFun } from "@/utils/array";
3 import { shuffle } from "@/utils/alea";
4
5 export class BaroqueRules extends ChessRules {
6
7 static get HasFlags() {
8 return false;
9 }
10
11 static get HasEnpassant() {
12 return false;
13 }
14
15 static get PIECES() {
16 return ChessRules.PIECES.concat([V.IMMOBILIZER]);
17 }
18
19 getPpath(b) {
20 if (b[1] == "m")
21 //'m' for Immobilizer (I is too similar to 1)
22 return "Baroque/" + b;
23 return b; //usual piece
24 }
25
26 // No castling, but checks, so keep track of kings
27 setOtherVariables(fen) {
28 this.kingPos = { w: [-1, -1], b: [-1, -1] };
29 const fenParts = fen.split(" ");
30 const position = fenParts[0].split("/");
31 for (let i = 0; i < position.length; i++) {
32 let k = 0;
33 for (let j = 0; j < position[i].length; j++) {
34 switch (position[i].charAt(j)) {
35 case "k":
36 this.kingPos["b"] = [i, k];
37 break;
38 case "K":
39 this.kingPos["w"] = [i, k];
40 break;
41 default: {
42 const num = parseInt(position[i].charAt(j), 10);
43 if (!isNaN(num)) k += num - 1;
44 }
45 }
46 k++;
47 }
48 }
49 }
50
51 static get IMMOBILIZER() {
52 return "m";
53 }
54 // Although other pieces keep their names here for coding simplicity,
55 // keep in mind that:
56 // - a "rook" is a coordinator, capturing by coordinating with the king
57 // - a "knight" is a long-leaper, capturing as in draughts
58 // - a "bishop" is a chameleon, capturing as its prey
59 // - a "queen" is a withdrawer, capturing by moving away from pieces
60
61 // Is piece on square (x,y) immobilized?
62 isImmobilized([x, y]) {
63 const piece = this.getPiece(x, y);
64 const color = this.getColor(x, y);
65 const oppCol = V.GetOppCol(color);
66 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
67 for (let step of adjacentSteps) {
68 const [i, j] = [x + step[0], y + step[1]];
69 if (
70 V.OnBoard(i, j) &&
71 this.board[i][j] != V.EMPTY &&
72 this.getColor(i, j) == oppCol
73 ) {
74 const oppPiece = this.getPiece(i, j);
75 if (oppPiece == V.IMMOBILIZER) {
76 // Moving is possible only if this immobilizer is neutralized
77 for (let step2 of adjacentSteps) {
78 const [i2, j2] = [i + step2[0], j + step2[1]];
79 if (i2 == x && j2 == y) continue; //skip initial piece!
80 if (
81 V.OnBoard(i2, j2) &&
82 this.board[i2][j2] != V.EMPTY &&
83 this.getColor(i2, j2) == color
84 ) {
85 if ([V.BISHOP, V.IMMOBILIZER].includes(this.getPiece(i2, j2)))
86 return false;
87 }
88 }
89 return true; //immobilizer isn't neutralized
90 }
91 // Chameleons can't be immobilized twice,
92 // because there is only one immobilizer
93 if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) return true;
94 }
95 }
96 return false;
97 }
98
99 getPotentialMovesFrom([x, y]) {
100 // Pre-check: is thing on this square immobilized?
101 if (this.isImmobilized([x, y])) return [];
102 switch (this.getPiece(x, y)) {
103 case V.IMMOBILIZER:
104 return this.getPotentialImmobilizerMoves([x, y]);
105 default:
106 return super.getPotentialMovesFrom([x, y]);
107 }
108 }
109
110 getSlideNJumpMoves([x, y], steps, oneStep) {
111 const piece = this.getPiece(x, y);
112 let moves = [];
113 outerLoop: for (let step of steps) {
114 let i = x + step[0];
115 let j = y + step[1];
116 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
117 moves.push(this.getBasicMove([x, y], [i, j]));
118 if (oneStep !== undefined) continue outerLoop;
119 i += step[0];
120 j += step[1];
121 }
122 // Only king can take on occupied square:
123 if (piece == V.KING && V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
124 moves.push(this.getBasicMove([x, y], [i, j]));
125 }
126 return moves;
127 }
128
129 // Modify capturing moves among listed pawn moves
130 addPawnCaptures(moves, byChameleon) {
131 const steps = V.steps[V.ROOK];
132 const color = this.turn;
133 const oppCol = V.GetOppCol(color);
134 moves.forEach(m => {
135 if (!!byChameleon && m.start.x != m.end.x && m.start.y != m.end.y)
136 // Chameleon not moving as pawn
137 return;
138 // Try capturing in every direction
139 for (let step of steps) {
140 const sq2 = [m.end.x + 2 * step[0], m.end.y + 2 * step[1]];
141 if (
142 V.OnBoard(sq2[0], sq2[1]) &&
143 this.board[sq2[0]][sq2[1]] != V.EMPTY &&
144 this.getColor(sq2[0], sq2[1]) == color
145 ) {
146 // Potential capture
147 const sq1 = [m.end.x + step[0], m.end.y + step[1]];
148 if (
149 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
150 this.getColor(sq1[0], sq1[1]) == oppCol
151 ) {
152 const piece1 = this.getPiece(sq1[0], sq1[1]);
153 if (!byChameleon || piece1 == V.PAWN) {
154 m.vanish.push(
155 new PiPo({
156 x: sq1[0],
157 y: sq1[1],
158 c: oppCol,
159 p: piece1
160 })
161 );
162 }
163 }
164 }
165 }
166 });
167 }
168
169 // "Pincer"
170 getPotentialPawnMoves([x, y]) {
171 let moves = super.getPotentialRookMoves([x, y]);
172 this.addPawnCaptures(moves);
173 return moves;
174 }
175
176 addRookCaptures(moves, byChameleon) {
177 const color = this.turn;
178 const oppCol = V.GetOppCol(color);
179 const kp = this.kingPos[color];
180 moves.forEach(m => {
181 // Check piece-king rectangle (if any) corners for enemy pieces
182 if (m.end.x == kp[0] || m.end.y == kp[1]) return; //"flat rectangle"
183 const corner1 = [m.end.x, kp[1]];
184 const corner2 = [kp[0], m.end.y];
185 for (let [i, j] of [corner1, corner2]) {
186 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == oppCol) {
187 const piece = this.getPiece(i, j);
188 if (!byChameleon || piece == V.ROOK) {
189 m.vanish.push(
190 new PiPo({
191 x: i,
192 y: j,
193 p: piece,
194 c: oppCol
195 })
196 );
197 }
198 }
199 }
200 });
201 }
202
203 // Coordinator
204 getPotentialRookMoves(sq) {
205 let moves = super.getPotentialQueenMoves(sq);
206 this.addRookCaptures(moves);
207 return moves;
208 }
209
210 getKnightCaptures(startSquare, byChameleon) {
211 // Look in every direction for captures
212 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
213 const color = this.turn;
214 const oppCol = V.GetOppCol(color);
215 let moves = [];
216 const [x, y] = [startSquare[0], startSquare[1]];
217 const piece = this.getPiece(x, y); //might be a chameleon!
218 outerLoop: for (let step of steps) {
219 let [i, j] = [x + step[0], y + step[1]];
220 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
221 i += step[0];
222 j += step[1];
223 }
224 if (
225 !V.OnBoard(i, j) ||
226 this.getColor(i, j) == color ||
227 (!!byChameleon && this.getPiece(i, j) != V.KNIGHT)
228 ) {
229 continue;
230 }
231 // last(thing), cur(thing) : stop if "cur" is our color,
232 // or beyond board limits, or if "last" isn't empty and cur neither.
233 // Otherwise, if cur is empty then add move until cur square;
234 // if cur is occupied then stop if !!byChameleon and the square not
235 // occupied by a leaper.
236 let last = [i, j];
237 let cur = [i + step[0], j + step[1]];
238 let vanished = [new PiPo({ x: x, y: y, c: color, p: piece })];
239 while (V.OnBoard(cur[0], cur[1])) {
240 if (this.board[last[0]][last[1]] != V.EMPTY) {
241 const oppPiece = this.getPiece(last[0], last[1]);
242 if (!!byChameleon && oppPiece != V.KNIGHT) continue outerLoop;
243 // Something to eat:
244 vanished.push(
245 new PiPo({ x: last[0], y: last[1], c: oppCol, p: oppPiece })
246 );
247 }
248 if (this.board[cur[0]][cur[1]] != V.EMPTY) {
249 if (
250 this.getColor(cur[0], cur[1]) == color ||
251 this.board[last[0]][last[1]] != V.EMPTY
252 ) {
253 //TODO: redundant test
254 continue outerLoop;
255 }
256 } else {
257 moves.push(
258 new Move({
259 appear: [new PiPo({ x: cur[0], y: cur[1], c: color, p: piece })],
260 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
261 start: { x: x, y: y },
262 end: { x: cur[0], y: cur[1] }
263 })
264 );
265 }
266 last = [last[0] + step[0], last[1] + step[1]];
267 cur = [cur[0] + step[0], cur[1] + step[1]];
268 }
269 }
270 return moves;
271 }
272
273 // Long-leaper
274 getPotentialKnightMoves(sq) {
275 return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
276 }
277
278 // Chameleon
279 getPotentialBishopMoves([x, y]) {
280 let moves = super
281 .getPotentialQueenMoves([x, y])
282 .concat(this.getKnightCaptures([x, y], "asChameleon"));
283 // No "king capture" because king cannot remain under check
284 this.addPawnCaptures(moves, "asChameleon");
285 this.addRookCaptures(moves, "asChameleon");
286 this.addQueenCaptures(moves, "asChameleon");
287 // Post-processing: merge similar moves, concatenating vanish arrays
288 let mergedMoves = {};
289 moves.forEach(m => {
290 const key = m.end.x + V.size.x * m.end.y;
291 if (!mergedMoves[key]) mergedMoves[key] = m;
292 else {
293 for (let i = 1; i < m.vanish.length; i++)
294 mergedMoves[key].vanish.push(m.vanish[i]);
295 }
296 });
297 return Object.values(mergedMoves);
298 }
299
300 addQueenCaptures(moves, byChameleon) {
301 if (moves.length == 0) return;
302 const [x, y] = [moves[0].start.x, moves[0].start.y];
303 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
304 let capturingDirections = [];
305 const color = this.turn;
306 const oppCol = V.GetOppCol(color);
307 adjacentSteps.forEach(step => {
308 const [i, j] = [x + step[0], y + step[1]];
309 if (
310 V.OnBoard(i, j) &&
311 this.board[i][j] != V.EMPTY &&
312 this.getColor(i, j) == oppCol &&
313 (!byChameleon || this.getPiece(i, j) == V.QUEEN)
314 ) {
315 capturingDirections.push(step);
316 }
317 });
318 moves.forEach(m => {
319 const step = [
320 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
321 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
322 ];
323 // NOTE: includes() and even _.isEqual() functions fail...
324 // TODO: this test should be done only once per direction
325 if (
326 capturingDirections.some(dir => {
327 return dir[0] == -step[0] && dir[1] == -step[1];
328 })
329 ) {
330 const [i, j] = [x - step[0], y - step[1]];
331 m.vanish.push(
332 new PiPo({
333 x: i,
334 y: j,
335 p: this.getPiece(i, j),
336 c: oppCol
337 })
338 );
339 }
340 });
341 }
342
343 // Withdrawer
344 getPotentialQueenMoves(sq) {
345 let moves = super.getPotentialQueenMoves(sq);
346 this.addQueenCaptures(moves);
347 return moves;
348 }
349
350 getPotentialImmobilizerMoves(sq) {
351 // Immobilizer doesn't capture
352 return super.getPotentialQueenMoves(sq);
353 }
354
355 // isAttacked() is OK because the immobilizer doesn't take
356
357 isAttackedByPawn([x, y], color) {
358 // Square (x,y) must be surroundable by two enemy pieces,
359 // and one of them at least should be a pawn (moving).
360 const dirs = [
361 [1, 0],
362 [0, 1]
363 ];
364 const steps = V.steps[V.ROOK];
365 for (let dir of dirs) {
366 const [i1, j1] = [x - dir[0], y - dir[1]]; //"before"
367 const [i2, j2] = [x + dir[0], y + dir[1]]; //"after"
368 if (V.OnBoard(i1, j1) && V.OnBoard(i2, j2)) {
369 if (
370 (
371 this.board[i1][j1] != V.EMPTY &&
372 this.getColor(i1, j1) == color &&
373 this.board[i2][j2] == V.EMPTY
374 )
375 ||
376 (
377 this.board[i2][j2] != V.EMPTY &&
378 this.getColor(i2, j2) == color &&
379 this.board[i1][j1] == V.EMPTY
380 )
381 ) {
382 // Search a movable enemy pawn landing on the empty square
383 for (let step of steps) {
384 let [ii, jj] = this.board[i1][j1] == V.EMPTY ? [i1, j1] : [i2, j2];
385 let [i3, j3] = [ii + step[0], jj + step[1]];
386 while (V.OnBoard(i3, j3) && this.board[i3][j3] == V.EMPTY) {
387 i3 += step[0];
388 j3 += step[1];
389 }
390 if (
391 V.OnBoard(i3, j3) &&
392 this.getColor(i3, j3) == color &&
393 this.getPiece(i3, j3) == V.PAWN &&
394 !this.isImmobilized([i3, j3])
395 ) {
396 return true;
397 }
398 }
399 }
400 }
401 }
402 return false;
403 }
404
405 isAttackedByRook([x, y], color) {
406 // King must be on same column or row,
407 // and a rook should be able to reach a capturing square
408 const sameRow = x == this.kingPos[color][0];
409 const sameColumn = y == this.kingPos[color][1];
410 if (sameRow || sameColumn) {
411 // Look for the enemy rook (maximum 1)
412 for (let i = 0; i < V.size.x; i++) {
413 for (let j = 0; j < V.size.y; j++) {
414 if (
415 this.board[i][j] != V.EMPTY &&
416 this.getColor(i, j) == color &&
417 this.getPiece(i, j) == V.ROOK
418 ) {
419 if (this.isImmobilized([i, j]))
420 // Because only one rook:
421 return false;
422 // Can it reach a capturing square? Easy but quite suboptimal way
423 // (TODO: generate all moves (turn is OK))
424 const moves = this.getPotentialMovesFrom([i, j]);
425 for (let move of moves) {
426 if (
427 (sameRow && move.end.y == y) ||
428 (sameColumn && move.end.x == x)
429 ) {
430 return true;
431 }
432 }
433 }
434 }
435 }
436 }
437 return false;
438 }
439
440 isAttackedByKnight([x, y], color) {
441 // Square (x,y) must be on same line as a knight,
442 // and there must be empty square(s) behind.
443 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
444 outerLoop: for (let step of steps) {
445 const [i0, j0] = [x + step[0], y + step[1]];
446 if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) {
447 // Try in opposite direction:
448 let [i, j] = [x - step[0], y - step[1]];
449 while (V.OnBoard(i, j)) {
450 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
451 i -= step[0];
452 j -= step[1];
453 }
454 if (V.OnBoard(i, j)) {
455 if (this.getColor(i, j) == color) {
456 if (
457 this.getPiece(i, j) == V.KNIGHT &&
458 !this.isImmobilized([i, j])
459 ) {
460 return true;
461 }
462 continue outerLoop;
463 }
464 // [else] Our color,
465 // could be captured *if there was an empty space*
466 if (this.board[i + step[0]][j + step[1]] != V.EMPTY)
467 continue outerLoop;
468 i -= step[0];
469 j -= step[1];
470 }
471 }
472 }
473 }
474 return false;
475 }
476
477 isAttackedByBishop([x, y], color) {
478 // We cheat a little here: since this function is used exclusively for
479 // the king, it's enough to check the immediate surrounding of the square.
480 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
481 for (let step of adjacentSteps) {
482 const [i, j] = [x + step[0], y + step[1]];
483 if (
484 V.OnBoard(i, j) &&
485 this.board[i][j] != V.EMPTY &&
486 this.getColor(i, j) == color &&
487 this.getPiece(i, j) == V.BISHOP &&
488 !this.isImmobilized([i, j])
489 ) {
490 return true;
491 }
492 }
493 return false;
494 }
495
496 isAttackedByQueen([x, y], color) {
497 // Square (x,y) must be adjacent to a queen, and the queen must have
498 // some free space in the opposite direction from (x,y)
499 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
500 for (let step of adjacentSteps) {
501 const sq2 = [x + 2 * step[0], y + 2 * step[1]];
502 if (V.OnBoard(sq2[0], sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) {
503 const sq1 = [x + step[0], y + step[1]];
504 if (
505 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
506 this.getColor(sq1[0], sq1[1]) == color &&
507 this.getPiece(sq1[0], sq1[1]) == V.QUEEN &&
508 !this.isImmobilized(sq1)
509 ) {
510 return true;
511 }
512 }
513 }
514 return false;
515 }
516
517 isAttackedByKing([x, y], color) {
518 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
519 for (let step of steps) {
520 let rx = x + step[0],
521 ry = y + step[1];
522 if (
523 V.OnBoard(rx, ry) &&
524 this.getPiece(rx, ry) === V.KING &&
525 this.getColor(rx, ry) == color &&
526 !this.isImmobilized([rx, ry])
527 ) {
528 return true;
529 }
530 }
531 return false;
532 }
533
534 static GenRandInitFen(randomness) {
535 if (randomness == 0)
536 // Deterministic:
537 return "rnbkqbnm/pppppppp/8/8/8/8/PPPPPPPP/MNBQKBNR w 0";
538
539 let pieces = { w: new Array(8), b: new Array(8) };
540 // Shuffle pieces on first and last rank
541 for (let c of ["w", "b"]) {
542 if (c == 'b' && randomness == 1) {
543 pieces['b'] = pieces['w'];
544 break;
545 }
546
547 // Get random squares for every piece, totally freely
548 let positions = shuffle(ArrayFun.range(8));
549 const composition = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'm'];
550 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
551 }
552 return (
553 pieces["b"].join("") +
554 "/pppppppp/8/8/8/8/PPPPPPPP/" +
555 pieces["w"].join("").toUpperCase() +
556 " w 0"
557 );
558 }
559
560 static get VALUES() {
561 return {
562 p: 1,
563 r: 2,
564 n: 5,
565 b: 3,
566 q: 3,
567 m: 5,
568 k: 1000
569 };
570 }
571
572 static get SEARCH_DEPTH() {
573 return 2;
574 }
575
576 getNotation(move) {
577 const initialSquare = V.CoordsToSquare(move.start);
578 const finalSquare = V.CoordsToSquare(move.end);
579 let notation = undefined;
580 if (move.appear[0].p == V.PAWN) {
581 // Pawn: generally ambiguous short notation, so we use full description
582 notation = "P" + initialSquare + finalSquare;
583 } else if (move.appear[0].p == V.KING)
584 notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
585 else notation = move.appear[0].p.toUpperCase() + finalSquare;
586 // Add a capture mark (not describing what is captured...):
587 if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
588 return notation;
589 }
590
591 };