Improve FEN length for Eightpieces 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));
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 // Finally return an array
297 moves = [];
298 Object.keys(mergedMoves).forEach(k => {
299 moves.push(mergedMoves[k]);
300 });
301 return moves;
302 }
303
304 addQueenCaptures(moves, byChameleon) {
305 if (moves.length == 0) return;
306 const [x, y] = [moves[0].start.x, moves[0].start.y];
307 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
308 let capturingDirections = [];
309 const color = this.turn;
310 const oppCol = V.GetOppCol(color);
311 adjacentSteps.forEach(step => {
312 const [i, j] = [x + step[0], y + step[1]];
313 if (
314 V.OnBoard(i, j) &&
315 this.board[i][j] != V.EMPTY &&
316 this.getColor(i, j) == oppCol &&
317 (!byChameleon || this.getPiece(i, j) == V.QUEEN)
318 ) {
319 capturingDirections.push(step);
320 }
321 });
322 moves.forEach(m => {
323 const step = [
324 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
325 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
326 ];
327 // NOTE: includes() and even _.isEqual() functions fail...
328 // TODO: this test should be done only once per direction
329 if (
330 capturingDirections.some(dir => {
331 return dir[0] == -step[0] && dir[1] == -step[1];
332 })
333 ) {
334 const [i, j] = [x - step[0], y - step[1]];
335 m.vanish.push(
336 new PiPo({
337 x: i,
338 y: j,
339 p: this.getPiece(i, j),
340 c: oppCol
341 })
342 );
343 }
344 });
345 }
346
347 // Withdrawer
348 getPotentialQueenMoves(sq) {
349 let moves = super.getPotentialQueenMoves(sq);
350 this.addQueenCaptures(moves);
351 return moves;
352 }
353
354 getPotentialImmobilizerMoves(sq) {
355 // Immobilizer doesn't capture
356 return super.getPotentialQueenMoves(sq);
357 }
358
359 // isAttacked() is OK because the immobilizer doesn't take
360
361 isAttackedByPawn([x, y], color) {
362 // Square (x,y) must be surroundable by two enemy pieces,
363 // and one of them at least should be a pawn (moving).
364 const dirs = [
365 [1, 0],
366 [0, 1]
367 ];
368 const steps = V.steps[V.ROOK];
369 for (let dir of dirs) {
370 const [i1, j1] = [x - dir[0], y - dir[1]]; //"before"
371 const [i2, j2] = [x + dir[0], y + dir[1]]; //"after"
372 if (V.OnBoard(i1, j1) && V.OnBoard(i2, j2)) {
373 if (
374 (
375 this.board[i1][j1] != V.EMPTY &&
376 this.getColor(i1, j1) == color &&
377 this.board[i2][j2] == V.EMPTY
378 )
379 ||
380 (
381 this.board[i2][j2] != V.EMPTY &&
382 this.getColor(i2, j2) == color &&
383 this.board[i1][j1] == V.EMPTY
384 )
385 ) {
386 // Search a movable enemy pawn landing on the empty square
387 for (let step of steps) {
388 let [ii, jj] = this.board[i1][j1] == V.EMPTY ? [i1, j1] : [i2, j2];
389 let [i3, j3] = [ii + step[0], jj + step[1]];
390 while (V.OnBoard(i3, j3) && this.board[i3][j3] == V.EMPTY) {
391 i3 += step[0];
392 j3 += step[1];
393 }
394 if (
395 V.OnBoard(i3, j3) &&
396 this.getColor(i3, j3) == color &&
397 this.getPiece(i3, j3) == V.PAWN &&
398 !this.isImmobilized([i3, j3])
399 ) {
400 return true;
401 }
402 }
403 }
404 }
405 }
406 return false;
407 }
408
409 isAttackedByRook([x, y], color) {
410 // King must be on same column or row,
411 // and a rook should be able to reach a capturing square
412 const sameRow = x == this.kingPos[color][0];
413 const sameColumn = y == this.kingPos[color][1];
414 if (sameRow || sameColumn) {
415 // Look for the enemy rook (maximum 1)
416 for (let i = 0; i < V.size.x; i++) {
417 for (let j = 0; j < V.size.y; j++) {
418 if (
419 this.board[i][j] != V.EMPTY &&
420 this.getColor(i, j) == color &&
421 this.getPiece(i, j) == V.ROOK
422 ) {
423 if (this.isImmobilized([i, j]))
424 // Because only one rook:
425 return false;
426 // Can it reach a capturing square? Easy but quite suboptimal way
427 // (TODO: generate all moves (turn is OK))
428 const moves = this.getPotentialMovesFrom([i, j]);
429 for (let move of moves) {
430 if (
431 (sameRow && move.end.y == y) ||
432 (sameColumn && move.end.x == x)
433 )
434 return true;
435 }
436 }
437 }
438 }
439 }
440 return false;
441 }
442
443 isAttackedByKnight([x, y], color) {
444 // Square (x,y) must be on same line as a knight,
445 // and there must be empty square(s) behind.
446 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
447 outerLoop: for (let step of steps) {
448 const [i0, j0] = [x + step[0], y + step[1]];
449 if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) {
450 // Try in opposite direction:
451 let [i, j] = [x - step[0], y - step[1]];
452 while (V.OnBoard(i, j)) {
453 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
454 i -= step[0];
455 j -= step[1];
456 }
457 if (V.OnBoard(i, j)) {
458 if (this.getColor(i, j) == color) {
459 if (
460 this.getPiece(i, j) == V.KNIGHT &&
461 !this.isImmobilized([i, j])
462 )
463 return true;
464 continue outerLoop;
465 }
466 // [else] Our color,
467 // could be captured *if there was an empty space*
468 if (this.board[i + step[0]][j + step[1]] != V.EMPTY)
469 continue outerLoop;
470 i -= step[0];
471 j -= step[1];
472 }
473 }
474 }
475 }
476 return false;
477 }
478
479 isAttackedByBishop([x, y], color) {
480 // We cheat a little here: since this function is used exclusively for
481 // the king, it's enough to check the immediate surrounding of the square.
482 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
483 for (let step of adjacentSteps) {
484 const [i, j] = [x + step[0], y + step[1]];
485 if (
486 V.OnBoard(i, j) &&
487 this.board[i][j] != V.EMPTY &&
488 this.getColor(i, j) == color &&
489 this.getPiece(i, j) == V.BISHOP
490 ) {
491 return true; //bishops are never immobilized
492 }
493 }
494 return false;
495 }
496
497 isAttackedByQueen([x, y], color) {
498 // Square (x,y) must be adjacent to a queen, and the queen must have
499 // some free space in the opposite direction from (x,y)
500 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
501 for (let step of adjacentSteps) {
502 const sq2 = [x + 2 * step[0], y + 2 * step[1]];
503 if (V.OnBoard(sq2[0], sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) {
504 const sq1 = [x + step[0], y + step[1]];
505 if (
506 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
507 this.getColor(sq1[0], sq1[1]) == color &&
508 this.getPiece(sq1[0], sq1[1]) == V.QUEEN &&
509 !this.isImmobilized(sq1)
510 ) {
511 return true;
512 }
513 }
514 }
515 return false;
516 }
517
518 isAttackedByKing([x, y], color) {
519 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
520 for (let step of steps) {
521 let rx = x + step[0],
522 ry = y + step[1];
523 if (
524 V.OnBoard(rx, ry) &&
525 this.getPiece(rx, ry) === V.KING &&
526 this.getColor(rx, ry) == color &&
527 !this.isImmobilized([rx, ry])
528 ) {
529 return true;
530 }
531 }
532 return false;
533 }
534
535 static get VALUES() {
536 return {
537 p: 1,
538 r: 2,
539 n: 5,
540 b: 3,
541 q: 3,
542 m: 5,
543 k: 1000
544 };
545 }
546
547 static get SEARCH_DEPTH() {
548 return 2;
549 }
550
551 static GenRandInitFen(randomness) {
552 if (randomness == 0)
553 // Deterministic:
554 return "rnbqkbnrm/pppppppp/8/8/8/8/PPPPPPPP/MNBKQBNR w 0";
555
556 let pieces = { w: new Array(8), b: new Array(8) };
557 // Shuffle pieces on first and last rank
558 for (let c of ["w", "b"]) {
559 if (c == 'b' && randomness == 1) {
560 pieces['b'] = pieces['w'];
561 break;
562 }
563
564 // Get random squares for every piece, totally freely
565 let positions = shuffle(ArrayFun.range(8));
566 const composition = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'm'];
567 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
568 }
569 return (
570 pieces["b"].join("") +
571 "/pppppppp/8/8/8/8/PPPPPPPP/" +
572 pieces["w"].join("").toUpperCase() +
573 " w 0"
574 );
575 }
576
577 getNotation(move) {
578 const initialSquare = V.CoordsToSquare(move.start);
579 const finalSquare = V.CoordsToSquare(move.end);
580 let notation = undefined;
581 if (move.appear[0].p == V.PAWN) {
582 // Pawn: generally ambiguous short notation, so we use full description
583 notation = "P" + initialSquare + finalSquare;
584 } else if (move.appear[0].p == V.KING)
585 notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
586 else notation = move.appear[0].p.toUpperCase() + finalSquare;
587 // Add a capture mark (not describing what is captured...):
588 if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
589 return notation;
590 }
591 };