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 // Is piece on square (x,y) immobilized?
218 isImmobilized([x
, y
]) {
219 const color
= this.getColor(x
, y
);
220 const oppCol
= V
.GetOppCol(color
);
221 for (let step
of V
.steps
[V
.ROOK
]) {
222 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
225 this.board
[i
][j
] != V
.EMPTY
&&
226 this.getColor(i
, j
) == oppCol
228 if (this.getPiece(i
, j
) == 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
].charAt(1)
250 c: this.getColor(sx
, sy
),
251 p: this.board
[sx
][sy
].charAt(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
].charAt(1)
271 canIplay(side
, [x
, y
]) {
273 (this.subTurn
== 1 && this.turn
== side
&& this.getColor(x
, y
) == side
) ||
274 (this.subTurn
== 2 && x
== this.sentryPos
.x
&& y
== this.sentryPos
.y
)
278 getPotentialMovesFrom([x
, y
]) {
279 // At subTurn == 2, jailers aren't effective (Jeff K)
280 if (this.subTurn
== 1) {
281 const jsq
= this.isImmobilized([x
, y
]);
284 // Special pass move if king:
285 if (this.getPiece(x
, y
) == V
.KING
) {
290 start: { x: x
, y: y
},
291 end: { x: jsq
[0], y: jsq
[1] }
298 if (this.subTurn
== 2) {
299 // Temporarily change pushed piece color.
300 // (Not using getPiece() because of lancers)
301 var oppCol
= this.getColor(x
, y
);
302 var color
= V
.GetOppCol(oppCol
);
303 var saveXYstate
= this.board
[x
][y
];
304 this.board
[x
][y
] = color
+ this.board
[x
][y
].charAt(1);
307 switch (this.getPiece(x
, y
)) {
309 moves
= this.getPotentialJailerMoves([x
, y
]);
312 moves
= this.getPotentialSentryMoves([x
, y
]);
315 moves
= this.getPotentialLancerMoves([x
, y
]);
318 moves
= super.getPotentialMovesFrom([x
, y
]);
321 const L
= this.sentryPush
.length
;
322 if (!!this.sentryPush
[L
-1]) {
323 // Delete moves walking back on sentry push path
324 moves
= moves
.filter(m
=> {
326 m
.vanish
[0].p
!= V
.PAWN
&&
327 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
334 else if (this.subTurn
== 2) {
335 // Don't forget to re-add the sentry on the board:
336 // Also fix color of pushed piece afterward:
338 m
.appear
.unshift({x: x
, y: y
, p: V
.SENTRY
, c: color
});
339 m
.appear
[1].c
= oppCol
;
340 m
.vanish
[0].c
= oppCol
;
342 this.board
[x
][y
] = saveXYstate
;
347 getPotentialPawnMoves([x
, y
]) {
348 const color
= this.getColor(x
, y
);
350 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
351 const shiftX
= color
== "w" ? -1 : 1;
352 const startRank
= color
== "w" ? sizeX
- 2 : 1;
353 const lastRank
= color
== "w" ? 0 : sizeX
- 1;
356 // No promotions after pushes!
357 x
+ shiftX
== lastRank
&& this.subTurn
== 1
359 Object
.keys(V
.LANCER_DIRS
).concat(
360 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.SENTRY
, V
.JAILER
])
362 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
363 // One square forward
364 for (let piece
of finalPieces
) {
366 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
374 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
377 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
381 for (let shiftY
of [-1, 1]) {
384 y
+ shiftY
< sizeY
&&
385 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
386 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
388 for (let piece
of finalPieces
) {
390 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
400 const Lep
= this.epSquares
.length
;
401 const epSquare
= this.epSquares
[Lep
- 1];
404 epSquare
.x
== x
+ shiftX
&&
405 Math
.abs(epSquare
.y
- y
) == 1
407 let enpassantMove
= this.getBasicMove([x
, y
], [epSquare
.x
, epSquare
.y
]);
408 enpassantMove
.vanish
.push({
412 c: this.getColor(x
, epSquare
.y
)
414 moves
.push(enpassantMove
);
420 // Obtain all lancer moves in "step" direction
421 getPotentialLancerMoves_aux([x
, y
], step
, tr
) {
423 // Add all moves to vacant squares until opponent is met:
424 const oppCol
= V
.GetOppCol(this.getColor(x
, y
));
425 let sq
= [x
+ step
[0], y
+ step
[1]];
426 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
427 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
428 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
432 if (V
.OnBoard(sq
[0], sq
[1]))
433 // Add capturing move
434 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
438 getPotentialLancerMoves([x
, y
]) {
440 // Add all lancer possible orientations, similar to pawn promotions.
441 // Except if just after a push: allow all movements from init square then
442 const L
= this.sentryPush
.length
;
443 if (!!this.sentryPush
[L
-1]) {
444 // Maybe I was pushed
445 const pl
= this.sentryPush
[L
-1].length
;
447 this.sentryPush
[L
-1][pl
-1].x
== x
&&
448 this.sentryPush
[L
-1][pl
-1].y
== y
450 // I was pushed: allow all directions (for this move only), but
451 // do not change direction after moving, *except* if I keep the
452 // same orientation in which I was pushed.
453 const color
= this.getColor(x
, y
);
454 const curDir
= V
.LANCER_DIRS
[this.board
[x
][x
].charAt(1)];
455 Object
.values(V
.LANCER_DIRS
).forEach(step
=> {
456 const dirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
458 V
.LANCER_DIRS
[k
][0] == step
[0] &&
459 V
.LANCER_DIRS
[k
][1] == step
[1]
463 this.getPotentialLancerMoves_aux(
466 { p: dirCode
, c: color
}
468 if (curDir
[0] == step
[0] && curDir
[1] == step
[1]) {
469 // Keeping same orientation: can choose after
470 let chooseMoves
= [];
471 dirMoves
.forEach(m
=> {
472 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
473 let mk
= JSON
.parse(JSON
.stringify(m
));
478 Array
.prototype.push
.apply(moves
, chooseMoves
);
479 } else Array
.prototype.push
.apply(moves
, dirMoves
);
484 // I wasn't pushed: standard lancer move
485 const dirCode
= this.board
[x
][y
][1];
487 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
488 // Add all possible orientations aftermove except if I'm being pushed
489 if (this.subTurn
== 1) {
490 monodirMoves
.forEach(m
=> {
491 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
492 let mk
= JSON
.parse(JSON
.stringify(m
));
499 // I'm pushed: add potential nudges
500 let potentialNudges
= [];
501 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
503 V
.OnBoard(x
+ step
[0], y
+ step
[1]) &&
504 this.board
[x
+ step
[0]][y
+ step
[1]] == V
.EMPTY
506 potentialNudges
.push(
509 [x
+ step
[0], y
+ step
[1]]
514 return monodirMoves
.concat(potentialNudges
);
518 getPotentialSentryMoves([x
, y
]) {
519 // The sentry moves a priori like a bishop:
520 let moves
= super.getPotentialBishopMoves([x
, y
]);
521 // ...but captures are replaced by special move, if and only if
522 // "captured" piece can move now, considered as the capturer unit.
523 // --> except is subTurn == 2, in this case I don't push anything.
524 if (this.subTurn
== 2) return moves
.filter(m
=> m
.vanish
.length
== 1);
526 if (m
.vanish
.length
== 2) {
527 // Temporarily cancel the sentry capture:
532 // Can the pushed unit make any move? ...resulting in a non-self-check?
533 const color
= this.getColor(x
, y
);
534 const fMoves
= moves
.filter(m
=> {
536 if (m
.appear
.length
== 0) {
539 let potentialMoves
= this.getPotentialMovesFrom([m
.end
.x
, m
.end
.y
]);
540 // Add nudges (if any a priori possible)
541 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
543 V
.OnBoard(m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]) &&
544 this.board
[m
.end
.x
+ step
[0]][m
.end
.y
+ step
[1]] == V
.EMPTY
549 [m
.end
.x
+ step
[0], m
.end
.y
+ step
[1]]
554 let moves2
= this.filterValid(potentialMoves
);
555 for (let m2
of moves2
) {
557 res
= !this.underCheck(color
);
569 getPotentialJailerMoves([x
, y
]) {
570 return super.getPotentialRookMoves([x
, y
]).filter(m
=> {
571 // Remove jailer captures
572 return m
.vanish
[0].p
!= V
.JAILER
|| m
.vanish
.length
== 1;
576 // Adapted: castle with jailer possible
577 getCastleMoves([x
, y
]) {
578 const c
= this.getColor(x
, y
);
579 const firstRank
= (c
== "w" ? V
.size
.x
- 1 : 0);
580 if (x
!= firstRank
|| y
!= this.INIT_COL_KING
[c
])
583 const oppCol
= V
.GetOppCol(c
);
586 // King, then rook or jailer:
587 const finalSquares
= [
589 [V
.size
.y
- 2, V
.size
.y
- 3]
596 if (this.castleFlags
[c
][castleSide
] >= 8) continue;
597 // Rook (or jailer) and king are on initial position
598 const finDist
= finalSquares
[castleSide
][0] - y
;
599 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
603 this.isAttacked([x
, i
], [oppCol
]) ||
604 (this.board
[x
][i
] != V
.EMPTY
&&
605 (this.getColor(x
, i
) != c
||
606 ![V
.KING
, V
.ROOK
, V
.JAILER
].includes(this.getPiece(x
, i
))))
608 continue castlingCheck
;
611 } while (i
!= finalSquares
[castleSide
][0]);
612 step
= castleSide
== 0 ? -1 : 1;
613 const rookOrJailerPos
= this.castleFlags
[c
][castleSide
];
614 for (i
= y
+ step
; i
!= rookOrJailerPos
; i
+= step
)
615 if (this.board
[x
][i
] != V
.EMPTY
) continue castlingCheck
;
617 // Nothing on final squares, except maybe king and castling rook or jailer?
618 for (i
= 0; i
< 2; i
++) {
620 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
621 this.getPiece(x
, finalSquares
[castleSide
][i
]) != V
.KING
&&
622 finalSquares
[castleSide
][i
] != rookOrJailerPos
624 continue castlingCheck
;
628 // If this code is reached, castle is valid
629 const castlingPiece
= this.getPiece(firstRank
, rookOrJailerPos
);
633 new PiPo({ x: x
, y: finalSquares
[castleSide
][0], p: V
.KING
, c: c
}),
634 new PiPo({ x: x
, y: finalSquares
[castleSide
][1], p: castlingPiece
, c: c
})
637 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
638 new PiPo({ x: x
, y: rookOrJailerPos
, p: castlingPiece
, c: c
})
641 Math
.abs(y
- rookOrJailerPos
) <= 2
642 ? { x: x
, y: rookOrJailerPos
}
643 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
652 // If in second-half of a move, we already know that a move is possible
653 if (this.subTurn
== 2) return true;
654 return super.atLeastOneMove();
658 // Disable check tests for sentry pushes,
659 // because in this case the move isn't finished
660 let movesWithoutSentryPushes
= [];
661 let movesWithSentryPushes
= [];
663 if (m
.appear
.length
> 0) movesWithoutSentryPushes
.push(m
);
664 else movesWithSentryPushes
.push(m
);
667 // TODO: if after move a sentry can take king in 2 times?!
669 const filteredMoves
= super.filterValid(movesWithoutSentryPushes
);
670 // If at least one full move made, everything is allowed:
671 if (this.movesCount
>= 2)
672 return filteredMoves
.concat(movesWithSentryPushes
);
673 // Else, forbid checks and captures:
674 const oppCol
= V
.GetOppCol(this.turn
);
675 return filteredMoves
.filter(m
=> {
676 if (m
.vanish
.length
== 2 && m
.appear
.length
== 1) return false;
678 const res
= !this.underCheck(oppCol
);
681 }).concat(movesWithSentryPushes
);
685 if (this.subTurn
== 1) return super.getAllValidMoves();
687 const sentrySq
= [this.sentryPos
.x
, this.sentryPos
.y
];
688 return this.filterValid(this.getPotentialMovesFrom(sentrySq
));
692 if (move.appear
.length
== 0 && move.vanish
.length
== 1) {
693 // The sentry is about to push a piece: subTurn goes from 1 to 2
694 this.sentryPos
= { x: move.end
.x
, y: move.end
.y
};
695 } else if (this.subTurn
== 2 && move.vanish
[0].p
!= V
.PAWN
) {
696 // A piece is pushed: forbid array of squares between start and end
697 // of move, included (except if it's a pawn)
699 if ([V
.KNIGHT
,V
.KING
].includes(move.vanish
[0].p
))
700 // short-range pieces: just forbid initial square
701 squares
.push({ x: move.start
.x
, y: move.start
.y
});
703 const deltaX
= move.end
.x
- move.start
.x
;
704 const deltaY
= move.end
.y
- move.start
.y
;
706 deltaX
/ Math
.abs(deltaX
) || 0,
707 deltaY
/ Math
.abs(deltaY
) || 0
710 let sq
= {x: move.start
.x
, y: move.start
.y
};
711 sq
.x
!= move.end
.x
&& sq
.y
!= move.end
.y
;
712 sq
.x
+= step
[0], sq
.y
+= step
[1]
714 squares
.push({ x: sq
.x
, y: sq
.y
});
717 // Add end square as well, to know if I was pushed (useful for lancers)
718 squares
.push({ x: move.end
.x
, y: move.end
.y
});
719 this.sentryPush
.push(squares
);
720 } else this.sentryPush
.push(null);
724 // if (!this.states) this.states = [];
725 // const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen();
726 // this.states.push(stateFen);
729 move.flags
= JSON
.stringify(this.aggregateFlags());
730 this.epSquares
.push(this.getEpSquare(move));
731 V
.PlayOnBoard(this.board
, move);
732 // Is it a sentry push? (useful for undo)
733 move.sentryPush
= (this.subTurn
== 2);
734 if (this.subTurn
== 1) this.movesCount
++;
735 if (move.appear
.length
== 0 && move.vanish
.length
== 1) this.subTurn
= 2;
737 // Turn changes only if not a sentry "pre-push"
738 this.turn
= V
.GetOppCol(this.turn
);
745 if (move.vanish
.length
== 0)
746 // Special pass move of the king: nothing to update!
748 super.postPlay(move);
752 this.epSquares
.pop();
753 this.disaggregateFlags(JSON
.parse(move.flags
));
754 V
.UndoOnBoard(this.board
, move);
755 // Decrement movesCount except if the move is a sentry push
756 if (!move.sentryPush
) this.movesCount
--;
757 if (this.subTurn
== 2) this.subTurn
= 1;
759 this.turn
= V
.GetOppCol(this.turn
);
760 if (move.sentryPush
) this.subTurn
= 2;
764 // const stateFen = this.getBaseFen() + this.getTurnFen() + this.getFlagsFen();
765 // if (stateFen != this.states[this.states.length-1]) debugger;
766 // this.states.pop();
770 super.postUndo(move);
771 this.sentryPush
.pop();
774 isAttacked(sq
, colors
) {
776 super.isAttacked(sq
, colors
) ||
777 this.isAttackedByLancer(sq
, colors
) ||
778 this.isAttackedBySentry(sq
, colors
)
782 isAttackedByLancer([x
, y
], colors
) {
783 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
784 // If in this direction there are only enemy pieces and empty squares,
785 // and we meet a lancer: can he reach us?
786 // NOTE: do not stop at first lancer, there might be several!
787 let coord
= { x: x
+ step
[0], y: y
+ step
[1] };
790 V
.OnBoard(coord
.x
, coord
.y
) &&
792 this.board
[coord
.x
][coord
.y
] == V
.EMPTY
||
793 colors
.includes(this.getColor(coord
.x
, coord
.y
))
796 if (this.getPiece(coord
.x
, coord
.y
) == V
.LANCER
)
797 lancerPos
.push({x: coord
.x
, y: coord
.y
});
801 for (let xy
of lancerPos
) {
802 const dir
= V
.LANCER_DIRS
[this.board
[xy
.x
][xy
.y
].charAt(1)];
803 if (dir
[0] == -step
[0] && dir
[1] == -step
[1]) return true;
809 // Helper to check sentries attacks:
810 selfAttack([x1
, y1
], [x2
, y2
]) {
811 const color
= this.getColor(x1
, y1
);
812 const sliderAttack
= (allowedSteps
, lancer
) => {
813 const deltaX
= x2
- x1
;
814 const deltaY
= y2
- y1
;
815 const step
= [ deltaX
/ Math
.abs(deltaX
), deltaY
/ Math
.abs(deltaY
) ];
816 if (allowedSteps
.every(st
=> st
[0] != step
[0] || st
[1] != step
[1]))
818 let sq
= [ x1
+ step
[0], y1
+ step
[1] ];
819 while (sq
[0] != x2
&& sq
[1] != y2
) {
821 // NOTE: no need to check OnBoard in this special case
822 (!lancer
&& this.board
[sq
[0]][sq
[1]] != V
.EMPTY
) ||
823 (!!lancer
&& this.getColor(sq
[0], sq
[1]) != color
)
832 switch (this.getPiece(x1
, y1
)) {
834 // Pushed pawns move as enemy pawns
835 const shift
= (color
== 'w' ? 1 : -1);
836 return (x1
+ shift
== x2
&& Math
.abs(y1
- y2
) == 1);
839 const deltaX
= Math
.abs(x1
- x2
);
840 const deltaY
= Math
.abs(y1
- y2
);
842 deltaX
+ deltaY
== 3 &&
843 [1, 2].includes(deltaX
) &&
844 [1, 2].includes(deltaY
)
848 return sliderAttack(V
.steps
[V
.ROOK
]);
850 return sliderAttack(V
.steps
[V
.BISHOP
]);
852 return sliderAttack(V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]));
854 // Special case: as long as no enemy units stands in-between, it attacks
855 // (if it points toward the king).
856 const allowedStep
= V
.LANCER_DIRS
[this.board
[x1
][y1
].charAt(1)];
857 return sliderAttack([allowedStep
], "lancer");
859 // No sentries or jailer tests: they cannot self-capture
864 isAttackedBySentry([x
, y
], colors
) {
865 // Attacked by sentry means it can self-take our king.
866 // Just check diagonals of enemy sentry(ies), and if it reaches
867 // one of our pieces: can I self-take?
868 const color
= V
.GetOppCol(colors
[0]);
870 for (let i
=0; i
<V
.size
.x
; i
++) {
871 for (let j
=0; j
<V
.size
.y
; j
++) {
873 this.getPiece(i
,j
) == V
.SENTRY
&&
874 colors
.includes(this.getColor(i
,j
))
876 for (let step
of V
.steps
[V
.BISHOP
]) {
877 let sq
= [ i
+ step
[0], j
+ step
[1] ];
879 V
.OnBoard(sq
[0], sq
[1]) &&
880 this.board
[sq
[0]][sq
[1]] == V
.EMPTY
886 V
.OnBoard(sq
[0], sq
[1]) &&
887 this.getColor(sq
[0], sq
[1]) == color
889 candidates
.push([ sq
[0], sq
[1] ]);
895 for (let c
of candidates
)
896 if (this.selfAttack(c
, [x
, y
])) return true;
900 // Jailer doesn't capture or give check
902 static get VALUES() {
903 return Object
.assign(
904 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
910 const maxeval
= V
.INFINITY
;
911 const color
= this.turn
;
912 let moves1
= this.getAllValidMoves();
914 if (moves1
.length
== 0)
915 // TODO: this situation should not happen
918 const setEval
= (move, next
) => {
919 const score
= this.getCurrentScore();
920 const curEval
= move.eval
;
925 : (score
== "1-0" ? 1 : -1) * maxeval
;
926 } else move.eval
= this.evalPosition();
928 // "next" is defined after sentry pushes
931 color
== 'w' && move.eval
> curEval
||
932 color
== 'b' && move.eval
< curEval
939 // Just search_depth == 1 (because of sentries. TODO: can do better...)
940 moves1
.forEach(m1
=> {
942 if (this.subTurn
== 1) setEval(m1
);
944 // Need to play every pushes and count:
945 const moves2
= this.getAllValidMoves();
946 moves2
.forEach(m2
=> {
955 moves1
.sort((a
, b
) => {
956 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
958 let candidates
= [0];
959 for (let j
= 1; j
< moves1
.length
&& moves1
[j
].eval
== moves1
[0].eval
; j
++)
961 const choice
= moves1
[candidates
[randInt(candidates
.length
)]];
962 return (!choice
.second
? choice : [choice
, choice
.second
]);
965 // TODO: if subTurn == 2, take some precautions, in particular pawn pushed on 1st rank.
966 // --> should indicate Sxb2,bxc1
968 // Special case "king takes jailer" is a pass move
969 if (move.appear
.length
== 0 && move.vanish
.length
== 0) return "pass";
970 return super.getNotation(move);