c83cf5ed48a5b2576ddd1cc3d4766b10d12da5af
[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 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 // Sentry position just after a "capture" (subTurn from 1 to 2)
76 this.sentryPos = null;
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].charAt(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].charAt(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].charAt(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].charAt(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 else 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 // No promotions after pushes!
331 x + shiftX == lastRank && this.subTurn == 1
332 ?
333 Object.keys(V.LANCER_DIRS).concat(
334 [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER])
335 : [V.PAWN];
336 if (this.board[x + shiftX][y] == V.EMPTY) {
337 // One square forward
338 for (let piece of finalPieces) {
339 moves.push(
340 this.getBasicMove([x, y], [x + shiftX, y], {
341 c: color,
342 p: piece
343 })
344 );
345 }
346 if (
347 x == startRank &&
348 this.board[x + 2 * shiftX][y] == V.EMPTY
349 ) {
350 // Two squares jump
351 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
352 }
353 }
354 // Captures
355 for (let shiftY of [-1, 1]) {
356 if (
357 y + shiftY >= 0 &&
358 y + shiftY < sizeY &&
359 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
360 this.canTake([x, y], [x + shiftX, y + shiftY])
361 ) {
362 for (let piece of finalPieces) {
363 moves.push(
364 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
365 c: color,
366 p: piece
367 })
368 );
369 }
370 }
371 }
372
373 // En passant:
374 const Lep = this.epSquares.length;
375 const epSquare = this.epSquares[Lep - 1]; //always at least one element
376 if (
377 !!epSquare &&
378 epSquare.x == x + shiftX &&
379 Math.abs(epSquare.y - y) == 1
380 ) {
381 let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
382 enpassantMove.vanish.push({
383 x: x,
384 y: epSquare.y,
385 p: "p",
386 c: this.getColor(x, epSquare.y)
387 });
388 moves.push(enpassantMove);
389 }
390
391 return moves;
392 }
393
394 // Obtain all lancer moves in "step" direction,
395 // without final re-orientation.
396 getPotentialLancerMoves_aux([x, y], step, tr) {
397 let moves = [];
398 // Add all moves to vacant squares until opponent is met:
399 const oppCol = V.GetOppCol(this.getColor(x, y));
400 let sq = [x + step[0], y + step[1]];
401 while (V.OnBoard(sq[0], sq[1]) && this.getColor(sq[0], sq[1]) != oppCol) {
402 if (this.board[sq[0]][sq[1]] == V.EMPTY)
403 moves.push(this.getBasicMove([x, y], sq, tr));
404 sq[0] += step[0];
405 sq[1] += step[1];
406 }
407 if (V.OnBoard(sq[0], sq[1]))
408 // Add capturing move
409 moves.push(this.getBasicMove([x, y], sq, tr));
410 return moves;
411 }
412
413 getPotentialLancerMoves([x, y]) {
414 let moves = [];
415 // Add all lancer possible orientations, similar to pawn promotions.
416 // Except if just after a push: allow all movements from init square then
417 const L = this.sentryPush.length;
418 if (!!this.sentryPush[L-1]) {
419 // Maybe I was pushed
420 const pl = this.sentryPush[L-1].length;
421 if (
422 this.sentryPush[L-1][pl-1].x == x &&
423 this.sentryPush[L-1][pl-1].y == y
424 ) {
425 // I was pushed: allow all directions (for this move only), but
426 // do not change direction after moving.
427 const color = this.getColor(x, y);
428 Object.values(V.LANCER_DIRS).forEach(step => {
429 const dirCode = Object.keys(V.LANCER_DIRS).find(k => {
430 return (V.LANCER_DIRS[k][0] == step[0] && V.LANCER_DIRS[k][1] == step[1]);
431 });
432 Array.prototype.push.apply(
433 moves,
434 this.getPotentialLancerMoves_aux([x, y], step, { p: dirCode, c: color })
435 );
436 });
437 return moves;
438 }
439 }
440 // I wasn't pushed: standard lancer move
441 const dirCode = this.board[x][y][1];
442 const monodirMoves =
443 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
444 // Add all possible orientations aftermove except if I'm being pushed
445 if (this.subTurn == 1) {
446 monodirMoves.forEach(m => {
447 Object.keys(V.LANCER_DIRS).forEach(k => {
448 let mk = JSON.parse(JSON.stringify(m));
449 mk.appear[0].p = k;
450 moves.push(mk);
451 });
452 });
453 return moves;
454 } else return monodirMoves;
455 }
456
457 getPotentialSentryMoves([x, y]) {
458 // The sentry moves a priori like a bishop:
459 let moves = super.getPotentialBishopMoves([x, y]);
460 // ...but captures are replaced by special move, if and only if
461 // "captured" piece can move now, considered as the capturer unit.
462 // --> except is subTurn == 2, in this case I don't push anything.
463 if (this.subTurn == 2) return moves.filter(m => m.vanish.length == 1);
464 moves.forEach(m => {
465 if (m.vanish.length == 2) {
466 // Temporarily cancel the sentry capture:
467 m.appear.pop();
468 m.vanish.pop();
469 }
470 });
471 // Can the pushed unit make any move? ...resulting in a non-self-check?
472 const color = this.getColor(x, y);
473 const fMoves = moves.filter(m => {
474 // Sentry push?
475 if (m.appear.length == 0) {
476 let res = false;
477 this.play(m);
478 let moves2 = this.filterValid(
479 this.getPotentialMovesFrom([m.end.x, m.end.y]));
480 for (let m2 of moves2) {
481 this.play(m2);
482 res = !this.underCheck(color);
483 this.undo(m2);
484 if (res) break;
485 }
486 this.undo(m);
487 return res;
488 }
489 return true;
490 });
491 return fMoves;
492 }
493
494 getPotentialJailerMoves([x, y]) {
495 return super.getPotentialRookMoves([x, y]).filter(m => {
496 // Remove jailer captures
497 return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
498 });
499 }
500
501 getPotentialKingMoves([x, y]) {
502 let moves = super.getPotentialKingMoves([x, y]);
503 // Augment with pass move is the king is immobilized:
504 const jsq = this.isImmobilized([x, y]);
505 if (!!jsq) {
506 moves.push(
507 new Move({
508 appear: [],
509 vanish: [],
510 start: { x: x, y: y },
511 end: { x: jsq[0], y: jsq[1] }
512 })
513 );
514 }
515 return moves;
516 }
517
518 // Adapted: castle with jailer possible
519 getCastleMoves([x, y]) {
520 const c = this.getColor(x, y);
521 const firstRank = (c == "w" ? V.size.x - 1 : 0);
522 if (x != firstRank || y != this.INIT_COL_KING[c])
523 return [];
524
525 const oppCol = V.GetOppCol(c);
526 let moves = [];
527 let i = 0;
528 // King, then rook or jailer:
529 const finalSquares = [
530 [2, 3],
531 [V.size.y - 2, V.size.y - 3]
532 ];
533 castlingCheck: for (
534 let castleSide = 0;
535 castleSide < 2;
536 castleSide++
537 ) {
538 if (!this.castleFlags[c][castleSide]) continue;
539 // Rook (or jailer) and king are on initial position
540
541 const finDist = finalSquares[castleSide][0] - y;
542 let step = finDist / Math.max(1, Math.abs(finDist));
543 i = y;
544 do {
545 if (
546 this.isAttacked([x, i], [oppCol]) ||
547 (this.board[x][i] != V.EMPTY &&
548 (this.getColor(x, i) != c ||
549 ![V.KING, V.ROOK].includes(this.getPiece(x, i))))
550 ) {
551 continue castlingCheck;
552 }
553 i += step;
554 } while (i != finalSquares[castleSide][0]);
555
556 step = castleSide == 0 ? -1 : 1;
557 const rookOrJailerPos =
558 castleSide == 0
559 ? Math.min(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c])
560 : Math.max(this.INIT_COL_ROOK[c], this.INIT_COL_JAILER[c]);
561 for (i = y + step; i != rookOrJailerPos; i += step)
562 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
563
564 // Nothing on final squares, except maybe king and castling rook or jailer?
565 for (i = 0; i < 2; i++) {
566 if (
567 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
568 this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
569 finalSquares[castleSide][i] != rookOrJailerPos
570 ) {
571 continue castlingCheck;
572 }
573 }
574
575 // If this code is reached, castle is valid
576 const castlingPiece = this.getPiece(firstRank, rookOrJailerPos);
577 moves.push(
578 new Move({
579 appear: [
580 new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
581 new PiPo({ x: x, y: finalSquares[castleSide][1], p: castlingPiece, c: c })
582 ],
583 vanish: [
584 new PiPo({ x: x, y: y, p: V.KING, c: c }),
585 new PiPo({ x: x, y: rookOrJailerPos, p: castlingPiece, c: c })
586 ],
587 end:
588 Math.abs(y - rookOrJailerPos) <= 2
589 ? { x: x, y: rookOrJailerPos }
590 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
591 })
592 );
593 }
594
595 return moves;
596 }
597
598 filterValid(moves) {
599 // Disable check tests for sentry pushes,
600 // because in this case the move isn't finished
601 let movesWithoutSentryPushes = [];
602 let movesWithSentryPushes = [];
603 moves.forEach(m => {
604 if (m.appear.length > 0) movesWithoutSentryPushes.push(m);
605 else movesWithSentryPushes.push(m);
606 });
607 const filteredMoves = super.filterValid(movesWithoutSentryPushes);
608 // If at least one full move made, everything is allowed:
609 if (this.movesCount >= 2)
610 return filteredMoves.concat(movesWithSentryPushes);
611 // Else, forbid checks and captures:
612 const oppCol = V.GetOppCol(this.turn);
613 return filteredMoves.filter(m => {
614 if (m.vanish.length == 2 && m.appear.length == 1) return false;
615 this.play(m);
616 const res = !this.underCheck(oppCol);
617 this.undo(m);
618 return res;
619 }).concat(movesWithSentryPushes);
620 }
621
622 getAllValidMoves() {
623 if (this.subTurn == 1) return super.getAllValidMoves();
624 // Sentry push:
625 const sentrySq = [this.sentryPos.x, this.sentryPos.y];
626 return this.filterValid(this.getPotentialMovesFrom(sentrySq));
627 }
628
629 updateVariables(move) {
630 const c = this.turn;
631 const piece = move.vanish[0].p;
632 const firstRank = (c == "w" ? V.size.x - 1 : 0);
633
634 // Update king position + flags
635 if (piece == V.KING) {
636 this.kingPos[c][0] = move.appear[0].x;
637 this.kingPos[c][1] = move.appear[0].y;
638 this.castleFlags[c] = [false, false];
639 return;
640 }
641
642 // Update castling flags if rook or jailer moved (or is captured)
643 const oppCol = V.GetOppCol(c);
644 const oppFirstRank = V.size.x - 1 - firstRank;
645 let flagIdx = 0;
646 if (
647 // Our rook moves?
648 move.start.x == firstRank &&
649 this.INIT_COL_ROOK[c] == move.start.y
650 ) {
651 if (this.INIT_COL_ROOK[c] > this.INIT_COL_JAILER[c]) flagIdx++;
652 this.castleFlags[c][flagIdx] = false;
653 } else if (
654 // Our jailer moves?
655 move.start.x == firstRank &&
656 this.INIT_COL_JAILER[c] == move.start.y
657 ) {
658 if (this.INIT_COL_JAILER[c] > this.INIT_COL_ROOK[c]) flagIdx++;
659 this.castleFlags[c][flagIdx] = false;
660 } else if (
661 // We took opponent's rook?
662 move.end.x == oppFirstRank &&
663 this.INIT_COL_ROOK[oppCol] == move.end.y
664 ) {
665 if (this.INIT_COL_ROOK[oppCol] > this.INIT_COL_JAILER[oppCol]) flagIdx++;
666 this.castleFlags[oppCol][flagIdx] = false;
667 } else if (
668 // We took opponent's jailer?
669 move.end.x == oppFirstRank &&
670 this.INIT_COL_JAILER[oppCol] == move.end.y
671 ) {
672 if (this.INIT_COL_JAILER[oppCol] > this.INIT_COL_ROOK[oppCol]) flagIdx++;
673 this.castleFlags[oppCol][flagIdx] = false;
674 }
675
676 if (move.appear.length == 0 && move.vanish.length == 1) {
677 // The sentry is about to push a piece: subTurn goes from 1 to 2
678 this.sentryPos = { x: move.end.x, y: move.end.y };
679 } else if (this.subTurn == 2) {
680 // A piece is pushed: forbid array of squares between start and end
681 // of move, included (except if it's a pawn)
682 let squares = [];
683 if (move.vanish[0].p != V.PAWN) {
684 if ([V.KNIGHT,V.KING].includes(move.vanish[0].p))
685 // short-range pieces: just forbid initial square
686 squares.push(move.start);
687 else {
688 const deltaX = move.end.x - move.start.x;
689 const deltaY = move.end.y - move.start.y;
690 const step = [
691 deltaX / Math.abs(deltaX) || 0,
692 deltaY / Math.abs(deltaY) || 0
693 ];
694 for (
695 let sq = {x: move.start.x, y: move.start.y};
696 sq.x != move.end.x && sq.y != move.end.y;
697 sq.x += step[0], sq.y += step[1]
698 ) {
699 squares.push(sq);
700 }
701 }
702 // Add end square as well, to know if I was pushed (useful for lancers)
703 squares.push(move.end);
704 }
705 this.sentryPush.push(squares);
706 } else this.sentryPush.push(null);
707 }
708
709 // TODO: cleaner (global) update/unupdate variables logic, rename...
710 unupdateVariables(move) {
711 super.unupdateVariables(move);
712 this.sentryPush.pop();
713 }
714
715 play(move) {
716 move.flags = JSON.stringify(this.aggregateFlags());
717 this.epSquares.push(this.getEpSquare(move));
718 V.PlayOnBoard(this.board, move);
719 this.updateVariables(move);
720 // Is it a sentry push? (useful for undo)
721 move.sentryPush = (this.subTurn == 2);
722 if (this.subTurn == 1) this.movesCount++;
723 if (move.appear.length == 0 && move.vanish.length == 1) this.subTurn = 2;
724 else {
725 // Turn changes only if not a sentry "pre-push"
726 this.turn = V.GetOppCol(this.turn);
727 this.subTurn = 1;
728 }
729 }
730
731 undo(move) {
732 this.epSquares.pop();
733 this.disaggregateFlags(JSON.parse(move.flags));
734 V.UndoOnBoard(this.board, move);
735 const L = this.sentryPush.length;
736 // Decrement movesCount except if the move is a sentry push
737 if (!move.sentryPush) this.movesCount--;
738 // Turn changes only if not undoing second part of a sentry push
739 if (!move.sentryPush || this.subTurn == 1)
740 this.turn = V.GetOppCol(this.turn);
741 this.unupdateVariables(move);
742 }
743
744 isAttacked(sq, colors) {
745 return (
746 super.isAttacked(sq, colors) ||
747 this.isAttackedByLancer(sq, colors) ||
748 this.isAttackedBySentry(sq, colors)
749 );
750 }
751
752 isAttackedByLancer([x, y], colors) {
753 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
754 // If in this direction there are only enemy pieces and empty squares,
755 // and we meet a lancer: can he reach us?
756 // NOTE: do not stop at first lancer, there might be several!
757 let coord = { x: x + step[0], y: y + step[1] };
758 let lancerPos = [];
759 while (
760 V.OnBoard(coord.x, coord.y) &&
761 (
762 this.board[coord.x][coord.y] == V.EMPTY ||
763 colors.includes(this.getColor(coord.x, coord.y))
764 )
765 ) {
766 lancerPos.push(coord);
767 }
768 for (let xy of lancerPos) {
769 const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
770 if (dir[0] == -step[0] && dir[1] == -step[1]) return true;
771 }
772 }
773 return false;
774 }
775
776 // Helper to check sentries attacks:
777 selfAttack([x1, y1], [x2, y2]) {
778 const color = this.getColor(x1, y1);
779 const sliderAttack = (allowedSteps, lancer) => {
780 const deltaX = x2 - x1;
781 const deltaY = y2 - y1;
782 const step = [ deltaX / Math.abs(deltaX), deltaY / Math.abs(deltaY) ];
783 if (allowedStep.every(st => st[0] != step[0] || st[1] != step[1]))
784 return false;
785 let sq = [ x1 = step[0], y1 + step[1] ];
786 while (sq[0] != x2 && sq[1] != y2) {
787 if (
788 (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) ||
789 (!!lancer && this.getColor(sq[0], sq[1]) != color)
790 ) {
791 return false;
792 }
793 }
794 return true;
795 };
796 switch (this.getPiece(x1, y1)) {
797 case V.PAWN: {
798 // Pushed pawns move as enemy pawns
799 const shift = (color == 'w' ? 1 : -1);
800 return (x1 + shift == x2 && Math.abs(y1 - y2) == 1);
801 }
802 case V.KNIGHT: {
803 const deltaX = Math.abs(x1 - x2);
804 const deltaY = Math.abs(y1 - y2);
805 return (
806 deltaX + deltaY == 3 &&
807 [1, 2].includes(deltaX) &&
808 [1, 2].includes(deltaY)
809 );
810 }
811 case V.ROOK:
812 return sliderAttack(V.steps[V.ROOK]);
813 case V.BISHOP:
814 return sliderAttack(V.steps[V.BISHOP]);
815 case V.QUEEN:
816 return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
817 case V.LANCER: {
818 // Special case: as long as no enemy units stands in-between, it attacks
819 // (if it points toward the king).
820 const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)];
821 return sliderAttack([allowedStep], "lancer");
822 }
823 // No sentries or jailer tests: they cannot self-capture
824 }
825 return false;
826 }
827
828 isAttackedBySentry([x, y], colors) {
829 // Attacked by sentry means it can self-take our king.
830 // Just check diagonals of enemy sentry(ies), and if it reaches
831 // one of our pieces: can I self-take?
832 const color = V.GetOppCol(colors[0]);
833 let candidates = [];
834 for (let i=0; i<V.size.x; i++) {
835 for (let j=0; j<V.size.y; j++) {
836 if (
837 this.getPiece(i,j) == V.SENTRY &&
838 colors.includes(this.getColor(i,j))
839 ) {
840 for (let step of V.steps[V.BISHOP]) {
841 let sq = [ i + step[0], j + step[1] ];
842 while (
843 V.OnBoard(sq[0], sq[1]) &&
844 this.board[sq[0]][sq[1]] == V.EMPTY
845 ) {
846 sq[0] += step[0];
847 sq[1] += step[1];
848 }
849 if (
850 V.OnBoard(sq[0], sq[1]) &&
851 this.getColor(sq[0], sq[1]) == color
852 ) {
853 candidates.push(sq);
854 }
855 }
856 }
857 }
858 }
859 for (let c of candidates)
860 if (this.selfAttack(c, [x, y])) return true;
861 return false;
862 }
863
864 // Jailer doesn't capture or give check
865
866 static get VALUES() {
867 return Object.assign(
868 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
869 ChessRules.VALUES
870 );
871 }
872
873 getComputerMove() {
874 const maxeval = V.INFINITY;
875 const color = this.turn;
876 let moves1 = this.getAllValidMoves();
877
878 if (moves1.length == 0)
879 // TODO: this situation should not happen
880 return null;
881
882 const setEval = (move) => {
883 const score = this.getCurrentScore();
884 if (score != "*") {
885 move.eval =
886 score == "1/2"
887 ? 0
888 : (score == "1-0" ? 1 : -1) * maxeval;
889 } else move[i].eval = this.evalPosition();
890 };
891
892 // Just search_depth == 1 (because of sentries. TODO: can do better...)
893 moves1.forEach(m1 => {
894 this.play(m1);
895 if (this.subTurn == 1) setEval(m1);
896 else {
897 // Need to play every pushes and count:
898 const moves2 = this.getAllValidMoves();
899 moves2.forEach(m2 => {
900 this.play(m2);
901 setEval(m1);
902 this.undo(m2);
903 });
904 }
905 this.undo(m1);
906 });
907
908 moves1.sort((a, b) => {
909 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
910 });
911 let candidates = [0];
912 for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
913 candidates.push(j);
914 return moves1[candidates[randInt(candidates.length)]];
915 }
916
917 getNotation(move) {
918 // Special case "king takes jailer" is a pass move
919 if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
920 return super.getNotation(move);
921 }
922 };