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