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