Fix bug in Eightpieces rules (division by 0)
[vchess.git] / client / src / variants / Eightpieces.js
1 import { ArrayFun } from "@/utils/array";
2 import { randInt } from "@/utils/alea";
3 import { ChessRules, PiPo, Move } from "@/base_rules";
4
5 export const VariantRules = class EightpiecesRules extends ChessRules {
6 static get JAILER() {
7 return "j";
8 }
9 static get SENTRY() {
10 return "s";
11 }
12 static get LANCER() {
13 return "l";
14 }
15
16 static get IMAGE_EXTENSION() {
17 // Temporarily, for the time SVG pieces are being designed:
18 return ".png";
19 }
20
21 // Lancer directions *from white perspective*
22 static get LANCER_DIRS() {
23 return {
24 'c': [-1, 0], //north
25 'd': [-1, 1], //N-E
26 'e': [0, 1], //east
27 'f': [1, 1], //S-E
28 'g': [1, 0], //south
29 'h': [1, -1], //S-W
30 'm': [0, -1], //west
31 'o': [-1, -1] //N-W
32 };
33 }
34
35 static get PIECES() {
36 return ChessRules.PIECES
37 .concat([V.JAILER, V.SENTRY])
38 .concat(Object.keys(V.LANCER_DIRS));
39 }
40
41 getPiece(i, j) {
42 const piece = this.board[i][j].charAt(1);
43 // Special lancer case: 8 possible orientations
44 if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
45 return piece;
46 }
47
48 getPpath(b, color, score, orientation) {
49 if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b;
50 if (Object.keys(V.LANCER_DIRS).includes(b[1])) {
51 if (orientation == 'w') return "Eightpieces/tmp_png/" + b;
52 // Find opposite direction for adequate display:
53 let oppDir = '';
54 switch (b[1]) {
55 case 'c':
56 oppDir = 'g';
57 break;
58 case 'g':
59 oppDir = 'c';
60 break;
61 case 'd':
62 oppDir = 'h';
63 break;
64 case 'h':
65 oppDir = 'd';
66 break;
67 case 'e':
68 oppDir = 'm';
69 break;
70 case 'm':
71 oppDir = 'e';
72 break;
73 case 'f':
74 oppDir = 'o';
75 break;
76 case 'o':
77 oppDir = 'f';
78 break;
79 }
80 return "Eightpieces/tmp_png/" + b[0] + oppDir;
81 }
82 // TODO: after we have SVG pieces, remove the folder and next prefix:
83 return "Eightpieces/tmp_png/" + b;
84 }
85
86 getPPpath(b, orientation) {
87 return this.getPpath(b, null, null, orientation);
88 }
89
90 static ParseFen(fen) {
91 const fenParts = fen.split(" ");
92 return Object.assign(
93 ChessRules.ParseFen(fen),
94 { sentrypush: fenParts[5] }
95 );
96 }
97
98 static IsGoodFen(fen) {
99 if (!ChessRules.IsGoodFen(fen)) return false;
100 const fenParsed = V.ParseFen(fen);
101 // 5) Check sentry push (if any)
102 if (
103 fenParsed.sentrypush != "-" &&
104 !fenParsed.sentrypush.match(/^([a-h][1-8],?)+$/)
105 ) {
106 return false;
107 }
108 return true;
109 }
110
111 getFen() {
112 return super.getFen() + " " + this.getSentrypushFen();
113 }
114
115 getFenForRepeat() {
116 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
117 }
118
119 getSentrypushFen() {
120 const L = this.sentryPush.length;
121 if (!this.sentryPush[L-1]) return "-";
122 let res = "";
123 this.sentryPush[L-1].forEach(coords =>
124 res += V.CoordsToSquare(coords) + ",");
125 return res.slice(0, -1);
126 }
127
128 setOtherVariables(fen) {
129 super.setOtherVariables(fen);
130 // subTurn == 2 only when a sentry moved, and is about to push something
131 this.subTurn = 1;
132 // Sentry position just after a "capture" (subTurn from 1 to 2)
133 this.sentryPos = null;
134 // Stack pieces' forbidden squares after a sentry move at each turn
135 const parsedFen = V.ParseFen(fen);
136 if (parsedFen.sentrypush == "-") this.sentryPush = [null];
137 else {
138 this.sentryPush = [
139 parsedFen.sentrypush.split(",").map(sq => {
140 return V.SquareToCoords(sq);
141 })
142 ];
143 }
144 }
145
146 static GenRandInitFen(randomness) {
147 if (randomness == 0)
148 // Deterministic:
149 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 ahah - -";
150
151 let pieces = { w: new Array(8), b: new Array(8) };
152 let flags = "";
153 // Shuffle pieces on first (and last rank if randomness == 2)
154 for (let c of ["w", "b"]) {
155 if (c == 'b' && randomness == 1) {
156 const lancerIdx = pieces['w'].findIndex(p => {
157 return Object.keys(V.LANCER_DIRS).includes(p);
158 });
159 pieces['b'] =
160 pieces['w'].slice(0, lancerIdx)
161 .concat(['g'])
162 .concat(pieces['w'].slice(lancerIdx + 1));
163 flags += flags;
164 break;
165 }
166
167 let positions = ArrayFun.range(8);
168
169 // Get random squares for bishop and sentry
170 let randIndex = 2 * randInt(4);
171 let bishopPos = positions[randIndex];
172 // The sentry must be on a square of different color
173 let randIndex_tmp = 2 * randInt(4) + 1;
174 let sentryPos = positions[randIndex_tmp];
175 if (c == 'b') {
176 // Check if white sentry is on the same color as ours.
177 // If yes: swap bishop and sentry positions.
178 if ((pieces['w'].indexOf('s') - sentryPos) % 2 == 0)
179 [bishopPos, sentryPos] = [sentryPos, bishopPos];
180 }
181 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
182 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
183
184 // Get random squares for knight and lancer
185 randIndex = randInt(6);
186 const knightPos = positions[randIndex];
187 positions.splice(randIndex, 1);
188 randIndex = randInt(5);
189 const lancerPos = positions[randIndex];
190 positions.splice(randIndex, 1);
191
192 // Get random square for queen
193 randIndex = randInt(4);
194 const queenPos = positions[randIndex];
195 positions.splice(randIndex, 1);
196
197 // Rook, jailer and king positions are now almost fixed,
198 // only the ordering rook-> jailer or jailer->rook must be decided.
199 let rookPos = positions[0];
200 let jailerPos = positions[2];
201 const kingPos = positions[1];
202 flags += V.CoordToColumn(rookPos) + V.CoordToColumn(jailerPos);
203 if (Math.random() < 0.5) [rookPos, jailerPos] = [jailerPos, rookPos];
204
205 pieces[c][rookPos] = "r";
206 pieces[c][knightPos] = "n";
207 pieces[c][bishopPos] = "b";
208 pieces[c][queenPos] = "q";
209 pieces[c][kingPos] = "k";
210 pieces[c][sentryPos] = "s";
211 // Lancer faces north for white, and south for black:
212 pieces[c][lancerPos] = c == 'w' ? 'c' : 'g';
213 pieces[c][jailerPos] = "j";
214 }
215 return (
216 pieces["b"].join("") +
217 "/pppppppp/8/8/8/8/PPPPPPPP/" +
218 pieces["w"].join("").toUpperCase() +
219 " w 0 " + flags + " - -"
220 );
221 }
222
223 canTake([x1, y1], [x2, y2]) {
224 if (this.subTurn == 2)
225 // Only self captures on this subturn:
226 return this.getColor(x1, y1) == this.getColor(x2, y2);
227 return super.canTake([x1, y1], [x2, y2]);
228 }
229
230 // Is piece on square (x,y) immobilized?
231 isImmobilized([x, y]) {
232 const color = this.getColor(x, y);
233 const oppCol = V.GetOppCol(color);
234 for (let step of V.steps[V.ROOK]) {
235 const [i, j] = [x + step[0], y + step[1]];
236 if (
237 V.OnBoard(i, j) &&
238 this.board[i][j] != V.EMPTY &&
239 this.getColor(i, j) == oppCol
240 ) {
241 if (this.getPiece(i, j) == V.JAILER) return [i, j];
242 }
243 }
244 return null;
245 }
246
247 // Because of the lancers, getPiece() could be wrong:
248 // use board[x][y][1] instead (always valid).
249 getBasicMove([sx, sy], [ex, ey], tr) {
250 let mv = new Move({
251 appear: [
252 new PiPo({
253 x: ex,
254 y: ey,
255 c: tr ? tr.c : this.getColor(sx, sy),
256 p: tr ? tr.p : this.board[sx][sy].charAt(1)
257 })
258 ],
259 vanish: [
260 new PiPo({
261 x: sx,
262 y: sy,
263 c: this.getColor(sx, sy),
264 p: this.board[sx][sy].charAt(1)
265 })
266 ]
267 });
268
269 // The opponent piece disappears if we take it
270 if (this.board[ex][ey] != V.EMPTY) {
271 mv.vanish.push(
272 new PiPo({
273 x: ex,
274 y: ey,
275 c: this.getColor(ex, ey),
276 p: this.board[ex][ey].charAt(1)
277 })
278 );
279 }
280
281 return mv;
282 }
283
284 canIplay(side, [x, y]) {
285 return (
286 (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side) ||
287 (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y)
288 );
289 }
290
291 getPotentialMovesFrom([x, y]) {
292 // At subTurn == 2, jailers aren't effective (Jeff K)
293 const piece = this.getPiece(x, y);
294 const L = this.sentryPush.length;
295 if (this.subTurn == 1) {
296 const jsq = this.isImmobilized([x, y]);
297 if (!!jsq) {
298 let moves = [];
299 // Special pass move if king:
300 if (piece == V.KING) {
301 moves.push(
302 new Move({
303 appear: [],
304 vanish: [],
305 start: { x: x, y: y },
306 end: { x: jsq[0], y: jsq[1] }
307 })
308 );
309 }
310 else if (piece == V.LANCER && !!this.sentryPush[L-1]) {
311 // A pushed lancer next to the jailer: reorient
312 const color = this.getColor(x, y);
313 const curDir = this.board[x][y].charAt(1);
314 Object.keys(V.LANCER_DIRS).forEach(k => {
315 moves.push(
316 new Move({
317 appear: [{ x: x, y: y, c: color, p: k }],
318 vanish: [{ x: x, y: y, c: color, p: curDir }],
319 start: { x: x, y: y },
320 end: { x: jsq[0], y: jsq[1] }
321 })
322 );
323 });
324 }
325 return moves;
326 }
327 }
328 let moves = [];
329 switch (piece) {
330 case V.JAILER:
331 moves = this.getPotentialJailerMoves([x, y]);
332 break;
333 case V.SENTRY:
334 moves = this.getPotentialSentryMoves([x, y]);
335 break;
336 case V.LANCER:
337 moves = this.getPotentialLancerMoves([x, y]);
338 break;
339 default:
340 moves = super.getPotentialMovesFrom([x, y]);
341 break;
342 }
343 if (!!this.sentryPush[L-1]) {
344 // Delete moves walking back on sentry push path,
345 // only if not a pawn, and the piece is the pushed one.
346 const pl = this.sentryPush[L-1].length;
347 const finalPushedSq = this.sentryPush[L-1][pl-1];
348 moves = moves.filter(m => {
349 if (
350 m.vanish[0].p != V.PAWN &&
351 m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y &&
352 this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
353 ) {
354 return false;
355 }
356 return true;
357 });
358 } else if (this.subTurn == 2) {
359 // Put back the sentinel on board:
360 const color = this.turn;
361 moves.forEach(m => {
362 m.appear.push({x: x, y: y, p: V.SENTRY, c: color});
363 });
364 }
365 return moves;
366 }
367
368 getPotentialPawnMoves([x, y]) {
369 const color = this.getColor(x, y);
370 let moves = [];
371 const [sizeX, sizeY] = [V.size.x, V.size.y];
372 let shiftX = (color == "w" ? -1 : 1);
373 if (this.subTurn == 2) shiftX *= -1;
374 const firstRank = color == "w" ? sizeX - 1 : 0;
375 const startRank = color == "w" ? sizeX - 2 : 1;
376 const lastRank = color == "w" ? 0 : sizeX - 1;
377
378 // Pawns might be pushed on 1st rank and attempt to move again:
379 if (!V.OnBoard(x + shiftX, y)) return [];
380
381 const finalPieces =
382 // A push cannot put a pawn on last rank (it goes backward)
383 x + shiftX == lastRank
384 ? Object.keys(V.LANCER_DIRS).concat(
385 [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER])
386 : [V.PAWN];
387 if (this.board[x + shiftX][y] == V.EMPTY) {
388 // One square forward
389 for (let piece of finalPieces) {
390 moves.push(
391 this.getBasicMove([x, y], [x + shiftX, y], {
392 c: color,
393 p: piece
394 })
395 );
396 }
397 if (
398 // 2-squares jumps forbidden if pawn push
399 this.subTurn == 1 &&
400 [startRank, firstRank].includes(x) &&
401 this.board[x + 2 * shiftX][y] == V.EMPTY
402 ) {
403 // Two squares jump
404 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
405 }
406 }
407 // Captures
408 for (let shiftY of [-1, 1]) {
409 if (
410 y + shiftY >= 0 &&
411 y + shiftY < sizeY &&
412 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
413 this.canTake([x, y], [x + shiftX, y + shiftY])
414 ) {
415 for (let piece of finalPieces) {
416 moves.push(
417 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
418 c: color,
419 p: piece
420 })
421 );
422 }
423 }
424 }
425
426 // En passant: only on subTurn == 1
427 const Lep = this.epSquares.length;
428 const epSquare = this.epSquares[Lep - 1];
429 if (
430 this.subTurn == 1 &&
431 !!epSquare &&
432 epSquare.x == x + shiftX &&
433 Math.abs(epSquare.y - y) == 1
434 ) {
435 let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
436 enpassantMove.vanish.push({
437 x: x,
438 y: epSquare.y,
439 p: "p",
440 c: this.getColor(x, epSquare.y)
441 });
442 moves.push(enpassantMove);
443 }
444
445 return moves;
446 }
447
448 // Obtain all lancer moves in "step" direction
449 getPotentialLancerMoves_aux([x, y], step, tr) {
450 let moves = [];
451 // Add all moves to vacant squares until opponent is met:
452 const color = this.getColor(x, y);
453 const oppCol =
454 this.subTurn == 1
455 ? V.GetOppCol(color)
456 // at subTurn == 2, consider own pieces as opponent
457 : color;
458 let sq = [x + step[0], y + step[1]];
459 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
460 if (this.board[sq[0]][sq[1]] == V.EMPTY)
461 moves.push(this.getBasicMove([x, y], sq, tr));
462 sq[0] += step[0];
463 sq[1] += step[1];
464 }
465 if (V.OnBoard(sq[0], sq[1]))
466 // Add capturing move
467 moves.push(this.getBasicMove([x, y], sq, tr));
468 return moves;
469 }
470
471 getPotentialLancerMoves([x, y]) {
472 let moves = [];
473 // Add all lancer possible orientations, similar to pawn promotions.
474 // Except if just after a push: allow all movements from init square then
475 const L = this.sentryPush.length;
476 const color = this.getColor(x, y);
477 if (!!this.sentryPush[L-1]) {
478 // Maybe I was pushed
479 const pl = this.sentryPush[L-1].length;
480 if (
481 this.sentryPush[L-1][pl-1].x == x &&
482 this.sentryPush[L-1][pl-1].y == y
483 ) {
484 // I was pushed: allow all directions (for this move only), but
485 // do not change direction after moving, *except* if I keep the
486 // same orientation in which I was pushed.
487 const curDir = V.LANCER_DIRS[this.board[x][y].charAt(1)];
488 Object.values(V.LANCER_DIRS).forEach(step => {
489 const dirCode = Object.keys(V.LANCER_DIRS).find(k => {
490 return (
491 V.LANCER_DIRS[k][0] == step[0] &&
492 V.LANCER_DIRS[k][1] == step[1]
493 );
494 });
495 const dirMoves =
496 this.getPotentialLancerMoves_aux(
497 [x, y],
498 step,
499 { p: dirCode, c: color }
500 );
501 if (curDir[0] == step[0] && curDir[1] == step[1]) {
502 // Keeping same orientation: can choose after
503 let chooseMoves = [];
504 dirMoves.forEach(m => {
505 Object.keys(V.LANCER_DIRS).forEach(k => {
506 let mk = JSON.parse(JSON.stringify(m));
507 mk.appear[0].p = k;
508 moves.push(mk);
509 });
510 });
511 Array.prototype.push.apply(moves, chooseMoves);
512 } else Array.prototype.push.apply(moves, dirMoves);
513 });
514 return moves;
515 }
516 }
517 // I wasn't pushed: standard lancer move
518 const dirCode = this.board[x][y][1];
519 const monodirMoves =
520 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
521 // Add all possible orientations aftermove except if I'm being pushed
522 if (this.subTurn == 1) {
523 monodirMoves.forEach(m => {
524 Object.keys(V.LANCER_DIRS).forEach(k => {
525 let mk = JSON.parse(JSON.stringify(m));
526 mk.appear[0].p = k;
527 moves.push(mk);
528 });
529 });
530 return moves;
531 } else {
532 // I'm pushed: add potential nudges
533 let potentialNudges = [];
534 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
535 if (
536 V.OnBoard(x + step[0], y + step[1]) &&
537 this.board[x + step[0]][y + step[1]] == V.EMPTY
538 ) {
539 const newDirCode = Object.keys(V.LANCER_DIRS).find(k => {
540 const codeStep = V.LANCER_DIRS[k];
541 return (codeStep[0] == step[0] && codeStep[1] == step[1]);
542 });
543 potentialNudges.push(
544 this.getBasicMove(
545 [x, y],
546 [x + step[0], y + step[1]],
547 { c: color, p: newDirCode }
548 )
549 );
550 }
551 }
552 return monodirMoves.concat(potentialNudges);
553 }
554 }
555
556 getPotentialSentryMoves([x, y]) {
557 // The sentry moves a priori like a bishop:
558 let moves = super.getPotentialBishopMoves([x, y]);
559 // ...but captures are replaced by special move, if and only if
560 // "captured" piece can move now, considered as the capturer unit.
561 // --> except is subTurn == 2, in this case I don't push anything.
562 if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1);
563 moves.forEach(m => {
564 if (m.vanish.length == 2) {
565 // Temporarily cancel the sentry capture:
566 m.appear.pop();
567 m.vanish.pop();
568 }
569 });
570 const color = this.getColor(x, y);
571 const fMoves = moves.filter(m => {
572 // Can the pushed unit make any move? ...resulting in a non-self-check?
573 if (m.appear.length == 0) {
574 let res = false;
575 this.play(m);
576 let moves2 = this.getPotentialMovesFrom([m.end.x, m.end.y]);
577 for (let m2 of moves2) {
578 this.play(m2);
579 res = !this.underCheck(color);
580 this.undo(m2);
581 if (res) break;
582 }
583 this.undo(m);
584 return res;
585 }
586 return true;
587 });
588 return fMoves;
589 }
590
591 getPotentialJailerMoves([x, y]) {
592 return super.getPotentialRookMoves([x, y]).filter(m => {
593 // Remove jailer captures
594 return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
595 });
596 }
597
598 getPotentialKingMoves(sq) {
599 const moves = this.getSlideNJumpMoves(
600 sq,
601 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
602 "oneStep"
603 );
604 return (
605 this.subTurn == 1
606 ? moves.concat(this.getCastleMoves(sq))
607 : moves
608 );
609 }
610
611 // Adapted: castle with jailer possible
612 getCastleMoves([x, y]) {
613 const c = this.getColor(x, y);
614 const firstRank = (c == "w" ? V.size.x - 1 : 0);
615 if (x != firstRank || y != this.INIT_COL_KING[c])
616 return [];
617
618 const oppCol = V.GetOppCol(c);
619 let moves = [];
620 let i = 0;
621 // King, then rook or jailer:
622 const finalSquares = [
623 [2, 3],
624 [V.size.y - 2, V.size.y - 3]
625 ];
626 castlingCheck: for (
627 let castleSide = 0;
628 castleSide < 2;
629 castleSide++
630 ) {
631 if (this.castleFlags[c][castleSide] >= 8) continue;
632 // Rook (or jailer) and king are on initial position
633 const finDist = finalSquares[castleSide][0] - y;
634 let step = finDist / Math.max(1, Math.abs(finDist));
635 i = y;
636 do {
637 if (
638 this.isAttacked([x, i], [oppCol]) ||
639 (this.board[x][i] != V.EMPTY &&
640 (this.getColor(x, i) != c ||
641 ![V.KING, V.ROOK, V.JAILER].includes(this.getPiece(x, i))))
642 ) {
643 continue castlingCheck;
644 }
645 i += step;
646 } while (i != finalSquares[castleSide][0]);
647 step = castleSide == 0 ? -1 : 1;
648 const rookOrJailerPos = this.castleFlags[c][castleSide];
649 for (i = y + step; i != rookOrJailerPos; i += step)
650 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
651
652 // Nothing on final squares, except maybe king and castling rook or jailer?
653 for (i = 0; i < 2; i++) {
654 if (
655 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
656 this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
657 finalSquares[castleSide][i] != rookOrJailerPos
658 ) {
659 continue castlingCheck;
660 }
661 }
662
663 // If this code is reached, castle is valid
664 const castlingPiece = this.getPiece(firstRank, rookOrJailerPos);
665 moves.push(
666 new Move({
667 appear: [
668 new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
669 new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c })
670 ],
671 vanish: [
672 new PiPo({ x: x, y: y, p: V.KING, c: c }),
673 new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c })
674 ],
675 end:
676 Math.abs(y - rookOrJailerPos) <= 2
677 ? { x: x, y: rookOrJailerPos }
678 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
679 })
680 );
681 }
682
683 return moves;
684 }
685
686 atLeastOneMove() {
687 // If in second-half of a move, we already know that a move is possible
688 if (this.subTurn == 2) return true;
689 return super.atLeastOneMove();
690 }
691
692 filterValid(moves) {
693 if (moves.length == 0) return [];
694 const basicFilter = (m, c) => {
695 this.play(m);
696 const res = !this.underCheck(c);
697 this.undo(m);
698 return res;
699 };
700 // Disable check tests for sentry pushes,
701 // because in this case the move isn't finished
702 let movesWithoutSentryPushes = [];
703 let movesWithSentryPushes = [];
704 moves.forEach(m => {
705 // Second condition below for special king "pass" moves
706 if (m.appear.length > 0 || m.vanish.length == 0)
707 movesWithoutSentryPushes.push(m);
708 else movesWithSentryPushes.push(m);
709 });
710 const color = this.turn;
711 const oppCol = V.GetOppCol(color);
712 const filteredMoves =
713 movesWithoutSentryPushes.filter(m => basicFilter(m, color));
714 // If at least one full move made, everything is allowed.
715 // Else: forbid checks and captures.
716 return (
717 this.movesCount >= 2
718 ? filteredMoves
719 : filteredMoves.filter(m => {
720 return (m.vanish.length <= 1 && basicFilter(m, oppCol));
721 })
722 ).concat(movesWithSentryPushes);
723 }
724
725 getAllValidMoves() {
726 if (this.subTurn == 1) return super.getAllValidMoves();
727 // Sentry push:
728 const sentrySq = [this.sentryPos.x, this.sentryPos.y];
729 return this.filterValid(this.getPotentialMovesFrom(sentrySq));
730 }
731
732 prePlay(move) {
733 if (move.appear.length == 0 && move.vanish.length == 1)
734 // The sentry is about to push a piece: subTurn goes from 1 to 2
735 this.sentryPos = { x: move.end.x, y: move.end.y };
736 if (this.subTurn == 2 && move.vanish[0].p != V.PAWN) {
737 // A piece is pushed: forbid array of squares between start and end
738 // of move, included (except if it's a pawn)
739 let squares = [];
740 if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
741 // short-range pieces: just forbid initial square
742 squares.push({ x: move.start.x, y: move.start.y });
743 else {
744 const deltaX = move.end.x - move.start.x;
745 const deltaY = move.end.y - move.start.y;
746 const step = [
747 deltaX / Math.abs(deltaX) || 0,
748 deltaY / Math.abs(deltaY) || 0
749 ];
750 for (
751 let sq = {x: move.start.x, y: move.start.y};
752 sq.x != move.end.x || sq.y != move.end.y;
753 sq.x += step[0], sq.y += step[1]
754 ) {
755 squares.push({ x: sq.x, y: sq.y });
756 }
757 }
758 // Add end square as well, to know if I was pushed (useful for lancers)
759 squares.push({ x: move.end.x, y: move.end.y });
760 this.sentryPush.push(squares);
761 } else this.sentryPush.push(null);
762 }
763
764 play(move) {
765 // if (!this.states) this.states = [];
766 // const stateFen = this.getFen();
767 // this.states.push(stateFen);
768
769 this.prePlay(move);
770 move.flags = JSON.stringify(this.aggregateFlags());
771 this.epSquares.push(this.getEpSquare(move));
772 V.PlayOnBoard(this.board, move);
773 // Is it a sentry push? (useful for undo)
774 move.sentryPush = (this.subTurn == 2);
775 if (this.subTurn == 1) this.movesCount++;
776 if (move.appear.length == 0 && move.vanish.length == 1) this.subTurn = 2;
777 else {
778 // Turn changes only if not a sentry "pre-push"
779 this.turn = V.GetOppCol(this.turn);
780 this.subTurn = 1;
781 }
782 this.postPlay(move);
783 }
784
785 postPlay(move) {
786 if (move.vanish.length == 0 || this.subTurn == 2)
787 // Special pass move of the king, or sentry pre-push: nothing to update
788 return;
789 const c = move.vanish[0].c;
790 const piece = move.vanish[0].p;
791 const firstRank = c == "w" ? V.size.x - 1 : 0;
792
793 if (piece == V.KING) {
794 this.kingPos[c][0] = move.appear[0].x;
795 this.kingPos[c][1] = move.appear[0].y;
796 this.castleFlags[c] = [V.size.y, V.size.y];
797 return;
798 }
799 // Update castling flags if rooks are moved
800 const oppCol = V.GetOppCol(c);
801 const oppFirstRank = V.size.x - 1 - firstRank;
802 if (
803 move.start.x == firstRank && //our rook moves?
804 this.castleFlags[c].includes(move.start.y)
805 ) {
806 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
807 this.castleFlags[c][flagIdx] = V.size.y;
808 } else if (
809 move.end.x == oppFirstRank && //we took opponent rook?
810 this.castleFlags[oppCol].includes(move.end.y)
811 ) {
812 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
813 this.castleFlags[oppCol][flagIdx] = V.size.y;
814 }
815 }
816
817 undo(move) {
818 this.epSquares.pop();
819 this.disaggregateFlags(JSON.parse(move.flags));
820 V.UndoOnBoard(this.board, move);
821 // Decrement movesCount except if the move is a sentry push
822 if (!move.sentryPush) this.movesCount--;
823 if (this.subTurn == 2) this.subTurn = 1;
824 else {
825 this.turn = V.GetOppCol(this.turn);
826 if (move.sentryPush) this.subTurn = 2;
827 }
828 this.postUndo(move);
829
830 // const stateFen = this.getFen();
831 // if (stateFen != this.states[this.states.length-1]) debugger;
832 // this.states.pop();
833 }
834
835 postUndo(move) {
836 super.postUndo(move);
837 this.sentryPush.pop();
838 }
839
840 isAttacked(sq, colors) {
841 return (
842 super.isAttacked(sq, colors) ||
843 this.isAttackedByLancer(sq, colors) ||
844 this.isAttackedBySentry(sq, colors)
845 );
846 }
847
848 isAttackedBySlideNJump([x, y], colors, piece, steps, oneStep) {
849 for (let step of steps) {
850 let rx = x + step[0],
851 ry = y + step[1];
852 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
853 rx += step[0];
854 ry += step[1];
855 }
856 if (
857 V.OnBoard(rx, ry) &&
858 this.getPiece(rx, ry) === piece &&
859 colors.includes(this.getColor(rx, ry)) &&
860 !this.isImmobilized([rx, ry])
861 ) {
862 return true;
863 }
864 }
865 return false;
866 }
867
868 isAttackedByPawn([x, y], colors) {
869 for (let c of colors) {
870 const pawnShift = c == "w" ? 1 : -1;
871 if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
872 for (let i of [-1, 1]) {
873 if (
874 y + i >= 0 &&
875 y + i < V.size.y &&
876 this.getPiece(x + pawnShift, y + i) == V.PAWN &&
877 this.getColor(x + pawnShift, y + i) == c &&
878 !this.isImmobilized([x + pawnShift, y + i])
879 ) {
880 return true;
881 }
882 }
883 }
884 }
885 return false;
886 }
887
888 isAttackedByLancer([x, y], colors) {
889 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
890 // If in this direction there are only enemy pieces and empty squares,
891 // and we meet a lancer: can he reach us?
892 // NOTE: do not stop at first lancer, there might be several!
893 let coord = { x: x + step[0], y: y + step[1] };
894 let lancerPos = [];
895 while (
896 V.OnBoard(coord.x, coord.y) &&
897 (
898 this.board[coord.x][coord.y] == V.EMPTY ||
899 colors.includes(this.getColor(coord.x, coord.y))
900 )
901 ) {
902 if (
903 this.getPiece(coord.x, coord.y) == V.LANCER &&
904 !this.isImmobilized([coord.x, coord.y])
905 ) {
906 lancerPos.push({x: coord.x, y: coord.y});
907 }
908 coord.x += step[0];
909 coord.y += step[1];
910 }
911 for (let xy of lancerPos) {
912 const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
913 if (dir[0] == -step[0] && dir[1] == -step[1]) return true;
914 }
915 }
916 return false;
917 }
918
919 // Helper to check sentries attacks:
920 selfAttack([x1, y1], [x2, y2]) {
921 const color = this.getColor(x1, y1);
922 const sliderAttack = (allowedSteps, lancer) => {
923 const deltaX = x2 - x1,
924 absDeltaX = Math.abs(deltaX);
925 const deltaY = y2 - y1,
926 absDeltaY = Math.abs(deltaY);
927 const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ];
928 if (
929 // Check that the step is a priori valid:
930 (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) ||
931 allowedSteps.every(st => st[0] != step[0] || st[1] != step[1])
932 ) {
933 return false;
934 }
935 let sq = [ x1 + step[0], y1 + step[1] ];
936 while (sq[0] != x2 && sq[1] != y2) {
937 if (
938 // NOTE: no need to check OnBoard in this special case
939 (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) ||
940 (!!lancer && this.getColor(sq[0], sq[1]) != color)
941 ) {
942 return false;
943 }
944 sq[0] += step[0];
945 sq[1] += step[1];
946 }
947 return true;
948 };
949 switch (this.getPiece(x1, y1)) {
950 case V.PAWN: {
951 // Pushed pawns move as enemy pawns
952 const shift = (color == 'w' ? 1 : -1);
953 return (x1 + shift == x2 && Math.abs(y1 - y2) == 1);
954 }
955 case V.KNIGHT: {
956 const deltaX = Math.abs(x1 - x2);
957 const deltaY = Math.abs(y1 - y2);
958 return (
959 deltaX + deltaY == 3 &&
960 [1, 2].includes(deltaX) &&
961 [1, 2].includes(deltaY)
962 );
963 }
964 case V.ROOK:
965 return sliderAttack(V.steps[V.ROOK]);
966 case V.BISHOP:
967 return sliderAttack(V.steps[V.BISHOP]);
968 case V.QUEEN:
969 return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
970 case V.LANCER: {
971 // Special case: as long as no enemy units stands in-between, it attacks
972 // (if it points toward the king).
973 const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)];
974 return sliderAttack([allowedStep], "lancer");
975 }
976 // No sentries or jailer tests: they cannot self-capture
977 }
978 return false;
979 }
980
981 isAttackedBySentry([x, y], colors) {
982 // Attacked by sentry means it can self-take our king.
983 // Just check diagonals of enemy sentry(ies), and if it reaches
984 // one of our pieces: can I self-take?
985 const color = V.GetOppCol(colors[0]);
986 let candidates = [];
987 for (let i=0; i<V.size.x; i++) {
988 for (let j=0; j<V.size.y; j++) {
989 if (
990 this.getPiece(i,j) == V.SENTRY &&
991 colors.includes(this.getColor(i,j)) &&
992 !this.isImmobilized([i, j])
993 ) {
994 for (let step of V.steps[V.BISHOP]) {
995 let sq = [ i + step[0], j + step[1] ];
996 while (
997 V.OnBoard(sq[0], sq[1]) &&
998 this.board[sq[0]][sq[1]] == V.EMPTY
999 ) {
1000 sq[0] += step[0];
1001 sq[1] += step[1];
1002 }
1003 if (
1004 V.OnBoard(sq[0], sq[1]) &&
1005 this.getColor(sq[0], sq[1]) == color
1006 ) {
1007 candidates.push([ sq[0], sq[1] ]);
1008 }
1009 }
1010 }
1011 }
1012 }
1013 for (let c of candidates)
1014 if (this.selfAttack(c, [x, y])) return true;
1015 return false;
1016 }
1017
1018 // Jailer doesn't capture or give check
1019
1020 static get VALUES() {
1021 return Object.assign(
1022 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
1023 ChessRules.VALUES
1024 );
1025 }
1026
1027 getComputerMove() {
1028 const maxeval = V.INFINITY;
1029 const color = this.turn;
1030 let moves1 = this.getAllValidMoves();
1031
1032 if (moves1.length == 0)
1033 // TODO: this situation should not happen
1034 return null;
1035
1036 const setEval = (move, next) => {
1037 const score = this.getCurrentScore();
1038 const curEval = move.eval;
1039 if (score != "*") {
1040 move.eval =
1041 score == "1/2"
1042 ? 0
1043 : (score == "1-0" ? 1 : -1) * maxeval;
1044 } else move.eval = this.evalPosition();
1045 if (
1046 // "next" is defined after sentry pushes
1047 !!next && (
1048 !curEval ||
1049 color == 'w' && move.eval > curEval ||
1050 color == 'b' && move.eval < curEval
1051 )
1052 ) {
1053 move.second = next;
1054 }
1055 };
1056
1057 // Just search_depth == 1 (because of sentries. TODO: can do better...)
1058 moves1.forEach(m1 => {
1059 this.play(m1);
1060 if (this.subTurn == 1) setEval(m1);
1061 else {
1062 // Need to play every pushes and count:
1063 const moves2 = this.getAllValidMoves();
1064 moves2.forEach(m2 => {
1065 this.play(m2);
1066 setEval(m1, m2);
1067 this.undo(m2);
1068 });
1069 }
1070 this.undo(m1);
1071 });
1072
1073 moves1.sort((a, b) => {
1074 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
1075 });
1076 let candidates = [0];
1077 for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
1078 candidates.push(j);
1079 const choice = moves1[candidates[randInt(candidates.length)]];
1080 return (!choice.second ? choice : [choice, choice.second]);
1081 }
1082
1083 getNotation(move) {
1084 // Special case "king takes jailer" is a pass move
1085 if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
1086 if (this.subTurn == 2) {
1087 // Do not consider appear[1] (sentry) for sentry pushes
1088 const simpleMove = {
1089 appear: [move.appear[0]],
1090 vanish: move.vanish,
1091 start: move.start,
1092 end: move.end
1093 };
1094 return super.getNotation(simpleMove);
1095 }
1096 return super.getNotation(move);
1097 }
1098 };