Attempt to fix promotions on mobile browsers
[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 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, 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 getKnightCaptures(startSquare, byChameleon) {
207 // Look in every direction for captures
208 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
209 const color = this.turn;
210 const oppCol = V.GetOppCol(color);
211 let moves = [];
212 const [x, y] = [startSquare[0], startSquare[1]];
213 const piece = this.getPiece(x, y); //might be a chameleon!
214 outerLoop: for (let step of steps) {
215 let [i, j] = [x + step[0], y + step[1]];
216 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
217 i += step[0];
218 j += step[1];
219 }
220 if (
221 !V.OnBoard(i, j) ||
222 this.getColor(i, j) == color ||
223 (!!byChameleon && this.getPiece(i, j) != V.KNIGHT)
224 ) {
225 continue;
226 }
227 // last(thing), cur(thing) : stop if "cur" is our color, or beyond board limits,
228 // or if "last" isn't empty and cur neither. Otherwise, if cur is empty then
229 // add move until cur square; if cur is occupied then stop if !!byChameleon and
230 // the square not occupied by a leaper.
231 let last = [i, j];
232 let cur = [i + step[0], j + step[1]];
233 let vanished = [new PiPo({ x: x, y: y, c: color, p: piece })];
234 while (V.OnBoard(cur[0], cur[1])) {
235 if (this.board[last[0]][last[1]] != V.EMPTY) {
236 const oppPiece = this.getPiece(last[0], last[1]);
237 if (!!byChameleon && oppPiece != V.KNIGHT) continue outerLoop;
238 // Something to eat:
239 vanished.push(
240 new PiPo({ x: last[0], y: last[1], c: oppCol, p: oppPiece })
241 );
242 }
243 if (this.board[cur[0]][cur[1]] != V.EMPTY) {
244 if (
245 this.getColor(cur[0], cur[1]) == color ||
246 this.board[last[0]][last[1]] != V.EMPTY
247 ) {
248 //TODO: redundant test
249 continue outerLoop;
250 }
251 } else {
252 moves.push(
253 new Move({
254 appear: [new PiPo({ x: cur[0], y: cur[1], c: color, p: piece })],
255 vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
256 start: { x: x, y: y },
257 end: { x: cur[0], y: cur[1] }
258 })
259 );
260 }
261 last = [last[0] + step[0], last[1] + step[1]];
262 cur = [cur[0] + step[0], cur[1] + step[1]];
263 }
264 }
265 return moves;
266 }
267
268 // Long-leaper
269 getPotentialKnightMoves(sq) {
270 return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
271 }
272
273 // Chameleon
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 addQueenCaptures(moves, byChameleon) {
301 if (moves.length == 0) return;
302 const [x, y] = [moves[0].start.x, moves[0].start.y];
303 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
304 let capturingDirections = [];
305 const color = this.turn;
306 const oppCol = V.GetOppCol(color);
307 adjacentSteps.forEach(step => {
308 const [i, j] = [x + step[0], y + step[1]];
309 if (
310 V.OnBoard(i, j) &&
311 this.board[i][j] != V.EMPTY &&
312 this.getColor(i, j) == oppCol &&
313 (!byChameleon || this.getPiece(i, j) == V.QUEEN)
314 ) {
315 capturingDirections.push(step);
316 }
317 });
318 moves.forEach(m => {
319 const step = [
320 m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
321 m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
322 ];
323 // NOTE: includes() and even _.isEqual() functions fail...
324 // TODO: this test should be done only once per direction
325 if (
326 capturingDirections.some(dir => {
327 return dir[0] == -step[0] && dir[1] == -step[1];
328 })
329 ) {
330 const [i, j] = [x - step[0], y - step[1]];
331 m.vanish.push(
332 new PiPo({
333 x: i,
334 y: j,
335 p: this.getPiece(i, j),
336 c: oppCol
337 })
338 );
339 }
340 });
341 }
342
343 // Withdrawer
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
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 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 isAttackedByKing([x, y], colors) {
516 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
517 for (let step of steps) {
518 let rx = x + step[0],
519 ry = y + step[1];
520 if (
521 V.OnBoard(rx, ry) &&
522 this.getPiece(rx, ry) === V.KING &&
523 colors.includes(this.getColor(rx, ry)) &&
524 !this.isImmobilized([rx, ry])
525 ) {
526 return true;
527 }
528 }
529 return false;
530 }
531
532 static get VALUES() {
533 return {
534 p: 1,
535 r: 2,
536 n: 5,
537 b: 3,
538 q: 3,
539 m: 5,
540 k: 1000
541 };
542 }
543
544 static get SEARCH_DEPTH() {
545 return 2;
546 }
547
548 static GenRandInitFen(randomness) {
549 if (randomness == 0)
550 // Deterministic:
551 return "rnbqkbnrm/pppppppp/8/8/8/8/PPPPPPPP/MNBKQBNR w 0";
552
553 let pieces = { w: new Array(8), b: new Array(8) };
554 // Shuffle pieces on first and last rank
555 for (let c of ["w", "b"]) {
556 if (c == 'b' && randomness == 1) {
557 pieces['b'] = pieces['w'];
558 break;
559 }
560
561 let positions = ArrayFun.range(8);
562 // Get random squares for every piece, totally freely
563
564 let randIndex = randInt(8);
565 const bishop1Pos = positions[randIndex];
566 positions.splice(randIndex, 1);
567
568 randIndex = randInt(7);
569 const bishop2Pos = positions[randIndex];
570 positions.splice(randIndex, 1);
571
572 randIndex = randInt(6);
573 const knight1Pos = positions[randIndex];
574 positions.splice(randIndex, 1);
575
576 randIndex = randInt(5);
577 const knight2Pos = positions[randIndex];
578 positions.splice(randIndex, 1);
579
580 randIndex = randInt(4);
581 const queenPos = positions[randIndex];
582 positions.splice(randIndex, 1);
583
584 randIndex = randInt(3);
585 const kingPos = positions[randIndex];
586 positions.splice(randIndex, 1);
587
588 randIndex = randInt(2);
589 const rookPos = positions[randIndex];
590 positions.splice(randIndex, 1);
591 const immobilizerPos = positions[0];
592
593 pieces[c][bishop1Pos] = "b";
594 pieces[c][bishop2Pos] = "b";
595 pieces[c][knight1Pos] = "n";
596 pieces[c][knight2Pos] = "n";
597 pieces[c][queenPos] = "q";
598 pieces[c][kingPos] = "k";
599 pieces[c][rookPos] = "r";
600 pieces[c][immobilizerPos] = "m";
601 }
602 return (
603 pieces["b"].join("") +
604 "/pppppppp/8/8/8/8/PPPPPPPP/" +
605 pieces["w"].join("").toUpperCase() +
606 " w 0"
607 );
608 }
609
610 getNotation(move) {
611 const initialSquare = V.CoordsToSquare(move.start);
612 const finalSquare = V.CoordsToSquare(move.end);
613 let notation = undefined;
614 if (move.appear[0].p == V.PAWN) {
615 // Pawn: generally ambiguous short notation, so we use full description
616 notation = "P" + initialSquare + finalSquare;
617 } else if (move.appear[0].p == V.KING)
618 notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
619 else notation = move.appear[0].p.toUpperCase() + finalSquare;
620 // Add a capture mark (not describing what is captured...):
621 if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
622 return notation;
623 }
624 };