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