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