74617cfbf1666cb8e674f087c6b99d8e78a3f46e
[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));
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 return false;
436 }
437
438 isAttackedByKnight([x, y], color) {
439 // Square (x,y) must be on same line as a knight,
440 // and there must be empty square(s) behind.
441 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
442 outerLoop: for (let step of steps) {
443 const [i0, j0] = [x + step[0], y + step[1]];
444 if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) {
445 // Try in opposite direction:
446 let [i, j] = [x - step[0], y - step[1]];
447 while (V.OnBoard(i, j)) {
448 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
449 i -= step[0];
450 j -= step[1];
451 }
452 if (V.OnBoard(i, j)) {
453 if (this.getColor(i, j) == color) {
454 if (
455 this.getPiece(i, j) == V.KNIGHT &&
456 !this.isImmobilized([i, j])
457 )
458 return true;
459 continue outerLoop;
460 }
461 // [else] Our color,
462 // could be captured *if there was an empty space*
463 if (this.board[i + step[0]][j + step[1]] != V.EMPTY)
464 continue outerLoop;
465 i -= step[0];
466 j -= step[1];
467 }
468 }
469 }
470 }
471 return false;
472 }
473
474 isAttackedByBishop([x, y], color) {
475 // We cheat a little here: since this function is used exclusively for
476 // the king, it's enough to check the immediate surrounding of the square.
477 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
478 for (let step of adjacentSteps) {
479 const [i, j] = [x + step[0], y + step[1]];
480 if (
481 V.OnBoard(i, j) &&
482 this.board[i][j] != V.EMPTY &&
483 this.getColor(i, j) == color &&
484 this.getPiece(i, j) == V.BISHOP &&
485 !this.isImmobilized([i, j])
486 ) {
487 return true;
488 }
489 }
490 return false;
491 }
492
493 isAttackedByQueen([x, y], color) {
494 // Square (x,y) must be adjacent to a queen, and the queen must have
495 // some free space in the opposite direction from (x,y)
496 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
497 for (let step of adjacentSteps) {
498 const sq2 = [x + 2 * step[0], y + 2 * step[1]];
499 if (V.OnBoard(sq2[0], sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) {
500 const sq1 = [x + step[0], y + step[1]];
501 if (
502 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
503 this.getColor(sq1[0], sq1[1]) == color &&
504 this.getPiece(sq1[0], sq1[1]) == V.QUEEN &&
505 !this.isImmobilized(sq1)
506 ) {
507 return true;
508 }
509 }
510 }
511 return false;
512 }
513
514 isAttackedByKing([x, y], color) {
515 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
516 for (let step of steps) {
517 let rx = x + step[0],
518 ry = y + step[1];
519 if (
520 V.OnBoard(rx, ry) &&
521 this.getPiece(rx, ry) === V.KING &&
522 this.getColor(rx, ry) == color &&
523 !this.isImmobilized([rx, ry])
524 ) {
525 return true;
526 }
527 }
528 return false;
529 }
530
531 static GenRandInitFen(randomness) {
532 if (randomness == 0)
533 // Deterministic:
534 return "rnbkqbnm/pppppppp/8/8/8/8/PPPPPPPP/MNBQKBNR w 0";
535
536 let pieces = { w: new Array(8), b: new Array(8) };
537 // Shuffle pieces on first and last rank
538 for (let c of ["w", "b"]) {
539 if (c == 'b' && randomness == 1) {
540 pieces['b'] = pieces['w'];
541 break;
542 }
543
544 // Get random squares for every piece, totally freely
545 let positions = shuffle(ArrayFun.range(8));
546 const composition = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'm'];
547 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
548 }
549 return (
550 pieces["b"].join("") +
551 "/pppppppp/8/8/8/8/PPPPPPPP/" +
552 pieces["w"].join("").toUpperCase() +
553 " w 0"
554 );
555 }
556
557 static get VALUES() {
558 return {
559 p: 1,
560 r: 2,
561 n: 5,
562 b: 3,
563 q: 3,
564 m: 5,
565 k: 1000
566 };
567 }
568
569 static get SEARCH_DEPTH() {
570 return 2;
571 }
572
573 getNotation(move) {
574 const initialSquare = V.CoordsToSquare(move.start);
575 const finalSquare = V.CoordsToSquare(move.end);
576 let notation = undefined;
577 if (move.appear[0].p == V.PAWN) {
578 // Pawn: generally ambiguous short notation, so we use full description
579 notation = "P" + initialSquare + finalSquare;
580 } else if (move.appear[0].p == V.KING)
581 notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
582 else notation = move.appear[0].p.toUpperCase() + finalSquare;
583 // Add a capture mark (not describing what is captured...):
584 if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
585 return notation;
586 }
587 };