1 import { ArrayFun
} from "@/utils/array";
2 import { randInt
} from "@/utils/alea";
3 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
5 export const VariantRules
= class EightpiecesRules
extends ChessRules
{
16 // Lancer directions *from white perspective*
17 static get LANCER_DIRS() {
31 return ChessRules
.PIECES
32 .concat([V
.JAILER
, V
.SENTRY
])
33 .concat(Object
.keys(V
.LANCER_DIRS
));
37 const piece
= this.board
[i
][j
].charAt(1);
38 // Special lancer case: 8 possible orientations
39 if (Object
.keys(V
.LANCER_DIRS
).includes(piece
)) return V
.LANCER
;
43 getPpath(b
, color
, score
, orientation
) {
44 if ([V
.JAILER
, V
.SENTRY
].includes(b
[1])) return "Eightpieces/" + b
;
45 if (Object
.keys(V
.LANCER_DIRS
).includes(b
[1])) {
46 if (orientation
== 'w') return "Eightpieces/" + b
;
47 // Find opposite direction for adequate display:
75 return "Eightpieces/" + b
[0] + oppDir
;
80 getPPpath(b
, orientation
) {
81 return this.getPpath(b
, null, null, orientation
);
84 static ParseFen(fen
) {
85 const fenParts
= fen
.split(" ");
87 ChessRules
.ParseFen(fen
),
88 { sentrypush: fenParts
[5] }
92 static IsGoodFen(fen
) {
93 if (!ChessRules
.IsGoodFen(fen
)) return false;
94 const fenParsed
= V
.ParseFen(fen
);
95 // 5) Check sentry push (if any)
97 fenParsed
.sentrypush
!= "-" &&
98 !fenParsed
.sentrypush
.match(/^([a-h][1-8],?)+$/)
106 return super.getFen() + " " + this.getSentrypushFen();
110 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
114 const L
= this.sentryPush
.length
;
115 if (!this.sentryPush
[L
-1]) return "-";
117 this.sentryPush
[L
-1].forEach(coords
=>
118 res
+= V
.CoordsToSquare(coords
) + ",");
119 return res
.slice(0, -1);
122 setOtherVariables(fen
) {
123 super.setOtherVariables(fen
);
124 // subTurn == 2 only when a sentry moved, and is about to push something
126 // Sentry position just after a "capture" (subTurn from 1 to 2)
127 this.sentryPos
= null;
128 // Stack pieces' forbidden squares after a sentry move at each turn
129 const parsedFen
= V
.ParseFen(fen
);
130 if (parsedFen
.sentrypush
== "-") this.sentryPush
= [null];
133 parsedFen
.sentrypush
.split(",").map(sq
=> {
134 return V
.SquareToCoords(sq
);
140 static GenRandInitFen(randomness
) {
143 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 ahah - -";
145 let pieces
= { w: new Array(8), b: new Array(8) };
147 // Shuffle pieces on first (and last rank if randomness == 2)
148 for (let c
of ["w", "b"]) {
149 if (c
== 'b' && randomness
== 1) {
150 const lancerIdx
= pieces
['w'].findIndex(p
=> {
151 return Object
.keys(V
.LANCER_DIRS
).includes(p
);
154 pieces
['w'].slice(0, lancerIdx
)
156 .concat(pieces
['w'].slice(lancerIdx
+ 1));
161 let positions
= ArrayFun
.range(8);
163 // Get random squares for bishop and sentry
164 let randIndex
= 2 * randInt(4);
165 let bishopPos
= positions
[randIndex
];
166 // The sentry must be on a square of different color
167 let randIndex_tmp
= 2 * randInt(4) + 1;
168 let sentryPos
= positions
[randIndex_tmp
];
170 // Check if white sentry is on the same color as ours.
171 // If yes: swap bishop and sentry positions.
172 if ((pieces
['w'].indexOf('s') - sentryPos
) % 2 == 0)
173 [bishopPos
, sentryPos
] = [sentryPos
, bishopPos
];
175 positions
.splice(Math
.max(randIndex
, randIndex_tmp
), 1);
176 positions
.splice(Math
.min(randIndex
, randIndex_tmp
), 1);
178 // Get random squares for knight and lancer
179 randIndex
= randInt(6);
180 const knightPos
= positions
[randIndex
];
181 positions
.splice(randIndex
, 1);
182 randIndex
= randInt(5);
183 const lancerPos
= positions
[randIndex
];
184 positions
.splice(randIndex
, 1);
186 // Get random square for queen
187 randIndex
= randInt(4);
188 const queenPos
= positions
[randIndex
];
189 positions
.splice(randIndex
, 1);
191 // Rook, jailer and king positions are now almost fixed,
192 // only the ordering rook-> jailer or jailer->rook must be decided.
193 let rookPos
= positions
[0];
194 let jailerPos
= positions
[2];
195 const kingPos
= positions
[1];
196 flags
+= V
.CoordToColumn(rookPos
) + V
.CoordToColumn(jailerPos
);
197 if (Math
.random() < 0.5) [rookPos
, jailerPos
] = [jailerPos
, rookPos
];
199 pieces
[c
][rookPos
] = "r";
200 pieces
[c
][knightPos
] = "n";
201 pieces
[c
][bishopPos
] = "b";
202 pieces
[c
][queenPos
] = "q";
203 pieces
[c
][kingPos
] = "k";
204 pieces
[c
][sentryPos
] = "s";
205 // Lancer faces north for white, and south for black:
206 pieces
[c
][lancerPos
] = c
== 'w' ? 'c' : 'g';
207 pieces
[c
][jailerPos
] = "j";
210 pieces
["b"].join("") +
211 "/pppppppp/8/8/8/8/PPPPPPPP/" +
212 pieces
["w"].join("").toUpperCase() +
213 " w 0 " + flags
+ " - -"
217 canTake([x1
, y1
], [x2
, y2
]) {
218 if (this.subTurn
== 2)
219 // Only self captures on this subturn:
220 return this.getColor(x1
, y1
) == this.getColor(x2
, y2
);
221 return super.canTake([x1
, y1
], [x2
, y2
]);
224 // Is piece on square (x,y) immobilized?
225 isImmobilized([x
, y
]) {
226 const color
= this.getColor(x
, y
);
227 const oppCol
= V
.GetOppCol(color
);
228 for (let step
of V
.steps
[V
.ROOK
]) {
229 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
232 this.board
[i
][j
] != V
.EMPTY
&&
233 this.getColor(i
, j
) == oppCol
235 if (this.getPiece(i
, j
) == V
.JAILER
) return [i
, j
];
241 // Because of the lancers, getPiece() could be wrong:
242 // use board[x][y][1] instead (always valid).
243 getBasicMove([sx
, sy
], [ex
, ey
], tr
) {
249 c: tr
? tr
.c : this.getColor(sx
, sy
),
250 p: tr
? tr
.p : this.board
[sx
][sy
].charAt(1)
257 c: this.getColor(sx
, sy
),
258 p: this.board
[sx
][sy
].charAt(1)
263 // The opponent piece disappears if we take it
264 if (this.board
[ex
][ey
] != V
.EMPTY
) {
269 c: this.getColor(ex
, ey
),
270 p: this.board
[ex
][ey
].charAt(1)
278 canIplay(side
, [x
, y
]) {
280 (this.subTurn
== 1 && this.turn
== side
&& this.getColor(x
, y
) == side
) ||
281 (this.subTurn
== 2 && x
== this.sentryPos
.x
&& y
== this.sentryPos
.y
)
285 getPotentialMovesFrom([x
, y
]) {
286 // At subTurn == 2, jailers aren't effective (Jeff K)
287 if (this.subTurn
== 1) {
288 const jsq
= this.isImmobilized([x
, y
]);
291 // Special pass move if king:
292 if (this.getPiece(x
, y
) == V
.KING
) {
297 start: { x: x
, y: y
},
298 end: { x: jsq
[0], y: jsq
[1] }
306 switch (this.getPiece(x
, y
)) {
308 moves
= this.getPotentialJailerMoves([x
, y
]);
311 moves
= this.getPotentialSentryMoves([x
, y
]);
314 moves
= this.getPotentialLancerMoves([x
, y
]);
317 moves
= super.getPotentialMovesFrom([x
, y
]);
320 const L
= this.sentryPush
.length
;
321 if (!!this.sentryPush
[L
-1]) {
322 // Delete moves walking back on sentry push path
323 moves
= moves
.filter(m
=> {
325 m
.vanish
[0].p
!= V
.PAWN
&&
326 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
332 } else if (this.subTurn
== 2) {
333 // Put back the sentinel on board:
334 const color
= this.turn
;
336 m
.appear
.push({x: x
, y: y
, p: V
.SENTRY
, c: color
});
342 getPotentialPawnMoves([x
, y
]) {
343 const color
= this.getColor(x
, y
);
345 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
346 let shiftX
= color
== "w" ? -1 : 1;
347 if (this.subTurn
== 2) shiftX
*= -1;
348 const startRank
= color
== "w" ? sizeX
- 2 : 1;
349 const lastRank
= color
== "w" ? 0 : sizeX
- 1;
351 // Pawns might be pushed on 1st rank and attempt to move again:
352 if (!V
.OnBoard(x
+ shiftX
, y
)) return [];
355 // No promotions after pushes!
356 x
+ shiftX
== lastRank
&& this.subTurn
== 1
358 Object
.keys(V
.LANCER_DIRS
).concat(
359 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.SENTRY
, V
.JAILER
])
361 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
362 // One square forward
363 for (let piece
of finalPieces
) {
365 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
373 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
376 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
380 for (let shiftY
of [-1, 1]) {
383 y
+ shiftY
< sizeY
&&
384 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
385 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
387 for (let piece
of finalPieces
) {
389 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
399 const Lep
= this.epSquares
.length
;
400 const epSquare
= this.epSquares
[Lep
- 1];
403 epSquare
.x
== x
+ shiftX
&&
404 Math
.abs(epSquare
.y
- y
) == 1
406 let enpassantMove
= this.getBasicMove([x
, y
], [epSquare
.x
, epSquare
.y
]);
407 enpassantMove
.vanish
.push({
411 c: this.getColor(x
, epSquare
.y
)
413 moves
.push(enpassantMove
);
419 // Obtain all lancer moves in "step" direction
420 getPotentialLancerMoves_aux([x
, y
], step
, tr
) {
422 // Add all moves to vacant squares until opponent is met:
423 const color
= this.getColor(x
, y
);
427 // at subTurn == 2, consider own pieces as opponent
429 let sq
= [x
+ step
[0], y
+ step
[1]];
430 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
431 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
432 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
436 if (V
.OnBoard(sq
[0], sq
[1]))
437 // Add capturing move
438 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
442 getPotentialLancerMoves([x
, y
]) {
444 // Add all lancer possible orientations, similar to pawn promotions.
445 // Except if just after a push: allow all movements from init square then
446 const L
= this.sentryPush
.length
;
447 if (!!this.sentryPush
[L
-1]) {
448 // Maybe I was pushed
449 const pl
= this.sentryPush
[L
-1].length
;
451 this.sentryPush
[L
-1][pl
-1].x
== x
&&
452 this.sentryPush
[L
-1][pl
-1].y
== y
454 // I was pushed: allow all directions (for this move only), but
455 // do not change direction after moving, *except* if I keep the
456 // same orientation in which I was pushed.
457 const color
= this.getColor(x
, y
);
458 const curDir
= V
.LANCER_DIRS
[this.board
[x
][y
].charAt(1)];
459 Object
.values(V
.LANCER_DIRS
).forEach(step
=> {
460 const dirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
462 V
.LANCER_DIRS
[k
][0] == step
[0] &&
463 V
.LANCER_DIRS
[k
][1] == step
[1]
467 this.getPotentialLancerMoves_aux(
470 { p: dirCode
, c: color
}
472 if (curDir
[0] == step
[0] && curDir
[1] == step
[1]) {
473 // Keeping same orientation: can choose after
474 let chooseMoves
= [];
475 dirMoves
.forEach(m
=> {
476 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
477 let mk
= JSON
.parse(JSON
.stringify(m
));
482 Array
.prototype.push
.apply(moves
, chooseMoves
);
483 } else Array
.prototype.push
.apply(moves
, dirMoves
);
488 // I wasn't pushed: standard lancer move
489 const dirCode
= this.board
[x
][y
][1];
491 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
492 // Add all possible orientations aftermove except if I'm being pushed
493 if (this.subTurn
== 1) {
494 monodirMoves
.forEach(m
=> {
495 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
496 let mk
= JSON
.parse(JSON
.stringify(m
));
503 // I'm pushed: add potential nudges
504 let potentialNudges
= [];
505 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
507 V
.OnBoard(x
+ step
[0], y
+ step
[1]) &&
508 this.board
[x
+ step
[0]][y
+ step
[1]] == V
.EMPTY
510 potentialNudges
.push(
513 [x
+ step
[0], y
+ step
[1]]
518 return monodirMoves
.concat(potentialNudges
);
522 getPotentialSentryMoves([x
, y
]) {
523 // The sentry moves a priori like a bishop:
524 let moves
= super.getPotentialBishopMoves([x
, y
]);
525 // ...but captures are replaced by special move, if and only if
526 // "captured" piece can move now, considered as the capturer unit.
527 // --> except is subTurn == 2, in this case I don't push anything.
528 if (this.subTurn
== 2) return moves
.filter(m
=> m
.vanish
.length
== 1);
530 if (m
.vanish
.length
== 2) {
531 // Temporarily cancel the sentry capture:
536 const color
= this.getColor(x
, y
);
537 const fMoves
= moves
.filter(m
=> {
538 // Can the pushed unit make any move? ...resulting in a non-self-check?
539 if (m
.appear
.length
== 0) {
542 let moves2
= this.getPotentialMovesFrom([m
.end
.x
, m
.end
.y
]);
543 for (let m2
of moves2
) {
545 res
= !this.underCheck(color
);
557 getPotentialJailerMoves([x
, y
]) {
558 return super.getPotentialRookMoves([x
, y
]).filter(m
=> {
559 // Remove jailer captures
560 return m
.vanish
[0].p
!= V
.JAILER
|| m
.vanish
.length
== 1;
564 getPotentialKingMoves(sq
) {
565 const moves
= this.getSlideNJumpMoves(
567 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
572 ? moves
.concat(this.getCastleMoves(sq
))
577 // Adapted: castle with jailer possible
578 getCastleMoves([x
, y
]) {
579 const c
= this.getColor(x
, y
);
580 const firstRank
= (c
== "w" ? V
.size
.x
- 1 : 0);
581 if (x
!= firstRank
|| y
!= this.INIT_COL_KING
[c
])
584 const oppCol
= V
.GetOppCol(c
);
587 // King, then rook or jailer:
588 const finalSquares
= [
590 [V
.size
.y
- 2, V
.size
.y
- 3]
597 if (this.castleFlags
[c
][castleSide
] >= 8) continue;
598 // Rook (or jailer) and king are on initial position
599 const finDist
= finalSquares
[castleSide
][0] - y
;
600 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
604 this.isAttacked([x
, i
], [oppCol
]) ||
605 (this.board
[x
][i
] != V
.EMPTY
&&
606 (this.getColor(x
, i
) != c
||
607 ![V
.KING
, V
.ROOK
, V
.JAILER
].includes(this.getPiece(x
, i
))))
609 continue castlingCheck
;
612 } while (i
!= finalSquares
[castleSide
][0]);
613 step
= castleSide
== 0 ? -1 : 1;
614 const rookOrJailerPos
= this.castleFlags
[c
][castleSide
];
615 for (i
= y
+ step
; i
!= rookOrJailerPos
; i
+= step
)
616 if (this.board
[x
][i
] != V
.EMPTY
) continue castlingCheck
;
618 // Nothing on final squares, except maybe king and castling rook or jailer?
619 for (i
= 0; i
< 2; i
++) {
621 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
622 this.getPiece(x
, finalSquares
[castleSide
][i
]) != V
.KING
&&
623 finalSquares
[castleSide
][i
] != rookOrJailerPos
625 continue castlingCheck
;
629 // If this code is reached, castle is valid
630 const castlingPiece
= this.getPiece(firstRank
, rookOrJailerPos
);
634 new PiPo({ x: x
, y: finalSquares
[castleSide
][0], p: V
.KING
, c: c
}),
635 new PiPo({ x: x
, y: finalSquares
[castleSide
][1], p: castlingPiece
, c: c
})
638 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
639 new PiPo({ x: x
, y: rookOrJailerPos
, p: castlingPiece
, c: c
})
642 Math
.abs(y
- rookOrJailerPos
) <= 2
643 ? { x: x
, y: rookOrJailerPos
}
644 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
653 // If in second-half of a move, we already know that a move is possible
654 if (this.subTurn
== 2) return true;
655 return super.atLeastOneMove();
659 if (moves
.length
== 0) return [];
660 const basicFilter
= (m
, c
) => {
662 const res
= !this.underCheck(c
);
666 // Disable check tests for sentry pushes,
667 // because in this case the move isn't finished
668 let movesWithoutSentryPushes
= [];
669 let movesWithSentryPushes
= [];
671 // Second condition below for special king "pass" moves
672 if (m
.appear
.length
> 0 || m
.vanish
.length
== 0)
673 movesWithoutSentryPushes
.push(m
);
674 else movesWithSentryPushes
.push(m
);
676 const color
= this.turn
;
677 const oppCol
= V
.GetOppCol(color
);
678 const filteredMoves
=
679 movesWithoutSentryPushes
.filter(m
=> basicFilter(m
, color
));
680 // If at least one full move made, everything is allowed.
681 // Else: forbid checks and captures.
685 : filteredMoves
.filter(m
=> {
687 m
.vanish
.length
<= 1 ||
688 m
.appear
.length
!= 1 ||
689 basicFilter(m
, oppCol
)
692 ).concat(movesWithSentryPushes
);
696 if (this.subTurn
== 1) return super.getAllValidMoves();
698 const sentrySq
= [this.sentryPos
.x
, this.sentryPos
.y
];
699 return this.filterValid(this.getPotentialMovesFrom(sentrySq
));
703 if (move.appear
.length
== 0 && move.vanish
.length
== 1)
704 // The sentry is about to push a piece: subTurn goes from 1 to 2
705 this.sentryPos
= { x: move.end
.x
, y: move.end
.y
};
706 if (this.subTurn
== 2 && move.vanish
[0].p
!= V
.PAWN
) {
707 // A piece is pushed: forbid array of squares between start and end
708 // of move, included (except if it's a pawn)
710 if ([V
.KNIGHT
,V
.KING
].includes(move.vanish
[0].p
))
711 // short-range pieces: just forbid initial square
712 squares
.push({ x: move.start
.x
, y: move.start
.y
});
714 const deltaX
= move.end
.x
- move.start
.x
;
715 const deltaY
= move.end
.y
- move.start
.y
;
717 deltaX
/ Math
.abs(deltaX
) || 0,
718 deltaY
/ Math
.abs(deltaY
) || 0
721 let sq
= {x: move.start
.x
, y: move.start
.y
};
722 sq
.x
!= move.end
.x
|| sq
.y
!= move.end
.y
;
723 sq
.x
+= step
[0], sq
.y
+= step
[1]
725 squares
.push({ x: sq
.x
, y: sq
.y
});
728 // Add end square as well, to know if I was pushed (useful for lancers)
729 squares
.push({ x: move.end
.x
, y: move.end
.y
});
730 this.sentryPush
.push(squares
);
731 } else this.sentryPush
.push(null);
735 if (!this.states
) this.states
= [];
736 const stateFen
= this.getFen();
737 this.states
.push(stateFen
);
740 move.flags
= JSON
.stringify(this.aggregateFlags());
741 this.epSquares
.push(this.getEpSquare(move));
742 V
.PlayOnBoard(this.board
, move);
743 // Is it a sentry push? (useful for undo)
744 move.sentryPush
= (this.subTurn
== 2);
745 if (this.subTurn
== 1) this.movesCount
++;
746 if (move.appear
.length
== 0 && move.vanish
.length
== 1) this.subTurn
= 2;
748 // Turn changes only if not a sentry "pre-push"
749 this.turn
= V
.GetOppCol(this.turn
);
756 if (move.vanish
.length
== 0 || this.subTurn
== 2)
757 // Special pass move of the king, or sentry pre-push: nothing to update
759 const c
= move.vanish
[0].c
;
760 const piece
= move.vanish
[0].p
;
761 const firstRank
= c
== "w" ? V
.size
.x
- 1 : 0;
763 if (piece
== V
.KING
) {
764 this.kingPos
[c
][0] = move.appear
[0].x
;
765 this.kingPos
[c
][1] = move.appear
[0].y
;
766 this.castleFlags
[c
] = [V
.size
.y
, V
.size
.y
];
769 // Update castling flags if rooks are moved
770 const oppCol
= V
.GetOppCol(c
);
771 const oppFirstRank
= V
.size
.x
- 1 - firstRank
;
773 move.start
.x
== firstRank
&& //our rook moves?
774 this.castleFlags
[c
].includes(move.start
.y
)
776 const flagIdx
= (move.start
.y
== this.castleFlags
[c
][0] ? 0 : 1);
777 this.castleFlags
[c
][flagIdx
] = V
.size
.y
;
779 move.end
.x
== oppFirstRank
&& //we took opponent rook?
780 this.castleFlags
[oppCol
].includes(move.end
.y
)
782 const flagIdx
= (move.end
.y
== this.castleFlags
[oppCol
][0] ? 0 : 1);
783 this.castleFlags
[oppCol
][flagIdx
] = V
.size
.y
;
788 this.epSquares
.pop();
789 this.disaggregateFlags(JSON
.parse(move.flags
));
790 V
.UndoOnBoard(this.board
, move);
791 // Decrement movesCount except if the move is a sentry push
792 if (!move.sentryPush
) this.movesCount
--;
793 if (this.subTurn
== 2) this.subTurn
= 1;
795 this.turn
= V
.GetOppCol(this.turn
);
796 if (move.sentryPush
) this.subTurn
= 2;
800 const stateFen
= this.getFen();
801 if (stateFen
!= this.states
[this.states
.length
-1]) debugger;
806 super.postUndo(move);
807 this.sentryPush
.pop();
810 isAttacked(sq
, colors
) {
812 super.isAttacked(sq
, colors
) ||
813 this.isAttackedByLancer(sq
, colors
) ||
814 this.isAttackedBySentry(sq
, colors
)
818 isAttackedBySlideNJump([x
, y
], colors
, piece
, steps
, oneStep
) {
819 for (let step
of steps
) {
820 let rx
= x
+ step
[0],
822 while (V
.OnBoard(rx
, ry
) && this.board
[rx
][ry
] == V
.EMPTY
&& !oneStep
) {
828 this.getPiece(rx
, ry
) === piece
&&
829 colors
.includes(this.getColor(rx
, ry
)) &&
830 !this.isImmobilized([rx
, ry
])
838 isAttackedByPawn([x
, y
], colors
) {
839 for (let c
of colors
) {
840 const pawnShift
= c
== "w" ? 1 : -1;
841 if (x
+ pawnShift
>= 0 && x
+ pawnShift
< V
.size
.x
) {
842 for (let i
of [-1, 1]) {
846 this.getPiece(x
+ pawnShift
, y
+ i
) == V
.PAWN
&&
847 this.getColor(x
+ pawnShift
, y
+ i
) == c
&&
848 !this.isImmobilized([x
+ pawnShift
, y
+ i
])
858 isAttackedByLancer([x
, y
], colors
) {
859 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
860 // If in this direction there are only enemy pieces and empty squares,
861 // and we meet a lancer: can he reach us?
862 // NOTE: do not stop at first lancer, there might be several!
863 let coord
= { x: x
+ step
[0], y: y
+ step
[1] };
866 V
.OnBoard(coord
.x
, coord
.y
) &&
868 this.board
[coord
.x
][coord
.y
] == V
.EMPTY
||
869 colors
.includes(this.getColor(coord
.x
, coord
.y
))
873 this.getPiece(coord
.x
, coord
.y
) == V
.LANCER
&&
874 !this.isImmobilized([coord
.x
, coord
.y
])
876 lancerPos
.push({x: coord
.x
, y: coord
.y
});
881 for (let xy
of lancerPos
) {
882 const dir
= V
.LANCER_DIRS
[this.board
[xy
.x
][xy
.y
].charAt(1)];
883 if (dir
[0] == -step
[0] && dir
[1] == -step
[1]) return true;
889 // Helper to check sentries attacks:
890 selfAttack([x1
, y1
], [x2
, y2
]) {
891 const color
= this.getColor(x1
, y1
);
892 const sliderAttack
= (allowedSteps
, lancer
) => {
893 const deltaX
= x2
- x1
;
894 const deltaY
= y2
- y1
;
895 const step
= [ deltaX
/ Math
.abs(deltaX
), deltaY
/ Math
.abs(deltaY
) ];
896 if (allowedSteps
.every(st
=> st
[0] != step
[0] || st
[1] != step
[1]))
898 let sq
= [ x1
+ step
[0], y1
+ step
[1] ];
899 while (sq
[0] != x2
&& sq
[1] != y2
) {
901 // NOTE: no need to check OnBoard in this special case
902 (!lancer
&& this.board
[sq
[0]][sq
[1]] != V
.EMPTY
) ||
903 (!!lancer
&& this.getColor(sq
[0], sq
[1]) != color
)
912 switch (this.getPiece(x1
, y1
)) {
914 // Pushed pawns move as enemy pawns
915 const shift
= (color
== 'w' ? 1 : -1);
916 return (x1
+ shift
== x2
&& Math
.abs(y1
- y2
) == 1);
919 const deltaX
= Math
.abs(x1
- x2
);
920 const deltaY
= Math
.abs(y1
- y2
);
922 deltaX
+ deltaY
== 3 &&
923 [1, 2].includes(deltaX
) &&
924 [1, 2].includes(deltaY
)
928 return sliderAttack(V
.steps
[V
.ROOK
]);
930 return sliderAttack(V
.steps
[V
.BISHOP
]);
932 return sliderAttack(V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]));
934 // Special case: as long as no enemy units stands in-between, it attacks
935 // (if it points toward the king).
936 const allowedStep
= V
.LANCER_DIRS
[this.board
[x1
][y1
].charAt(1)];
937 return sliderAttack([allowedStep
], "lancer");
939 // No sentries or jailer tests: they cannot self-capture
944 isAttackedBySentry([x
, y
], colors
) {
945 // Attacked by sentry means it can self-take our king.
946 // Just check diagonals of enemy sentry(ies), and if it reaches
947 // one of our pieces: can I self-take?
948 const color
= V
.GetOppCol(colors
[0]);
950 for (let i
=0; i
<V
.size
.x
; i
++) {
951 for (let j
=0; j
<V
.size
.y
; j
++) {
953 this.getPiece(i
,j
) == V
.SENTRY
&&
954 colors
.includes(this.getColor(i
,j
)) &&
955 !this.isImmobilized([i
, j
])
957 for (let step
of V
.steps
[V
.BISHOP
]) {
958 let sq
= [ i
+ step
[0], j
+ step
[1] ];
960 V
.OnBoard(sq
[0], sq
[1]) &&
961 this.board
[sq
[0]][sq
[1]] == V
.EMPTY
967 V
.OnBoard(sq
[0], sq
[1]) &&
968 this.getColor(sq
[0], sq
[1]) == color
970 candidates
.push([ sq
[0], sq
[1] ]);
976 for (let c
of candidates
)
977 if (this.selfAttack(c
, [x
, y
])) return true;
981 // Jailer doesn't capture or give check
983 static get VALUES() {
984 return Object
.assign(
985 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
991 const maxeval
= V
.INFINITY
;
992 const color
= this.turn
;
993 let moves1
= this.getAllValidMoves();
995 if (moves1
.length
== 0)
996 // TODO: this situation should not happen
999 const setEval
= (move, next
) => {
1000 const score
= this.getCurrentScore();
1001 const curEval
= move.eval
;
1006 : (score
== "1-0" ? 1 : -1) * maxeval
;
1007 } else move.eval
= this.evalPosition();
1009 // "next" is defined after sentry pushes
1012 color
== 'w' && move.eval
> curEval
||
1013 color
== 'b' && move.eval
< curEval
1020 // Just search_depth == 1 (because of sentries. TODO: can do better...)
1021 moves1
.forEach(m1
=> {
1023 if (this.subTurn
== 1) setEval(m1
);
1025 // Need to play every pushes and count:
1026 const moves2
= this.getAllValidMoves();
1027 moves2
.forEach(m2
=> {
1036 moves1
.sort((a
, b
) => {
1037 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
1039 let candidates
= [0];
1040 for (let j
= 1; j
< moves1
.length
&& moves1
[j
].eval
== moves1
[0].eval
; j
++)
1042 const choice
= moves1
[candidates
[randInt(candidates
.length
)]];
1043 return (!choice
.second
? choice : [choice
, choice
.second
]);
1047 // Special case "king takes jailer" is a pass move
1048 if (move.appear
.length
== 0 && move.vanish
.length
== 0) return "pass";
1049 if (this.subTurn
== 2) {
1050 // Do not consider appear[1] (sentry) for sentry pushes
1051 const simpleMove
= {
1052 appear: [move.appear
[0]],
1053 vanish: move.vanish
,
1057 return super.getNotation(simpleMove
);
1059 return super.getNotation(move);