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