Some fixes, and add 2 variants: Checkless and Parachute
[vchess.git] / client / src / variants / Baroque.js
CommitLineData
0c3fe8a6
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { ArrayFun } from "@/utils/array";
32f6285e 3import { shuffle } from "@/utils/alea";
0c3fe8a6 4
32f6285e 5export class BaroqueRules extends ChessRules {
6808d7a1
BA
6 static get HasFlags() {
7 return false;
8 }
dac39588 9
6808d7a1
BA
10 static get HasEnpassant() {
11 return false;
12 }
dac39588 13
241bf8f2
BA
14 static get PIECES() {
15 return ChessRules.PIECES.concat([V.IMMOBILIZER]);
16 }
17
18 getPpath(b) {
6808d7a1
BA
19 if (b[1] == "m")
20 //'m' for Immobilizer (I is too similar to 1)
dac39588
BA
21 return "Baroque/" + b;
22 return b; //usual piece
23 }
24
dac39588 25 // No castling, but checks, so keep track of kings
6808d7a1
BA
26 setOtherVariables(fen) {
27 this.kingPos = { w: [-1, -1], b: [-1, -1] };
dac39588
BA
28 const fenParts = fen.split(" ");
29 const position = fenParts[0].split("/");
6808d7a1 30 for (let i = 0; i < position.length; i++) {
dac39588 31 let k = 0;
6808d7a1
BA
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];
dac39588 36 break;
6808d7a1
BA
37 case "K":
38 this.kingPos["w"] = [i, k];
dac39588 39 break;
6808d7a1
BA
40 default: {
41 const num = parseInt(position[i].charAt(j));
42 if (!isNaN(num)) k += num - 1;
43 }
dac39588
BA
44 }
45 k++;
46 }
47 }
48 }
49
6808d7a1
BA
50 static get IMMOBILIZER() {
51 return "m";
52 }
dac39588
BA
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?
6808d7a1
BA
61 isImmobilized([x, y]) {
62 const piece = this.getPiece(x, y);
63 const color = this.getColor(x, y);
dac39588
BA
64 const oppCol = V.GetOppCol(color);
65 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
6808d7a1
BA
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) {
4404e58c 75 // Moving is possible only if this immobilizer is neutralized
6808d7a1
BA
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)))
dac39588
BA
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
6808d7a1 91 if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) return true;
dac39588
BA
92 }
93 }
94 return false;
95 }
96
6808d7a1 97 getPotentialMovesFrom([x, y]) {
dac39588 98 // Pre-check: is thing on this square immobilized?
6808d7a1
BA
99 if (this.isImmobilized([x, y])) return [];
100 switch (this.getPiece(x, y)) {
dac39588 101 case V.IMMOBILIZER:
6808d7a1 102 return this.getPotentialImmobilizerMoves([x, y]);
dac39588 103 default:
6808d7a1 104 return super.getPotentialMovesFrom([x, y]);
dac39588
BA
105 }
106 }
107
6808d7a1
BA
108 getSlideNJumpMoves([x, y], steps, oneStep) {
109 const piece = this.getPiece(x, y);
dac39588 110 let moves = [];
6808d7a1 111 outerLoop: for (let step of steps) {
dac39588
BA
112 let i = x + step[0];
113 let j = y + step[1];
6808d7a1
BA
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;
dac39588
BA
117 i += step[0];
118 j += step[1];
119 }
120 // Only king can take on occupied square:
6808d7a1
BA
121 if (piece == V.KING && V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
122 moves.push(this.getBasicMove([x, y], [i, j]));
dac39588
BA
123 }
124 return moves;
125 }
126
127 // Modify capturing moves among listed pawn moves
6808d7a1 128 addPawnCaptures(moves, byChameleon) {
dac39588
BA
129 const steps = V.steps[V.ROOK];
130 const color = this.turn;
131 const oppCol = V.GetOppCol(color);
132 moves.forEach(m => {
6808d7a1 133 if (!!byChameleon && m.start.x != m.end.x && m.start.y != m.end.y) return; //chameleon not moving as pawn
dac39588 134 // Try capturing in every direction
6808d7a1
BA
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 ) {
dac39588 142 // Potential capture
6808d7a1
BA
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 );
dac39588
BA
158 }
159 }
160 }
161 }
162 });
163 }
164
165 // "Pincer"
6808d7a1
BA
166 getPotentialPawnMoves([x, y]) {
167 let moves = super.getPotentialRookMoves([x, y]);
dac39588
BA
168 this.addPawnCaptures(moves);
169 return moves;
170 }
171
6808d7a1 172 addRookCaptures(moves, byChameleon) {
dac39588
BA
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
6808d7a1 178 if (m.end.x == kp[0] || m.end.y == kp[1]) return; //"flat rectangle"
dac39588
BA
179 const corner1 = [m.end.x, kp[1]];
180 const corner2 = [kp[0], m.end.y];
6808d7a1
BA
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 );
dac39588
BA
193 }
194 }
195 }
196 });
197 }
198
199 // Coordinator
6808d7a1 200 getPotentialRookMoves(sq) {
dac39588
BA
201 let moves = super.getPotentialQueenMoves(sq);
202 this.addRookCaptures(moves);
203 return moves;
204 }
205
6808d7a1 206 getKnightCaptures(startSquare, byChameleon) {
dac39588
BA
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 = [];
6808d7a1
BA
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) {
dac39588
BA
217 i += step[0];
218 j += step[1];
219 }
6808d7a1
BA
220 if (
221 !V.OnBoard(i, j) ||
222 this.getColor(i, j) == color ||
223 (!!byChameleon && this.getPiece(i, j) != V.KNIGHT)
224 ) {
dac39588
BA
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.
6808d7a1
BA
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;
dac39588 238 // Something to eat:
6808d7a1
BA
239 vanished.push(
240 new PiPo({ x: last[0], y: last[1], c: oppCol, p: oppPiece })
241 );
dac39588 242 }
6808d7a1
BA
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
dac39588
BA
249 continue outerLoop;
250 }
6808d7a1
BA
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 );
dac39588 260 }
6808d7a1
BA
261 last = [last[0] + step[0], last[1] + step[1]];
262 cur = [cur[0] + step[0], cur[1] + step[1]];
dac39588
BA
263 }
264 }
265 return moves;
266 }
267
268 // Long-leaper
6808d7a1 269 getPotentialKnightMoves(sq) {
dac39588
BA
270 return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
271 }
272
78d64531 273 // Chameleon
6808d7a1
BA
274 getPotentialBishopMoves([x, y]) {
275 let moves = super
276 .getPotentialQueenMoves([x, y])
277 .concat(this.getKnightCaptures([x, y], "asChameleon"));
dac39588
BA
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;
6808d7a1
BA
286 if (!mergedMoves[key]) mergedMoves[key] = m;
287 else {
288 for (let i = 1; i < m.vanish.length; i++)
dac39588
BA
289 mergedMoves[key].vanish.push(m.vanish[i]);
290 }
291 });
292 // Finally return an array
293 moves = [];
6808d7a1
BA
294 Object.keys(mergedMoves).forEach(k => {
295 moves.push(mergedMoves[k]);
296 });
dac39588
BA
297 return moves;
298 }
299
6808d7a1
BA
300 addQueenCaptures(moves, byChameleon) {
301 if (moves.length == 0) return;
302 const [x, y] = [moves[0].start.x, moves[0].start.y];
dac39588
BA
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 => {
6808d7a1
BA
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 ) {
dac39588
BA
315 capturingDirections.push(step);
316 }
317 });
318 moves.forEach(m => {
319 const step = [
6808d7a1
BA
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
dac39588
BA
322 ];
323 // NOTE: includes() and even _.isEqual() functions fail...
324 // TODO: this test should be done only once per direction
6808d7a1
BA
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 );
dac39588
BA
339 }
340 });
341 }
342
78d64531 343 // Withdrawer
6808d7a1 344 getPotentialQueenMoves(sq) {
dac39588
BA
345 let moves = super.getPotentialQueenMoves(sq);
346 this.addQueenCaptures(moves);
347 return moves;
348 }
349
6808d7a1 350 getPotentialImmobilizerMoves(sq) {
dac39588
BA
351 // Immobilizer doesn't capture
352 return super.getPotentialQueenMoves(sq);
353 }
354
dac39588
BA
355 // isAttacked() is OK because the immobilizer doesn't take
356
68e19a44 357 isAttackedByPawn([x, y], color) {
dac39588
BA
358 // Square (x,y) must be surroundable by two enemy pieces,
359 // and one of them at least should be a pawn (moving).
6808d7a1
BA
360 const dirs = [
361 [1, 0],
362 [0, 1]
363 ];
dac39588 364 const steps = V.steps[V.ROOK];
6808d7a1
BA
365 for (let dir of dirs) {
366 const [i1, j1] = [x - dir[0], y - dir[1]]; //"before"
367 const [i2, j2] = [x + dir[0], y + dir[1]]; //"after"
368 if (V.OnBoard(i1, j1) && V.OnBoard(i2, j2)) {
369 if (
68e19a44
BA
370 (
371 this.board[i1][j1] != V.EMPTY &&
372 this.getColor(i1, j1) == color &&
373 this.board[i2][j2] == V.EMPTY
374 )
375 ||
376 (
377 this.board[i2][j2] != V.EMPTY &&
378 this.getColor(i2, j2) == color &&
379 this.board[i1][j1] == V.EMPTY
380 )
6808d7a1 381 ) {
dac39588 382 // Search a movable enemy pawn landing on the empty square
6808d7a1
BA
383 for (let step of steps) {
384 let [ii, jj] = this.board[i1][j1] == V.EMPTY ? [i1, j1] : [i2, j2];
385 let [i3, j3] = [ii + step[0], jj + step[1]];
386 while (V.OnBoard(i3, j3) && this.board[i3][j3] == V.EMPTY) {
dac39588
BA
387 i3 += step[0];
388 j3 += step[1];
389 }
6808d7a1
BA
390 if (
391 V.OnBoard(i3, j3) &&
68e19a44 392 this.getColor(i3, j3) == color &&
6808d7a1
BA
393 this.getPiece(i3, j3) == V.PAWN &&
394 !this.isImmobilized([i3, j3])
395 ) {
dac39588
BA
396 return true;
397 }
398 }
399 }
400 }
401 }
402 return false;
403 }
404
68e19a44 405 isAttackedByRook([x, y], color) {
dac39588
BA
406 // King must be on same column or row,
407 // and a rook should be able to reach a capturing square
68e19a44
BA
408 const sameRow = x == this.kingPos[color][0];
409 const sameColumn = y == this.kingPos[color][1];
6808d7a1 410 if (sameRow || sameColumn) {
dac39588 411 // Look for the enemy rook (maximum 1)
6808d7a1
BA
412 for (let i = 0; i < V.size.x; i++) {
413 for (let j = 0; j < V.size.y; j++) {
414 if (
415 this.board[i][j] != V.EMPTY &&
68e19a44 416 this.getColor(i, j) == color &&
6808d7a1
BA
417 this.getPiece(i, j) == V.ROOK
418 ) {
419 if (this.isImmobilized([i, j])) return false; //because only one rook
dac39588
BA
420 // Can it reach a capturing square?
421 // Easy but quite suboptimal way (TODO): generate all moves (turn is OK)
6808d7a1
BA
422 const moves = this.getPotentialMovesFrom([i, j]);
423 for (let move of moves) {
424 if (
425 (sameRow && move.end.y == y) ||
426 (sameColumn && move.end.x == x)
427 )
dac39588
BA
428 return true;
429 }
430 }
431 }
432 }
433 }
434 return false;
435 }
436
68e19a44 437 isAttackedByKnight([x, y], color) {
dac39588
BA
438 // Square (x,y) must be on same line as a knight,
439 // and there must be empty square(s) behind.
440 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
6808d7a1
BA
441 outerLoop: for (let step of steps) {
442 const [i0, j0] = [x + step[0], y + step[1]];
443 if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) {
dac39588 444 // Try in opposite direction:
6808d7a1
BA
445 let [i, j] = [x - step[0], y - step[1]];
446 while (V.OnBoard(i, j)) {
447 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
dac39588
BA
448 i -= step[0];
449 j -= step[1];
450 }
6808d7a1 451 if (V.OnBoard(i, j)) {
68e19a44 452 if (this.getColor(i, j) == color) {
6808d7a1
BA
453 if (
454 this.getPiece(i, j) == V.KNIGHT &&
455 !this.isImmobilized([i, j])
456 )
dac39588
BA
457 return true;
458 continue outerLoop;
459 }
460 // [else] Our color, could be captured *if there was an empty space*
6808d7a1 461 if (this.board[i + step[0]][j + step[1]] != V.EMPTY)
dac39588
BA
462 continue outerLoop;
463 i -= step[0];
464 j -= step[1];
465 }
466 }
467 }
468 }
469 return false;
470 }
471
68e19a44 472 isAttackedByBishop([x, y], color) {
4404e58c
BA
473 // We cheat a little here: since this function is used exclusively for
474 // the king, it's enough to check the immediate surrounding of the square.
dac39588 475 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
6808d7a1
BA
476 for (let step of adjacentSteps) {
477 const [i, j] = [x + step[0], y + step[1]];
478 if (
479 V.OnBoard(i, j) &&
480 this.board[i][j] != V.EMPTY &&
68e19a44 481 this.getColor(i, j) == color &&
6808d7a1
BA
482 this.getPiece(i, j) == V.BISHOP
483 ) {
dac39588
BA
484 return true; //bishops are never immobilized
485 }
486 }
487 return false;
488 }
489
68e19a44 490 isAttackedByQueen([x, y], color) {
dac39588
BA
491 // Square (x,y) must be adjacent to a queen, and the queen must have
492 // some free space in the opposite direction from (x,y)
493 const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
6808d7a1
BA
494 for (let step of adjacentSteps) {
495 const sq2 = [x + 2 * step[0], y + 2 * step[1]];
496 if (V.OnBoard(sq2[0], sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) {
497 const sq1 = [x + step[0], y + step[1]];
498 if (
499 this.board[sq1[0]][sq1[1]] != V.EMPTY &&
68e19a44 500 this.getColor(sq1[0], sq1[1]) == color &&
6808d7a1
BA
501 this.getPiece(sq1[0], sq1[1]) == V.QUEEN &&
502 !this.isImmobilized(sq1)
503 ) {
dac39588
BA
504 return true;
505 }
506 }
507 }
508 return false;
509 }
510
68e19a44 511 isAttackedByKing([x, y], color) {
4404e58c
BA
512 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
513 for (let step of steps) {
514 let rx = x + step[0],
515 ry = y + step[1];
516 if (
517 V.OnBoard(rx, ry) &&
518 this.getPiece(rx, ry) === V.KING &&
68e19a44 519 this.getColor(rx, ry) == color &&
4404e58c
BA
520 !this.isImmobilized([rx, ry])
521 ) {
522 return true;
523 }
524 }
525 return false;
526 }
527
6808d7a1 528 static get VALUES() {
dac39588 529 return {
6808d7a1
BA
530 p: 1,
531 r: 2,
532 n: 5,
533 b: 3,
534 q: 3,
535 m: 5,
536 k: 1000
dac39588
BA
537 };
538 }
539
6808d7a1
BA
540 static get SEARCH_DEPTH() {
541 return 2;
542 }
dac39588 543
7ba4a5bc 544 static GenRandInitFen(randomness) {
7ba4a5bc
BA
545 if (randomness == 0)
546 // Deterministic:
547 return "rnbqkbnrm/pppppppp/8/8/8/8/PPPPPPPP/MNBKQBNR w 0";
548
6808d7a1 549 let pieces = { w: new Array(8), b: new Array(8) };
dac39588 550 // Shuffle pieces on first and last rank
6808d7a1 551 for (let c of ["w", "b"]) {
7ba4a5bc
BA
552 if (c == 'b' && randomness == 1) {
553 pieces['b'] = pieces['w'];
554 break;
555 }
556
dac39588 557 // Get random squares for every piece, totally freely
32f6285e
BA
558 let positions = shuffle(ArrayFun.range(8));
559 const composition = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'm'];
560 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
dac39588 561 }
6808d7a1
BA
562 return (
563 pieces["b"].join("") +
dac39588
BA
564 "/pppppppp/8/8/8/8/PPPPPPPP/" +
565 pieces["w"].join("").toUpperCase() +
6808d7a1
BA
566 " w 0"
567 );
dac39588
BA
568 }
569
6808d7a1 570 getNotation(move) {
dac39588
BA
571 const initialSquare = V.CoordsToSquare(move.start);
572 const finalSquare = V.CoordsToSquare(move.end);
573 let notation = undefined;
6808d7a1 574 if (move.appear[0].p == V.PAWN) {
dac39588
BA
575 // Pawn: generally ambiguous short notation, so we use full description
576 notation = "P" + initialSquare + finalSquare;
6808d7a1
BA
577 } else if (move.appear[0].p == V.KING)
578 notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
579 else notation = move.appear[0].p.toUpperCase() + finalSquare;
e9b736ee
BA
580 // Add a capture mark (not describing what is captured...):
581 if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
dac39588
BA
582 return notation;
583 }
6808d7a1 584};