1 import { ArrayFun
} from "@/utils/array";
2 import { randInt
, shuffle
} from "@/utils/alea";
3 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
5 export const VariantRules
= class EightpiecesRules
extends ChessRules
{
17 return ChessRules
.PIECES
.concat([V
.JAILER
, V
.SENTRY
, V
.LANCER
]);
20 // Lancer directions *from white perspective*
21 static get LANCER_DIRS() {
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
;
42 if ([V
.JAILER
, V
.SENTRY
].concat(Object
.keys(V
.LANCER_DIRS
)).includes(b
[1]))
43 return "Eightpieces/" + b
;
47 static ParseFen(fen
) {
48 const fenParts
= fen
.split(" ");
49 return Object
.assign(ChessRules
.ParseFen(fen
), {
50 sentrypush: fenParts
[5]
55 return super.getFen() + " " + this.getSentrypushFen();
59 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
63 const L
= this.sentryPush
.length
;
64 if (!this.sentryPush
[L
-1]) return "-";
66 this.sentryPush
[L
-1].forEach(coords
=>
67 res
+= V
.CoordsToSquare(coords
) + ",");
68 return res
.slice(0, -1);
71 setOtherVariables(fen
) {
72 super.setOtherVariables(fen
);
73 // subTurn == 2 only when a sentry moved, and is about to push something
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];
82 parsedFen
.sentrypush
.split(",").map(sq
=> {
83 return V
.SquareToCoords(sq
);
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
]);
96 static GenRandInitFen(randomness
) {
99 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -";
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
);
109 pieces
['w'].slice(0, lancerIdx
)
111 .concat(pieces
['w'].slice(lancerIdx
+ 1));
115 let positions
= ArrayFun
.range(8);
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
];
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
];
129 positions
.splice(Math
.max(randIndex
, randIndex_tmp
), 1);
130 positions
.splice(Math
.min(randIndex
, randIndex_tmp
), 1);
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);
140 // Get random square for queen
141 randIndex
= randInt(4);
142 const queenPos
= positions
[randIndex
];
143 positions
.splice(randIndex
, 1);
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
];
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";
163 pieces
["b"].join("") +
164 "/pppppppp/8/8/8/8/PPPPPPPP/" +
165 pieces
["w"].join("").toUpperCase() +
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
++) {
180 for (let j
= 0; j
< fenRows
[i
].length
; j
++) {
181 switch (fenRows
[i
].charAt(j
)) {
183 this.kingPos
["b"] = [i
, k
];
184 this.INIT_COL_KING
["b"] = k
;
187 this.kingPos
["w"] = [i
, k
];
188 this.INIT_COL_KING
["w"] = k
;
191 if (i
== startRow
['b'] && this.INIT_COL_ROOK
["b"] < 0)
192 this.INIT_COL_ROOK
["b"] = k
;
195 if (i
== startRow
['w'] && this.INIT_COL_ROOK
["w"] < 0)
196 this.INIT_COL_ROOK
["w"] = k
;
199 if (i
== startRow
['b'] && this.INIT_COL_JAILER
["b"] < 0)
200 this.INIT_COL_JAILER
["b"] = k
;
203 if (i
== startRow
['w'] && this.INIT_COL_JAILER
["w"] < 0)
204 this.INIT_COL_JAILER
["w"] = k
;
207 const num
= parseInt(fenRows
[i
].charAt(j
));
208 if (!isNaN(num
)) k
+= num
- 1;
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]];
224 this.board
[i
][j
] != V
.EMPTY
&&
225 this.getColor(i
, j
) == oppCol
227 const oppPiece
= this.getPiece(i
, j
);
228 if (oppPiece
== V
.JAILER
) return [i
, j
];
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
) {
242 c: tr
? tr
.c : this.getColor(sx
, sy
),
243 p: tr
? tr
.p : this.board
[sx
][sy
][1]
250 c: this.getColor(sx
, sy
),
251 p: this.board
[sx
][sy
][1]
256 // The opponent piece disappears if we take it
257 if (this.board
[ex
][ey
] != V
.EMPTY
) {
262 c: this.getColor(ex
, ey
),
263 p: this.board
[ex
][ey
][1]
271 getPotentialMovesFrom_aux([x
, y
]) {
272 switch (this.getPiece(x
, y
)) {
274 return this.getPotentialJailerMoves([x
, y
]);
276 return this.getPotentialSentryMoves([x
, y
]);
278 return this.getPotentialLancerMoves([x
, y
]);
280 return super.getPotentialMovesFrom([x
, y
]);
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
=> {
293 m
.vanish
[0].p
!= V
.PAWN
&&
294 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
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
});
315 getPotentialPawnMoves([x
, y
]) {
316 const color
= this.turn
;
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;
326 x
+ shiftX
== lastRank
328 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.SENTRY
, V
.JAILER
]
329 .concat(Object
.keys(V
.LANCER_DIRS
))
331 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
332 // One square forward
333 for (let piece
of finalPieces
) {
335 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
343 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
346 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
350 for (let shiftY
of [-1, 1]) {
353 y
+ shiftY
< sizeY
&&
354 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
355 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
357 for (let piece
of finalPieces
) {
359 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
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
373 epSquare
.x
== x
+ shiftX
&&
374 Math
.abs(epSquare
.y
- y
) == 1
376 let enpassantMove
= this.getBasicMove([x
, y
], [epSquare
.x
, epSquare
.y
]);
377 enpassantMove
.vanish
.push({
381 c: this.getColor(x
, epSquare
.y
)
383 moves
.push(enpassantMove
);
389 // Obtain all lancer moves in "step" direction,
390 // without final re-orientation.
391 getPotentialLancerMoves_aux([x
, y
], step
) {
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
));
402 if (V
.OnBoard(sq
[0], sq
[1]))
403 // Add capturing move
404 moves
.push(this.getBasicMove([x
, y
], sq
));
408 getPotentialLancerMoves([x
, y
]) {
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
;
417 this.sentryPush
[L
-1][pl
-1].x
== x
&&
418 this.sentryPush
[L
-1][pl
-1].y
== y
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(
425 this.getPotentialLancerMoves_aux([x
, y
], step
)
431 // I wasn't pushed: standard lancer move
432 const dirCode
= this.board
[x
][y
][1];
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
));
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.
452 if (m
.vanish
.length
== 2) {
453 // Temporarily cancel the sentry capture:
458 // Can the pushed unit make any move?
460 const fMoves
= moves
.filter(m
=> {
461 V
.PlayOnBoard(this.board
, m
);
463 (this.filterValid(this.getPotentialMovesFrom([x
, y
])).length
> 0);
464 V
.UndoOnBoard(this.board
, m
);
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;
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
]);
487 start: { x: x
, y: y
},
488 end: { x: jsq
[0], y: jsq
[1] }
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
])
502 const oppCol
= V
.GetOppCol(c
);
505 // King, then rook or jailer:
506 const finalSquares
= [
508 [V
.size
.y
- 2, V
.size
.y
- 3]
515 if (!this.castleFlags
[c
][castleSide
]) continue;
516 // Rook (or jailer) and king are on initial position
518 const finDist
= finalSquares
[castleSide
][0] - y
;
519 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
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
))))
528 continue castlingCheck
;
531 } while (i
!= finalSquares
[castleSide
][0]);
533 step
= castleSide
== 0 ? -1 : 1;
534 const rookOrJailerPos
=
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
;
541 // Nothing on final squares, except maybe king and castling rook or jailer?
542 for (i
= 0; i
< 2; i
++) {
544 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
545 this.getPiece(x
, finalSquares
[castleSide
][i
]) != V
.KING
&&
546 finalSquares
[castleSide
][i
] != rookOrJailerPos
548 continue castlingCheck
;
552 // If this code is reached, castle is valid
553 const castlingPiece
= this.getPiece(firstRank
, rookOrJailerPos
);
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
})
561 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
562 new PiPo({ x: x
, y: rookOrJailerPos
, p: castlingPiece
, c: c
})
565 Math
.abs(y
- rookOrJailerPos
) <= 2
566 ? { x: x
, y: rookOrJailerPos
}
567 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
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;
586 const res
= !this.underCheck(oppCol
);
592 updateVariables(move) {
594 const piece
= move.vanish
[0].p
;
595 const firstRank
= c
== "w" ? V
.size
.x
- 1 : 0;
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];
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
;
611 move.start
.x
== firstRank
&&
612 this.INIT_COL_ROOK
[c
] == move.start
.y
614 if (this.INIT_COL_ROOK
[c
] > this.INIT_COL_JAILER
[c
]) flagIdx
++;
615 this.castleFlags
[c
][flagIdx
] = false;
618 move.start
.x
== firstRank
&&
619 this.INIT_COL_JAILER
[c
] == move.start
.y
621 if (this.INIT_COL_JAILER
[c
] > this.INIT_COL_ROOK
[c
]) flagIdx
++;
622 this.castleFlags
[c
][flagIdx
] = false;
624 // We took opponent's rook?
625 move.end
.x
== oppFirstRank
&&
626 this.INIT_COL_ROOK
[oppCol
] == move.end
.y
628 if (this.INIT_COL_ROOK
[oppCol
] > this.INIT_COL_JAILER
[oppCol
]) flagIdx
++;
629 this.castleFlags
[oppCol
][flagIdx
] = false;
631 // We took opponent's jailer?
632 move.end
.x
== oppFirstRank
&&
633 this.INIT_COL_JAILER
[oppCol
] == move.end
.y
635 if (this.INIT_COL_JAILER
[oppCol
] > this.INIT_COL_ROOK
[oppCol
]) flagIdx
++;
636 this.castleFlags
[oppCol
][flagIdx
] = false;
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)
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
);
648 const deltaX
= move.end
.x
- move.start
.x
;
649 const deltaY
= move.end
.y
- move.start
.y
;
651 deltaX
/ Math
.abs(deltaX
) || 0,
652 deltaY
/ Math
.abs(deltaY
) || 0
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]
662 // Add end square as well, to know if I was pushed (useful for lancers)
663 squares
.push(move.end
);
665 this.sentryPush
.push(squares
);
666 } else this.sentryPush
.push(null);
669 // TODO: cleaner (global) update/unupdate variables logic, rename...
670 unupdateVariables(move) {
671 super.unupdateVariables(move);
672 this.sentryPush
.pop();
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
};
686 // Turn changes only if not a sentry "pre-push"
687 this.turn
= V
.GetOppCol(this.turn
);
689 const L
= this.sentryPush
.length
;
690 // Is it a sentry push? (useful for undo)
691 move.sentryPush
= !!this.sentryPush
[L
-1];
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
);
708 static get VALUES() {
709 return Object
.assign(
710 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
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);