a799cff1503b6aa13dab165fcc905cbb63cea539
[vchess.git] / client / src / variants / Eightpieces.js
1 import { ArrayFun } from "@/utils/array";
2 import { randInt, shuffle } 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 PIECES() {
17 return ChessRules.PIECES.concat([V.JAILER, V.SENTRY, V.LANCER]);
18 }
19
20 // Lancer directions *from white perspective*
21 static get LANCER_DIRS() {
22 return {
23 'c': [-1, 0], //north
24 'd': [-1, 1], //N-E
25 'e': [0, 1], //east
26 'f': [1, 1], //S-E
27 'g': [1, 0], //south
28 'h': [1, -1], //S-W
29 'm': [0, -1], //west
30 'o': [-1, -1] //N-W
31 };
32 }
33
34 getPiece(i, j) {
35 const piece = this.board[i][j].charAt(1);
36 // Special lancer case: 8 possible orientations
37 if (Object.keys(V.LANCER_DIRS).includes(piece)) return V.LANCER;
38 return piece;
39 }
40
41 getPpath(b) {
42 if ([V.JAILER, V.SENTRY].concat(Object.keys(V.LANCER_DIRS)).includes(b[1]))
43 return "Eightpieces/" + b;
44 return b;
45 }
46
47 static ParseFen(fen) {
48 const fenParts = fen.split(" ");
49 return Object.assign(ChessRules.ParseFen(fen), {
50 sentrypush: fenParts[5]
51 });
52 }
53
54 getFen() {
55 return super.getFen() + " " + this.getSentrypushFen();
56 }
57
58 getFenForRepeat() {
59 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
60 }
61
62 getSentrypushFen() {
63 const L = this.sentryPush.length;
64 if (!this.sentryPush[L-1]) return "-";
65 let res = "";
66 this.sentryPush[L-1].forEach(coords =>
67 res += V.CoordsToSquare(coords) + ",");
68 return res.slice(0, -1);
69 }
70
71 setOtherVariables(fen) {
72 super.setOtherVariables(fen);
73 // subTurn == 2 only when a sentry moved, and is about to push something
74 this.subTurn = 1;
75 // Pushing sentry position, updated after each push (subTurn == 1)
76 this.sentryPos = { x: -1, y: -1 };
77 // Stack pieces' forbidden squares after a sentry move at each turn
78 const parsedFen = V.ParseFen(fen);
79 if (parsedFen.sentrypush == "-") this.sentryPush = [null];
80 else {
81 this.sentryPush = [
82 parsedFen.sentrypush.split(",").map(sq => {
83 return V.SquareToCoords(sq);
84 })
85 ];
86 }
87 }
88
89 static GenRandInitFen(randomness) {
90 if (randomness == 0)
91 // Deterministic:
92 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -";
93
94 let pieces = { w: new Array(8), b: new Array(8) };
95 // Shuffle pieces on first (and last rank if randomness == 2)
96 for (let c of ["w", "b"]) {
97 if (c == 'b' && randomness == 1) {
98 const lancerIdx = pieces['w'].findIndex(p => {
99 return Object.keys(V.LANCER_DIRS).includes(p);
100 });
101 pieces['b'] =
102 pieces['w'].slice(0, lancerIdx)
103 .concat(['g'])
104 .concat(pieces['w'].slice(lancerIdx + 1));
105 break;
106 }
107
108 let positions = ArrayFun.range(8);
109
110 // Get random squares for bishop and sentry
111 let randIndex = 2 * randInt(4);
112 let bishopPos = positions[randIndex];
113 // The sentry must be on a square of different color
114 let randIndex_tmp = 2 * randInt(4) + 1;
115 let sentryPos = positions[randIndex_tmp];
116 if (c == 'b') {
117 // Check if white sentry is on the same color as ours.
118 // If yes: swap bishop and sentry positions.
119 if ((pieces['w'].indexOf('s') - sentryPos) % 2 == 0)
120 [bishopPos, sentryPos] = [sentryPos, bishopPos];
121 }
122 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
123 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
124
125 // Get random squares for knight and lancer
126 randIndex = randInt(6);
127 const knightPos = positions[randIndex];
128 positions.splice(randIndex, 1);
129 randIndex = randInt(5);
130 const lancerPos = positions[randIndex];
131 positions.splice(randIndex, 1);
132
133 // Get random square for queen
134 randIndex = randInt(4);
135 const queenPos = positions[randIndex];
136 positions.splice(randIndex, 1);
137
138 // Rook, jailer and king positions are now almost fixed,
139 // only the ordering rook-> jailer or jailer->rook must be decided.
140 let rookPos = positions[0];
141 let jailerPos = positions[2];
142 const kingPos = positions[1];
143 if (Math.random() < 0.5) [rookPos, jailerPos] = [jailerPos, rookPos];
144
145 pieces[c][rookPos] = "r";
146 pieces[c][knightPos] = "n";
147 pieces[c][bishopPos] = "b";
148 pieces[c][queenPos] = "q";
149 pieces[c][kingPos] = "k";
150 pieces[c][sentryPos] = "s";
151 // Lancer faces north for white, and south for black:
152 pieces[c][lancerPos] = c == 'w' ? 'c' : 'g';
153 pieces[c][jailerPos] = "j";
154 }
155 return (
156 pieces["b"].join("") +
157 "/pppppppp/8/8/8/8/PPPPPPPP/" +
158 pieces["w"].join("").toUpperCase() +
159 " w 0 1111 - -"
160 );
161 }
162
163 // Scan kings, rooks and jailers
164 scanKingsRooks(fen) {
165 this.INIT_COL_KING = { w: -1, b: -1 };
166 this.INIT_COL_ROOK = { w: -1, b: -1 };
167 this.INIT_COL_JAILER = { w: -1, b: -1 };
168 this.kingPos = { w: [-1, -1], b: [-1, -1] };
169 const fenRows = V.ParseFen(fen).position.split("/");
170 const startRow = { 'w': V.size.x - 1, 'b': 0 };
171 for (let i = 0; i < fenRows.length; i++) {
172 let k = 0;
173 for (let j = 0; j < fenRows[i].length; j++) {
174 switch (fenRows[i].charAt(j)) {
175 case "k":
176 this.kingPos["b"] = [i, k];
177 this.INIT_COL_KING["b"] = k;
178 break;
179 case "K":
180 this.kingPos["w"] = [i, k];
181 this.INIT_COL_KING["w"] = k;
182 break;
183 case "r":
184 if (i == startRow['b'] && this.INIT_COL_ROOK["b"] < 0)
185 this.INIT_COL_ROOK["b"] = k;
186 break;
187 case "R":
188 if (i == startRow['w'] && this.INIT_COL_ROOK["w"] < 0)
189 this.INIT_COL_ROOK["w"] = k;
190 break;
191 case "j":
192 if (i == startRow['b'] && this.INIT_COL_JAILER["b"] < 0)
193 this.INIT_COL_JAILER["b"] = k;
194 break;
195 case "J":
196 if (i == startRow['w'] && this.INIT_COL_JAILER["w"] < 0)
197 this.INIT_COL_JAILER["w"] = k;
198 break;
199 default: {
200 const num = parseInt(fenRows[i].charAt(j));
201 if (!isNaN(num)) k += num - 1;
202 }
203 }
204 k++;
205 }
206 }
207 }
208
209 // Is piece on square (x,y) immobilized?
210 isImmobilized([x, y]) {
211 const color = this.getColor(x, y);
212 const oppCol = V.GetOppCol(color);
213 for (let step of V.steps[V.ROOK]) {
214 const [i, j] = [x + step[0], y + step[1]];
215 if (
216 V.OnBoard(i, j) &&
217 this.board[i][j] != V.EMPTY &&
218 this.getColor(i, j) == oppCol
219 ) {
220 if (this.getPiece(i, j) == V.JAILER) return [i, j];
221 }
222 }
223 return null;
224 }
225
226 // Because of the lancers, getPiece() could be wrong:
227 // use board[x][y][1] instead (always valid).
228 getBasicMove([sx, sy], [ex, ey], tr) {
229 let mv = new Move({
230 appear: [
231 new PiPo({
232 x: ex,
233 y: ey,
234 c: tr ? tr.c : this.getColor(sx, sy),
235 p: tr ? tr.p : this.board[sx][sy][1]
236 })
237 ],
238 vanish: [
239 new PiPo({
240 x: sx,
241 y: sy,
242 c: this.getColor(sx, sy),
243 p: this.board[sx][sy][1]
244 })
245 ]
246 });
247
248 // The opponent piece disappears if we take it
249 if (this.board[ex][ey] != V.EMPTY) {
250 mv.vanish.push(
251 new PiPo({
252 x: ex,
253 y: ey,
254 c: this.getColor(ex, ey),
255 p: this.board[ex][ey][1]
256 })
257 );
258 }
259
260 return mv;
261 }
262
263 canIplay(side, [x, y]) {
264 return (
265 (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side) ||
266 (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y)
267 );
268 }
269
270 getPotentialMovesFrom([x,y]) {
271 // At subTurn == 2, jailers aren't effective (Jeff K)
272 if (this.subTurn == 1 && !!this.isImmobilized([x, y])) return [];
273 if (this.subTurn == 2) {
274 // Temporarily change pushed piece color.
275 // (Not using getPiece() because of lancers)
276 var oppCol = this.getColor(x, y);
277 var color = V.GetOppCol(oppCol);
278 var saveXYstate = this.board[x][y];
279 this.board[x][y] = color + this.board[x][y][1];
280 }
281 let moves = [];
282 switch (this.getPiece(x, y)) {
283 case V.JAILER:
284 moves = this.getPotentialJailerMoves([x, y]);
285 break;
286 case V.SENTRY:
287 moves = this.getPotentialSentryMoves([x, y]);
288 break;
289 case V.LANCER:
290 moves = this.getPotentialLancerMoves([x, y]);
291 break;
292 default:
293 moves = super.getPotentialMovesFrom([x, y]);
294 break;
295 }
296 const L = this.sentryPush.length;
297 if (!!this.sentryPush[L-1]) {
298 // Delete moves walking back on sentry push path
299 moves = moves.filter(m => {
300 if (
301 m.vanish[0].p != V.PAWN &&
302 this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
303 ) {
304 return false;
305 }
306 return true;
307 });
308 }
309 if (this.subTurn == 2) {
310 // Don't forget to re-add the sentry on the board:
311 // Also fix color of pushed piece afterward:
312 moves.forEach(m => {
313 m.appear.push({x: x, y: y, p: V.SENTRY, c: color});
314 m.appear[0].c = oppCol;
315 m.vanish[0].c = oppCol;
316 });
317 }
318 return moves;
319 }
320
321 getPotentialPawnMoves([x, y]) {
322 const color = this.getColor(x, y);
323 let moves = [];
324 const [sizeX, sizeY] = [V.size.x, V.size.y];
325 const shiftX = color == "w" ? -1 : 1;
326 const startRank = color == "w" ? sizeX - 2 : 1;
327 const lastRank = color == "w" ? 0 : sizeX - 1;
328
329 const finalPieces =
330 x + shiftX == lastRank
331 ?
332 Object.keys(V.LANCER_DIRS).concat(
333 [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER])
334 : [V.PAWN];
335 if (this.board[x + shiftX][y] == V.EMPTY) {
336 // One square forward
337 for (let piece of finalPieces) {
338 moves.push(
339 this.getBasicMove([x, y], [x + shiftX, y], {
340 c: color,
341 p: piece
342 })
343 );
344 }
345 if (
346 x == startRank &&
347 this.board[x + 2 * shiftX][y] == V.EMPTY
348 ) {
349 // Two squares jump
350 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
351 }
352 }
353 // Captures
354 for (let shiftY of [-1, 1]) {
355 if (
356 y + shiftY >= 0 &&
357 y + shiftY < sizeY &&
358 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
359 this.canTake([x, y], [x + shiftX, y + shiftY])
360 ) {
361 for (let piece of finalPieces) {
362 moves.push(
363 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
364 c: color,
365 p: piece
366 })
367 );
368 }
369 }
370 }
371
372 // En passant:
373 const Lep = this.epSquares.length;
374 const epSquare = this.epSquares[Lep - 1]; //always at least one element
375 if (
376 !!epSquare &&
377 epSquare.x == x + shiftX &&
378 Math.abs(epSquare.y - y) == 1
379 ) {
380 let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
381 enpassantMove.vanish.push({
382 x: x,
383 y: epSquare.y,
384 p: "p",
385 c: this.getColor(x, epSquare.y)
386 });
387 moves.push(enpassantMove);
388 }
389
390 return moves;
391 }
392
393 // Obtain all lancer moves in "step" direction,
394 // without final re-orientation.
395 getPotentialLancerMoves_aux([x, y], step) {
396 let moves = [];
397 // Add all moves to vacant squares until opponent is met:
398 const oppCol = V.GetOppCol(this.getColor(x, y));
399 let sq = [x + step[0], y + step[1]];
400 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
401 if (this.board[sq[0]][sq[1]] == V.EMPTY)
402 moves.push(this.getBasicMove([x, y], sq));
403 sq[0] += step[0];
404 sq[1] += step[1];
405 }
406 if (V.OnBoard(sq[0], sq[1]))
407 // Add capturing move
408 moves.push(this.getBasicMove([x, y], sq));
409 return moves;
410 }
411
412 getPotentialLancerMoves([x, y]) {
413 let moves = [];
414 // Add all lancer possible orientations, similar to pawn promotions.
415 // Except if just after a push: allow all movements from init square then
416 const L = this.sentryPush.length;
417 if (!!this.sentryPush[L-1]) {
418 // Maybe I was pushed
419 const pl = this.sentryPush[L-1].length;
420 if (
421 this.sentryPush[L-1][pl-1].x == x &&
422 this.sentryPush[L-1][pl-1].y == y
423 ) {
424 // I was pushed: allow all directions (for this move only), but
425 // do not change direction after moving.
426 Object.values(V.LANCER_DIRS).forEach(step => {
427 Array.prototype.push.apply(
428 moves,
429 this.getPotentialLancerMoves_aux([x, y], step)
430 );
431 });
432 return moves;
433 }
434 }
435 // I wasn't pushed: standard lancer move
436 const dirCode = this.board[x][y][1];
437 const monodirMoves =
438 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
439 // Add all possible orientations aftermove except if I'm being pushed
440 if (this.subTurn == 1) {
441 monodirMoves.forEach(m => {
442 Object.keys(V.LANCER_DIRS).forEach(k => {
443 let mk = JSON.parse(JSON.stringify(m));
444 mk.appear[0].p = k;
445 moves.push(mk);
446 });
447 });
448 return moves;
449 } else return monodirMoves;
450 }
451
452 getPotentialSentryMoves([x, y]) {
453 // The sentry moves a priori like a bishop:
454 let moves = super.getPotentialBishopMoves([x, y]);
455 // ...but captures are replaced by special move, if and only if
456 // "captured" piece can move now, considered as the capturer unit.
457 moves.forEach(m => {
458 if (m.vanish.length == 2) {
459 // Temporarily cancel the sentry capture:
460 m.appear.pop();
461 m.vanish.pop();
462 }
463 });
464 // Can the pushed unit make any move? ...resulting in a non-self-check?
465 const color = this.getColor(x, y);
466 const fMoves = moves.filter(m => {
467 // Sentry push?
468 if (m.appear.length == 0) {
469 let res = false;
470 this.play(m);
471 let moves2 = this.filterValid(
472 this.getPotentialMovesFrom([m.end.x, m.end.y]));
473 for (let m2 of moves2) {
474 this.play(m2);
475 res = !this.underCheck(color);
476 this.undo(m2);
477 if (res) break;
478 }
479 this.undo(m);
480 return res;
481 }
482 return true;
483 });
484 return fMoves;
485 }
486
487 getPotentialJailerMoves([x, y]) {
488 return super.getPotentialRookMoves([x, y]).filter(m => {
489 // Remove jailer captures
490 return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
491 });
492 }
493
494 getPotentialKingMoves([x, y]) {
495 let moves = super.getPotentialKingMoves([x, y]);
496 // Augment with pass move is the king is immobilized:
497 const jsq = this.isImmobilized([x, y]);
498 if (!!jsq) {
499 moves.push(
500 new Move({
501 appear: [],
502 vanish: [],
503 start: { x: x, y: y },
504 end: { x: jsq[0], y: jsq[1] }
505 })
506 );
507 }
508 return moves;
509 }
510
511 // Adapted: castle with jailer possible
512 getCastleMoves([x, y]) {
513 const c = this.getColor(x, y);
514 const firstRank = (c == "w" ? V.size.x - 1 : 0);
515 if (x != firstRank || y != this.INIT_COL_KING[c])
516 return [];
517
518 const oppCol = V.GetOppCol(c);
519 let moves = [];
520 let i = 0;
521 // King, then rook or jailer:
522 const finalSquares = [
523 [2, 3],
524 [V.size.y - 2, V.size.y - 3]
525 ];
526 castlingCheck: for (
527 let castleSide = 0;
528 castleSide < 2;
529 castleSide++
530 ) {
531 if (!this.castleFlags[c][castleSide]) continue;
532 // Rook (or jailer) and king are on initial position
533
534 const finDist = finalSquares[castleSide][0] - y;
535 let step = finDist / Math.max(1, Math.abs(finDist));
536 i = y;
537 do {
538 if (
539 this.isAttacked([x, i], [oppCol]) ||
540 (this.board[x][i] != V.EMPTY &&
541 (this.getColor(x, i) != c ||
542 ![V.KING, V.ROOK].includes(this.getPiece(x, i))))
543 ) {
544 continue castlingCheck;
545 }
546 i += step;
547 } while (i != finalSquares[castleSide][0]);
548
549 step = castleSide == 0 ? -1 : 1;
550 const rookOrJailerPos =
551 castleSide == 0
552 ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c])
553 : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]);
554 for (i = y + step; i != rookOrJailerPos; i += step)
555 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
556
557 // Nothing on final squares, except maybe king and castling rook or jailer?
558 for (i = 0; i < 2; i++) {
559 if (
560 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
561 this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
562 finalSquares[castleSide][i] != rookOrJailerPos
563 ) {
564 continue castlingCheck;
565 }
566 }
567
568 // If this code is reached, castle is valid
569 const castlingPiece = this.getPiece(firstRank, rookOrJailerPos);
570 moves.push(
571 new Move({
572 appear: [
573 new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
574 new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c })
575 ],
576 vanish: [
577 new PiPo({ x: x, y: y, p: V.KING, c: c }),
578 new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c })
579 ],
580 end:
581 Math.abs(y - rookOrJailerPos) <= 2
582 ? { x: x, y: rookOrJailerPos }
583 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
584 })
585 );
586 }
587
588 return moves;
589 }
590
591 filterValid(moves) {
592 // Disable check tests for sentry pushes,
593 // because in this case the move isn't finished
594 let movesWithoutSentryPushes = [];
595 let movesWithSentryPushes = [];
596 moves.forEach(m => {
597 if (m.appear.length > 0) movesWithoutSentryPushes.push(m);
598 else movesWithSentryPushes.push(m);
599 });
600 const filteredMoves = super.filterValid(movesWithoutSentryPushes)
601 // If at least one full move made, everything is allowed:
602 if (this.movesCount >= 2)
603 return filteredMoves.concat(movesWithSentryPushes);
604 // Else, forbid check and captures:
605 const oppCol = V.GetOppCol(this.turn);
606 return filteredMoves.filter(m => {
607 if (m.vanish.length == 2 && m.appear.length == 1) return false;
608 this.play(m);
609 const res = !this.underCheck(oppCol);
610 this.undo(m);
611 return res;
612 }).concat(movesWithSentryPushes);
613 }
614
615 getAllValidMoves() {
616 if (this.subTurn == 1) return super.getAllValidMoves();
617 // Sentry push:
618 const sentrySq = [this.sentryPos.x, this.SentryPos.y];
619 return this.filterValid(this.getPotentialMovesFrom(sentrySq));
620 }
621
622 updateVariables(move) {
623 const c = this.turn;
624 const piece = move.vanish[0].p;
625 const firstRank = c == "w" ? V.size.x - 1 : 0;
626
627 // Update king position + flags
628 if (piece == V.KING) {
629 this.kingPos[c][0] = move.appear[0].x;
630 this.kingPos[c][1] = move.appear[0].y;
631 this.castleFlags[c] = [false, false];
632 return;
633 }
634
635 // Update castling flags if rook or jailer moved (or is captured)
636 const oppCol = V.GetOppCol(c);
637 const oppFirstRank = V.size.x - 1 - firstRank;
638 let flagIdx = 0;
639 if (
640 // Our rook moves?
641 move.start.x == firstRank &&
642 this.INIT_COL_ROOK[c] == move.start.y
643 ) {
644 if (this.INIT_COL_ROOK[c] > this.INIT_COL_JAILER[c]) flagIdx++;
645 this.castleFlags[c][flagIdx] = false;
646 } else if (
647 // Our jailer moves?
648 move.start.x == firstRank &&
649 this.INIT_COL_JAILER[c] == move.start.y
650 ) {
651 if (this.INIT_COL_JAILER[c] > this.INIT_COL_ROOK[c]) flagIdx++;
652 this.castleFlags[c][flagIdx] = false;
653 } else if (
654 // We took opponent's rook?
655 move.end.x == oppFirstRank &&
656 this.INIT_COL_ROOK[oppCol] == move.end.y
657 ) {
658 if (this.INIT_COL_ROOK[oppCol] > this.INIT_COL_JAILER[oppCol]) flagIdx++;
659 this.castleFlags[oppCol][flagIdx] = false;
660 } else if (
661 // We took opponent's jailer?
662 move.end.x == oppFirstRank &&
663 this.INIT_COL_JAILER[oppCol] == move.end.y
664 ) {
665 if (this.INIT_COL_JAILER[oppCol] > this.INIT_COL_ROOK[oppCol]) flagIdx++;
666 this.castleFlags[oppCol][flagIdx] = false;
667 }
668
669 if (this.subTurn == 2) {
670 // A piece is pushed: forbid array of squares between start and end
671 // of move, included (except if it's a pawn)
672 let squares = [];
673 if (move.vanish[0].p != V.PAWN) {
674 if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
675 // short-range pieces: just forbid initial square
676 squares.push(move.start);
677 else {
678 const deltaX = move.end.x - move.start.x;
679 const deltaY = move.end.y - move.start.y;
680 const step = [
681 deltaX / Math.abs(deltaX) || 0,
682 deltaY / Math.abs(deltaY) || 0
683 ];
684 for (
685 let sq = {x: move.start.x, y: move.start.y};
686 sq.x != move.end.x && sq.y != move.end.y;
687 sq.x += step[0], sq.y += step[1]
688 ) {
689 squares.push(sq);
690 }
691 }
692 // Add end square as well, to know if I was pushed (useful for lancers)
693 squares.push(move.end);
694 }
695 this.sentryPush.push(squares);
696 } else this.sentryPush.push(null);
697 }
698
699 // TODO: cleaner (global) update/unupdate variables logic, rename...
700 unupdateVariables(move) {
701 super.unupdateVariables(move);
702 this.sentryPush.pop();
703 }
704
705 play(move) {
706 move.flags = JSON.stringify(this.aggregateFlags());
707 this.epSquares.push(this.getEpSquare(move));
708 V.PlayOnBoard(this.board, move);
709 if (this.subTurn == 1) this.movesCount++;
710 if (move.appear.length == 0 && move.vanish.length == 1) {
711 // The sentry is about to push a piece:
712 this.sentryPos = { x: move.end.x, y: move.end.y };
713 this.subTurn = 2;
714 } else {
715 // Turn changes only if not a sentry "pre-push"
716 this.turn = V.GetOppCol(this.turn);
717 this.subTurn = 1;
718 const L = this.sentryPush.length;
719 // Is it a sentry push? (useful for undo)
720 move.sentryPush = !!this.sentryPush[L-1];
721 }
722 this.updateVariables(move);
723 }
724
725 undo(move) {
726 this.epSquares.pop();
727 this.disaggregateFlags(JSON.parse(move.flags));
728 V.UndoOnBoard(this.board, move);
729 const L = this.sentryPush.length;
730 // Decrement movesCount except if the move is a sentry push
731 if (!move.sentryPush) this.movesCount--;
732 // Turn changes only if not undoing second part of a sentry push
733 if (!move.sentryPush || this.subTurn == 1)
734 this.turn = V.GetOppCol(this.turn);
735 this.unupdateVariables(move);
736 }
737
738 static get VALUES() {
739 return Object.assign(
740 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
741 ChessRules.VALUES
742 );
743 }
744
745 getNotation(move) {
746 // Special case "king takes jailer" is a pass move
747 if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
748 return super.getNotation(move);
749 }
750 };