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 static get LANCER_DIRS() {
34 const piece
= this.board
[i
][j
].charAt(1);
35 // Special lancer case: 8 possible orientations
36 if (Object
.keys(V
.LANCER_DIRS
).includes(piece
)) return V
.LANCER
;
41 if ([V
.JAILER
, V
.SENTRY
].concat(Object
.keys(V
.LANCER_DIRS
)).includes(b
[1]))
42 return "Eightpieces/" + b
;
46 static ParseFen(fen
) {
47 const fenParts
= fen
.split(" ");
48 return Object
.assign(ChessRules
.ParseFen(fen
), {
49 sentrypush: fenParts
[5]
54 return super.getFen() + " " + this.getSentrypushFen();
58 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
62 const L
= this.sentryPush
.length
;
63 if (!this.sentryPush
[L
-1]) return "-";
65 this.sentryPush
[L
-1].forEach(coords
=>
66 res
+= V
.CoordsToSquare(coords
) + ",");
67 return res
.slice(0, -1);
70 setOtherVariables(fen
) {
71 super.setOtherVariables(fen
);
72 // subTurn == 2 only when a sentry moved, and is about to push something
74 // Stack pieces' forbidden squares after a sentry move at each turn
75 const parsedFen
= V
.ParseFen(fen
);
76 if (parsedFen
.sentrypush
== "-") this.sentryPush
= [null];
79 parsedFen
.sentrypush
.split(",").map(sq
=> {
80 return V
.SquareToCoords(sq
);
86 canTake([x1
,y1
], [x2
, y2
]) {
87 if (this.subTurn
== 2)
88 // Sentry push: pieces can capture own color (only)
89 return this.getColor(x1
, y1
) == this.getColor(x2
, y2
);
90 return super.canTake([x1
,y1
], [x2
, y2
]);
93 static GenRandInitFen(randomness
) {
96 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -";
98 let pieces
= { w: new Array(8), b: new Array(8) };
99 // Shuffle pieces on first (and last rank if randomness == 2)
100 for (let c
of ["w", "b"]) {
101 if (c
== 'b' && randomness
== 1) {
102 pieces
['b'] = pieces
['w'];
106 let positions
= ArrayFun
.range(8);
108 // Get random squares for bishop and sentry
109 let randIndex
= 2 * randInt(4);
110 let bishopPos
= positions
[randIndex
];
111 // The sentry must be on a square of different color
112 let randIndex_tmp
= 2 * randInt(4) + 1;
113 let sentryPos
= positions
[randIndex_tmp
];
115 // Check if white sentry is on the same color as ours.
116 // If yes: swap bishop and sentry positions.
117 if ((pieces
['w'].indexOf('s') - sentryPos
) % 2 == 0)
118 [bishopPos
, sentryPos
] = [sentryPos
, bishopPos
];
120 positions
.splice(Math
.max(randIndex
, randIndex_tmp
), 1);
121 positions
.splice(Math
.min(randIndex
, randIndex_tmp
), 1);
123 // Get random squares for knight and lancer
124 randIndex
= randInt(6);
125 const knightPos
= positions
[randIndex
];
126 positions
.splice(randIndex
, 1);
127 randIndex
= randInt(5);
128 const lancerPos
= positions
[randIndex
];
129 positions
.splice(randIndex
, 1);
131 // Get random square for queen
132 randIndex
= randInt(4);
133 const queenPos
= positions
[randIndex
];
134 positions
.splice(randIndex
, 1);
136 // Rook, jailer and king positions are now almost fixed,
137 // only the ordering rook-> jailer or jailer->rook must be decided.
138 let rookPos
= positions
[0];
139 let jailerPos
= positions
[2];
140 const kingPos
= positions
[1];
141 if (Math
.random() < 0.5) [rookPos
, jailerPos
] = [jailerPos
, rookPos
];
143 pieces
[c
][rookPos
] = "r";
144 pieces
[c
][knightPos
] = "n";
145 pieces
[c
][bishopPos
] = "b";
146 pieces
[c
][queenPos
] = "q";
147 pieces
[c
][kingPos
] = "k";
148 pieces
[c
][sentryPos
] = "s";
149 // Lancer faces north for white, and south for black:
150 pieces
[c
][lancerPos
] = c
== 'w' ? 'c' : 'g';
151 pieces
[c
][jailerPos
] = "j";
154 pieces
["b"].join("") +
155 "/pppppppp/8/8/8/8/PPPPPPPP/" +
156 pieces
["w"].join("").toUpperCase() +
161 // Scan kings, rooks and jailers
162 scanKingsRooks(fen
) {
163 this.INIT_COL_KING
= { w: -1, b: -1 };
164 this.INIT_COL_ROOK
= { w: -1, b: -1 };
165 this.INIT_COL_JAILER
= { w: -1, b: -1 };
166 this.kingPos
= { w: [-1, -1], b: [-1, -1] };
167 const fenRows
= V
.ParseFen(fen
).position
.split("/");
168 const startRow
= { 'w': V
.size
.x
- 1, 'b': 0 };
169 for (let i
= 0; i
< fenRows
.length
; i
++) {
171 for (let j
= 0; j
< fenRows
[i
].length
; j
++) {
172 switch (fenRows
[i
].charAt(j
)) {
174 this.kingPos
["b"] = [i
, k
];
175 this.INIT_COL_KING
["b"] = k
;
178 this.kingPos
["w"] = [i
, k
];
179 this.INIT_COL_KING
["w"] = k
;
182 if (i
== startRow
['b'] && this.INIT_COL_ROOK
["b"] < 0)
183 this.INIT_COL_ROOK
["b"] = k
;
186 if (i
== startRow
['w'] && this.INIT_COL_ROOK
["w"] < 0)
187 this.INIT_COL_ROOK
["w"] = k
;
190 if (i
== startRow
['b'] && this.INIT_COL_JAILER
["b"] < 0)
191 this.INIT_COL_JAILER
["b"] = k
;
194 if (i
== startRow
['w'] && this.INIT_COL_JAILER
["w"] < 0)
195 this.INIT_COL_JAILER
["w"] = k
;
198 const num
= parseInt(fenRows
[i
].charAt(j
));
199 if (!isNaN(num
)) k
+= num
- 1;
207 // Is piece on square (x,y) immobilized?
208 isImmobilized([x
, y
]) {
209 const color
= this.getColor(x
, y
);
210 const oppCol
= V
.GetOppCol(color
);
211 for (let step
of V
.steps
[V
.ROOK
]) {
212 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
215 this.board
[i
][j
] != V
.EMPTY
&&
216 this.getColor(i
, j
) == oppCol
218 const oppPiece
= this.getPiece(i
, j
);
219 if (oppPiece
== V
.JAILER
) return [i
, j
];
225 getPotentialMovesFrom_aux([x
, y
]) {
226 switch (this.getPiece(x
, y
)) {
228 return this.getPotentialJailerMoves([x
, y
]);
230 return this.getPotentialSentryMoves([x
, y
]);
232 return this.getPotentialLancerMoves([x
, y
]);
234 return super.getPotentialMovesFrom([x
, y
]);
238 getPotentialMovesFrom([x
,y
]) {
239 if (this.subTurn
== 1) {
240 if (!!this.isImmobilized([x
, y
])) return [];
241 return this.getPotentialMovesFrom_aux([x
, y
]);
243 // subTurn == 2: only the piece pushed by the sentry is allowed to move,
244 // as if the sentry didn't exist
245 if (x
!= this.sentryPos
.x
&& y
!= this.sentryPos
.y
) return [];
246 return this.getPotentialMovesFrom_aux([x
, y
]);
250 let moves
= super.getAllValidMoves().filter(m
=> {
251 // Remove jailer captures
252 return m
.vanish
[0].p
!= V
.JAILER
|| m
.vanish
.length
== 1;
254 const L
= this.sentryPush
.length
;
255 if (!!this.sentryPush
[L
-1] && this.subTurn
== 1) {
256 // Delete moves walking back on sentry push path
257 moves
= moves
.filter(m
=> {
259 m
.vanish
[0].p
!= V
.PAWN
&&
260 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
271 // Disable check tests when subTurn == 2, because the move isn't finished
272 if (this.subTurn
== 2) return moves
;
273 const filteredMoves
= super.filterValid(moves
);
274 // If at least one full move made, everything is allowed:
275 if (this.movesCount
>= 2) return filteredMoves
;
276 // Else, forbid check and captures:
277 const oppCol
= V
.GetOppCol(this.turn
);
278 return filteredMoves
.filter(m
=> {
279 if (m
.vanish
.length
== 2 && m
.appear
.length
== 1) return false;
281 const res
= !this.underCheck(oppCol
);
287 // Obtain all lancer moves in "step" direction,
288 // without final re-orientation.
289 getPotentialLancerMoves_aux([x
, y
], step
) {
291 // Add all moves to vacant squares until opponent is met:
292 const oppCol
= V
.GetOppCol(this.turn
);
293 let sq
= [x
+ step
[0], y
+ step
[1]];
294 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
295 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
296 moves
.push(this.getBasicMove([x
, y
], sq
));
300 if (V
.OnBoard(sq
[0], sq
[1]))
301 // Add capturing move
302 moves
.push(this.getBasicMove([x
, y
], sq
));
306 getPotentialLancerMoves([x
, y
]) {
308 // Add all lancer possible orientations, similar to pawn promotions.
309 // Except if just after a push: allow all movements from init square then
310 if (!!this.sentryPath
[L
-1]) {
311 // Maybe I was pushed
312 const pl
= this.sentryPath
[L
-1].length
;
314 this.sentryPath
[L
-1][pl
-1].x
== x
&&
315 this.sentryPath
[L
-1][pl
-1].y
== y
317 // I was pushed: allow all directions (for this move only), but
318 // do not change direction after moving.
319 Object
.values(V
.LANCER_DIRS
).forEach(step
=> {
320 Array
.prototype.push
.apply(
322 this.getPotentialLancerMoves_aux([x
, y
], step
)
328 // I wasn't pushed: standard lancer move
329 const dirCode
= this.board
[x
][y
][1];
331 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
332 // Add all possible orientations aftermove:
333 monodirMoves
.forEach(m
=> {
334 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
335 let mk
= JSON
.parse(JSON
.stringify(m
));
343 getPotentialSentryMoves([x
, y
]) {
344 // The sentry moves a priori like a bishop:
345 let moves
= super.getPotentialBishopMoves([x
, y
]);
346 // ...but captures are replaced by special move
348 if (m
.vanish
.length
== 2) {
349 // Temporarily cancel the sentry capture:
357 getPotentialJailerMoves([x
, y
]) {
358 // Captures are removed afterward:
359 return super.getPotentialRookMoves([x
, y
]);
362 getPotentialKingMoves([x
, y
]) {
363 let moves
= super.getPotentialKingMoves([x
, y
]);
364 // Augment with pass move is the king is immobilized:
365 const jsq
= this.isImmobilized([x
, y
]);
371 start: { x: x
, y: y
},
372 end: { x: jsq
[0], y: jsq
[1] }
379 // Adapted: castle with jailer possible
380 getCastleMoves([x
, y
]) {
381 const c
= this.getColor(x
, y
);
382 const firstRank
= (c
== "w" ? V
.size
.x
- 1 : 0);
383 if (x
!= firstRank
|| y
!= this.INIT_COL_KING
[c
])
386 const oppCol
= V
.GetOppCol(c
);
389 // King, then rook or jailer:
390 const finalSquares
= [
392 [V
.size
.y
- 2, V
.size
.y
- 3]
399 if (!this.castleFlags
[c
][castleSide
]) continue;
400 // Rook (or jailer) and king are on initial position
402 const finDist
= finalSquares
[castleSide
][0] - y
;
403 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
407 this.isAttacked([x
, i
], [oppCol
]) ||
408 (this.board
[x
][i
] != V
.EMPTY
&&
409 (this.getColor(x
, i
) != c
||
410 ![V
.KING
, V
.ROOK
].includes(this.getPiece(x
, i
))))
412 continue castlingCheck
;
415 } while (i
!= finalSquares
[castleSide
][0]);
417 step
= castleSide
== 0 ? -1 : 1;
418 const rookOrJailerPos
=
420 ? Math
.min(this.INIT_COL_ROOK
[c
], this.INIT_COL_JAILER
[c
])
421 : Math
.max(this.INIT_COL_ROOK
[c
], this.INIT_COL_JAILER
[c
]);
422 for (i
= y
+ step
; i
!= rookOrJailerPos
; i
+= step
)
423 if (this.board
[x
][i
] != V
.EMPTY
) continue castlingCheck
;
425 // Nothing on final squares, except maybe king and castling rook or jailer?
426 for (i
= 0; i
< 2; i
++) {
428 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
429 this.getPiece(x
, finalSquares
[castleSide
][i
]) != V
.KING
&&
430 finalSquares
[castleSide
][i
] != rookOrJailerPos
432 continue castlingCheck
;
436 // If this code is reached, castle is valid
437 const castlingPiece
= this.getPiece(firstRank
, rookOrJailerPos
);
441 new PiPo({ x: x
, y: finalSquares
[castleSide
][0], p: V
.KING
, c: c
}),
442 new PiPo({ x: x
, y: finalSquares
[castleSide
][1], p: castlingPiece
, c: c
})
445 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
446 new PiPo({ x: x
, y: rookOrJailerPos
, p: castlingPiece
, c: c
})
449 Math
.abs(y
- rookOrJailerPos
) <= 2
450 ? { x: x
, y: rookOrJailerPos
}
451 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
459 updateVariables(move) {
460 super.updateVariables(move);
461 if (this.subTurn
== 2) {
462 // A piece is pushed: forbid array of squares between start and end
463 // of move, included (except if it's a pawn)
465 if (move.vanish
[0].p
!= V
.PAWN
) {
466 if ([V
.KNIGHT
,V
.KING
].insludes(move.vanish
[0].p
))
467 // short-range pieces: just forbid initial square
468 squares
.push(move.start
);
470 const deltaX
= move.end
.x
- move.start
.x
;
471 const deltaY
= move.end
.y
- move.start
.y
;
473 deltaX
/ Math
.abs(deltaX
) || 0,
474 deltaY
/ Math
.abs(deltaY
) || 0
477 let sq
= {x: x
, y: y
};
478 sq
.x
!= move.end
.x
&& sq
.y
!= move.end
.y
;
479 sq
.x
+= step
[0], sq
.y
+= step
[1]
484 // Add end square as well, to know if I was pushed (useful for lancers)
485 squares
.push(move.end
);
487 this.sentryPush
.push(squares
);
488 } else this.sentryPush
.push(null);
492 move.flags
= JSON
.stringify(this.aggregateFlags());
493 this.epSquares
.push(this.getEpSquare(move));
494 V
.PlayOnBoard(this.board
, move);
495 if (this.subTurn
== 1) this.movesCount
++;
496 this.updateVariables(move);
497 // move.sentryPush indicates that sentry is *about to* push
498 move.sentryPush
= (move.appear
.length
== 0 && move.vanish
.length
== 1);
499 // Turn changes only if not a sentry "pre-push" or subTurn == 2 (push)
500 if (!move.sentryPush
|| this.subTurn
== 2)
501 this.turn
= V
.GetOppCol(this.turn
);
505 this.epSquares
.pop();
506 this.disaggregateFlags(JSON
.parse(move.flags
));
507 V
.UndoOnBoard(this.board
, move);
508 if (this.subTurn
== 2) this.movesCount
--;
509 this.unupdateVariables(move);
510 // Turn changes only if not undoing second part of a sentry push
511 if (!move.sentryPush
|| this.subTurn
== 1)
512 this.turn
= V
.GetOppCol(this.turn
);
515 static get VALUES() {
516 return Object
.assign(
517 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
523 // Special case "king takes jailer" is a pass move
524 if (move.appear
.length
== 0 && move.vanish
.length
== 0) return "pass";
525 return super.getNotation(move);