1 import { ArrayFun
} from "@/utils/array";
2 import { randInt
} from "@/utils/alea";
3 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
5 export class EightpiecesRules
extends ChessRules
{
17 static get IMAGE_EXTENSION() {
18 // Temporarily, for the time SVG pieces are being designed:
22 // Lancer directions *from white perspective*
23 static get LANCER_DIRS() {
37 return ChessRules
.PIECES
38 .concat([V
.JAILER
, V
.SENTRY
])
39 .concat(Object
.keys(V
.LANCER_DIRS
));
43 const piece
= this.board
[i
][j
].charAt(1);
44 // Special lancer case: 8 possible orientations
45 if (Object
.keys(V
.LANCER_DIRS
).includes(piece
)) return V
.LANCER
;
49 getPpath(b
, color
, score
, orientation
) {
50 if ([V
.JAILER
, V
.SENTRY
].includes(b
[1])) return "Eightpieces/tmp_png/" + b
;
51 if (Object
.keys(V
.LANCER_DIRS
).includes(b
[1])) {
52 if (orientation
== 'w') return "Eightpieces/tmp_png/" + b
;
53 // Find opposite direction for adequate display:
81 return "Eightpieces/tmp_png/" + b
[0] + oppDir
;
83 // TODO: after we have SVG pieces, remove the folder and next prefix:
84 return "Eightpieces/tmp_png/" + b
;
87 getPPpath(m
, orientation
) {
90 m
.appear
[0].c
+ m
.appear
[0].p
,
98 static ParseFen(fen
) {
99 const fenParts
= fen
.split(" ");
100 return Object
.assign(
101 ChessRules
.ParseFen(fen
),
102 { sentrypush: fenParts
[5] }
106 static IsGoodFen(fen
) {
107 if (!ChessRules
.IsGoodFen(fen
)) return false;
108 const fenParsed
= V
.ParseFen(fen
);
109 // 5) Check sentry push (if any)
111 fenParsed
.sentrypush
!= "-" &&
112 !fenParsed
.sentrypush
.match(/^([a-h][1-8]){2,2}$/)
120 return super.getFen() + " " + this.getSentrypushFen();
124 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
128 const L
= this.sentryPush
.length
;
129 if (!this.sentryPush
[L
-1]) return "-";
131 const spL
= this.sentryPush
[L
-1].length
;
132 // Condensate path: just need initial and final squares:
134 .map(i
=> V
.CoordsToSquare(this.sentryPush
[L
-1][i
]))
138 setOtherVariables(fen
) {
139 super.setOtherVariables(fen
);
140 // subTurn == 2 only when a sentry moved, and is about to push something
142 // Sentry position just after a "capture" (subTurn from 1 to 2)
143 this.sentryPos
= null;
144 // Stack pieces' forbidden squares after a sentry move at each turn
145 const parsedFen
= V
.ParseFen(fen
);
146 if (parsedFen
.sentrypush
== "-") this.sentryPush
= [null];
148 // Expand init + dest squares into a full path:
149 const init
= V
.SquareToCoords(parsedFen
.sentrypush
.substr(0, 2)),
150 dest
= V
.SquareToCoords(parsedFen
.sentrypush
.substr(2));
151 let newPath
= [init
];
152 const delta
= ['x', 'y'].map(i
=> Math
.abs(dest
[i
] - init
[i
]));
153 // Check that it's not a knight movement:
154 if (delta
[0] == 0 || delta
[1] == 0 || delta
[0] == delta
[1]) {
155 const step
= ['x', 'y'].map((i
, idx
) => {
156 return (dest
[i
] - init
[i
]) / delta
[idx
] || 0
158 let x
= init
.x
+ step
[0],
159 y
= init
.y
+ step
[1];
160 while (x
!= dest
.x
|| y
!= dest
.y
) {
161 newPath
.push({ x: x
, y: y
});
167 this.sentryPush
= [newPath
];
171 static GenRandInitFen(randomness
) {
174 return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -";
176 let pieces
= { w: new Array(8), b: new Array(8) };
178 // Shuffle pieces on first (and last rank if randomness == 2)
179 for (let c
of ["w", "b"]) {
180 if (c
== 'b' && randomness
== 1) {
181 const lancerIdx
= pieces
['w'].findIndex(p
=> {
182 return Object
.keys(V
.LANCER_DIRS
).includes(p
);
185 pieces
['w'].slice(0, lancerIdx
)
187 .concat(pieces
['w'].slice(lancerIdx
+ 1));
192 let positions
= ArrayFun
.range(8);
194 // Get random squares for bishop and sentry
195 let randIndex
= 2 * randInt(4);
196 let bishopPos
= positions
[randIndex
];
197 // The sentry must be on a square of different color
198 let randIndex_tmp
= 2 * randInt(4) + 1;
199 let sentryPos
= positions
[randIndex_tmp
];
201 // Check if white sentry is on the same color as ours.
202 // If yes: swap bishop and sentry positions.
203 // NOTE: test % 2 == 1 because there are 7 slashes.
204 if ((pieces
['w'].indexOf('s') - sentryPos
) % 2 == 1)
205 [bishopPos
, sentryPos
] = [sentryPos
, bishopPos
];
207 positions
.splice(Math
.max(randIndex
, randIndex_tmp
), 1);
208 positions
.splice(Math
.min(randIndex
, randIndex_tmp
), 1);
210 // Get random squares for knight and lancer
211 randIndex
= randInt(6);
212 const knightPos
= positions
[randIndex
];
213 positions
.splice(randIndex
, 1);
214 randIndex
= randInt(5);
215 const lancerPos
= positions
[randIndex
];
216 positions
.splice(randIndex
, 1);
218 // Get random square for queen
219 randIndex
= randInt(4);
220 const queenPos
= positions
[randIndex
];
221 positions
.splice(randIndex
, 1);
223 // Rook, jailer and king positions are now almost fixed,
224 // only the ordering rook->jailer or jailer->rook must be decided.
225 let rookPos
= positions
[0];
226 let jailerPos
= positions
[2];
227 const kingPos
= positions
[1];
228 flags
+= V
.CoordToColumn(rookPos
) + V
.CoordToColumn(jailerPos
);
229 if (Math
.random() < 0.5) [rookPos
, jailerPos
] = [jailerPos
, rookPos
];
231 pieces
[c
][rookPos
] = "r";
232 pieces
[c
][knightPos
] = "n";
233 pieces
[c
][bishopPos
] = "b";
234 pieces
[c
][queenPos
] = "q";
235 pieces
[c
][kingPos
] = "k";
236 pieces
[c
][sentryPos
] = "s";
237 // Lancer faces north for white, and south for black:
238 pieces
[c
][lancerPos
] = c
== 'w' ? 'c' : 'g';
239 pieces
[c
][jailerPos
] = "j";
242 pieces
["b"].join("") +
243 "/pppppppp/8/8/8/8/PPPPPPPP/" +
244 pieces
["w"].join("").toUpperCase() +
245 " w 0 " + flags
+ " - -"
249 canTake([x1
, y1
], [x2
, y2
]) {
250 if (this.subTurn
== 2)
251 // Only self captures on this subturn:
252 return this.getColor(x1
, y1
) == this.getColor(x2
, y2
);
253 return super.canTake([x1
, y1
], [x2
, y2
]);
256 // Is piece on square (x,y) immobilized?
257 isImmobilized([x
, y
]) {
258 const color
= this.getColor(x
, y
);
259 const oppCol
= V
.GetOppCol(color
);
260 for (let step
of V
.steps
[V
.ROOK
]) {
261 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
264 this.board
[i
][j
] != V
.EMPTY
&&
265 this.getColor(i
, j
) == oppCol
267 if (this.getPiece(i
, j
) == V
.JAILER
) return [i
, j
];
273 // Because of the lancers, getPiece() could be wrong:
274 // use board[x][y][1] instead (always valid).
275 getBasicMove([sx
, sy
], [ex
, ey
], tr
) {
276 const initColor
= this.getColor(sx
, sy
);
277 const initPiece
= this.board
[sx
][sy
].charAt(1);
283 c: tr
? tr
.c : initColor
,
284 p: tr
? tr
.p : initPiece
297 // The opponent piece disappears if we take it
298 if (this.board
[ex
][ey
] != V
.EMPTY
) {
303 c: this.getColor(ex
, ey
),
304 p: this.board
[ex
][ey
].charAt(1)
312 canIplay(side
, [x
, y
]) {
314 (this.subTurn
== 1 && this.turn
== side
&& this.getColor(x
, y
) == side
)
316 (this.subTurn
== 2 && x
== this.sentryPos
.x
&& y
== this.sentryPos
.y
)
320 getPotentialMovesFrom([x
, y
]) {
321 const piece
= this.getPiece(x
, y
);
322 const L
= this.sentryPush
.length
;
323 // At subTurn == 2, jailers aren't effective (Jeff K)
324 if (this.subTurn
== 1) {
325 const jsq
= this.isImmobilized([x
, y
]);
328 // Special pass move if king:
329 if (piece
== V
.KING
) {
334 start: { x: x
, y: y
},
335 end: { x: jsq
[0], y: jsq
[1] }
339 else if (piece
== V
.LANCER
&& !!this.sentryPush
[L
-1]) {
340 // A pushed lancer next to the jailer: reorient
341 const color
= this.getColor(x
, y
);
342 const curDir
= this.board
[x
][y
].charAt(1);
343 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
346 appear: [{ x: x
, y: y
, c: color
, p: k
}],
347 vanish: [{ x: x
, y: y
, c: color
, p: curDir
}],
348 start: { x: x
, y: y
},
349 end: { x: jsq
[0], y: jsq
[1] }
360 moves
= this.getPotentialJailerMoves([x
, y
]);
363 moves
= this.getPotentialSentryMoves([x
, y
]);
366 moves
= this.getPotentialLancerMoves([x
, y
]);
369 moves
= super.getPotentialMovesFrom([x
, y
]);
372 if (!!this.sentryPush
[L
-1]) {
373 // Delete moves walking back on sentry push path,
374 // only if not a pawn, and the piece is the pushed one.
375 const pl
= this.sentryPush
[L
-1].length
;
376 const finalPushedSq
= this.sentryPush
[L
-1][pl
-1];
377 moves
= moves
.filter(m
=> {
379 m
.vanish
[0].p
!= V
.PAWN
&&
380 m
.start
.x
== finalPushedSq
.x
&& m
.start
.y
== finalPushedSq
.y
&&
381 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
387 } else if (this.subTurn
== 2) {
388 // Put back the sentinel on board:
389 const color
= this.turn
;
391 m
.appear
.push({x: x
, y: y
, p: V
.SENTRY
, c: color
});
397 getPotentialPawnMoves([x
, y
]) {
398 const color
= this.getColor(x
, y
);
400 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
401 let shiftX
= (color
== "w" ? -1 : 1);
402 if (this.subTurn
== 2) shiftX
*= -1;
403 const firstRank
= color
== "w" ? sizeX
- 1 : 0;
404 const startRank
= color
== "w" ? sizeX
- 2 : 1;
405 const lastRank
= color
== "w" ? 0 : sizeX
- 1;
407 // Pawns might be pushed on 1st rank and attempt to move again:
408 if (!V
.OnBoard(x
+ shiftX
, y
)) return [];
410 // A push cannot put a pawn on last rank (it goes backward)
411 let finalPieces
= [V
.PAWN
];
412 if (x
+ shiftX
== lastRank
) {
413 // Only allow direction facing inside board:
414 const allowedLancerDirs
=
416 ? ['e', 'f', 'g', 'h', 'm']
417 : ['c', 'd', 'e', 'm', 'o'];
420 .concat([V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.SENTRY
, V
.JAILER
]);
422 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
423 // One square forward
424 for (let piece
of finalPieces
) {
426 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
433 // 2-squares jumps forbidden if pawn push
435 [startRank
, firstRank
].includes(x
) &&
436 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
439 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
443 for (let shiftY
of [-1, 1]) {
446 y
+ shiftY
< sizeY
&&
447 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
448 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
450 for (let piece
of finalPieces
) {
452 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
461 // En passant: only on subTurn == 1
462 const Lep
= this.epSquares
.length
;
463 const epSquare
= this.epSquares
[Lep
- 1];
467 epSquare
.x
== x
+ shiftX
&&
468 Math
.abs(epSquare
.y
- y
) == 1
470 let enpassantMove
= this.getBasicMove([x
, y
], [epSquare
.x
, epSquare
.y
]);
471 enpassantMove
.vanish
.push({
475 c: this.getColor(x
, epSquare
.y
)
477 moves
.push(enpassantMove
);
484 if (isNaN(square
[0])) return null;
485 const L
= this.sentryPush
.length
;
486 const [x
, y
] = [square
[0], square
[1]];
487 const color
= this.turn
;
490 this.board
[x
][y
] == V
.EMPTY
||
491 this.getPiece(x
, y
) != V
.LANCER
||
492 this.getColor(x
, y
) != color
||
493 !!this.sentryPush
[L
-1]
498 const orientation
= this.board
[x
][y
][1];
499 const step
= V
.LANCER_DIRS
[orientation
];
500 if (!V
.OnBoard(x
+ step
[0], y
+ step
[1])) {
502 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
503 const dir
= V
.LANCER_DIRS
[k
];
505 (dir
[0] != step
[0] || dir
[1] != step
[1]) &&
506 V
.OnBoard(x
+ dir
[0], y
+ dir
[1])
526 start: { x: x
, y : y
},
527 end: { x: -1, y: -1 }
537 // Obtain all lancer moves in "step" direction
538 getPotentialLancerMoves_aux([x
, y
], step
, tr
) {
540 // Add all moves to vacant squares until opponent is met:
541 const color
= this.getColor(x
, y
);
545 // at subTurn == 2, consider own pieces as opponent
547 let sq
= [x
+ step
[0], y
+ step
[1]];
548 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
549 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
550 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
554 if (V
.OnBoard(sq
[0], sq
[1]))
555 // Add capturing move
556 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
560 getPotentialLancerMoves([x
, y
]) {
562 // Add all lancer possible orientations, similar to pawn promotions.
563 // Except if just after a push: allow all movements from init square then
564 const L
= this.sentryPush
.length
;
565 const color
= this.getColor(x
, y
);
566 const dirCode
= this.board
[x
][y
][1];
567 const curDir
= V
.LANCER_DIRS
[dirCode
];
568 if (!!this.sentryPush
[L
-1]) {
569 // Maybe I was pushed
570 const pl
= this.sentryPush
[L
-1].length
;
572 this.sentryPush
[L
-1][pl
-1].x
== x
&&
573 this.sentryPush
[L
-1][pl
-1].y
== y
575 // I was pushed: allow all directions (for this move only), but
576 // do not change direction after moving, *except* if I keep the
577 // same orientation in which I was pushed.
578 // Also allow simple reorientation ("capturing king"):
579 if (!V
.OnBoard(x
+ curDir
[0], y
+ curDir
[1])) {
580 const kp
= this.kingPos
[color
];
581 let reorientMoves
= [];
582 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
583 const dir
= V
.LANCER_DIRS
[k
];
585 (dir
[0] != curDir
[0] || dir
[1] != curDir
[1]) &&
586 V
.OnBoard(x
+ dir
[0], y
+ dir
[1])
606 start: { x: x
, y : y
},
607 end: { x: kp
[0], y: kp
[1] }
612 Array
.prototype.push
.apply(moves
, reorientMoves
);
614 Object
.values(V
.LANCER_DIRS
).forEach(step
=> {
615 const dirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
617 V
.LANCER_DIRS
[k
][0] == step
[0] &&
618 V
.LANCER_DIRS
[k
][1] == step
[1]
622 this.getPotentialLancerMoves_aux(
625 { p: dirCode
, c: color
}
627 if (curDir
[0] == step
[0] && curDir
[1] == step
[1]) {
628 // Keeping same orientation: can choose after
629 let chooseMoves
= [];
630 dirMoves
.forEach(m
=> {
631 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
632 const newDir
= V
.LANCER_DIRS
[k
];
633 // Prevent orientations toward outer board:
634 if (V
.OnBoard(m
.end
.x
+ newDir
[0], m
.end
.y
+ newDir
[1])) {
635 let mk
= JSON
.parse(JSON
.stringify(m
));
637 chooseMoves
.push(mk
);
641 Array
.prototype.push
.apply(moves
, chooseMoves
);
643 else Array
.prototype.push
.apply(moves
, dirMoves
);
648 // I wasn't pushed: standard lancer move
650 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
651 // Add all possible orientations aftermove except if I'm being pushed
652 if (this.subTurn
== 1) {
653 monodirMoves
.forEach(m
=> {
654 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
655 const newDir
= V
.LANCER_DIRS
[k
];
656 // Prevent orientations toward outer board:
657 if (V
.OnBoard(m
.end
.x
+ newDir
[0], m
.end
.y
+ newDir
[1])) {
658 let mk
= JSON
.parse(JSON
.stringify(m
));
667 // I'm pushed: add potential nudges, except for current orientation
668 let potentialNudges
= [];
669 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
671 (step
[0] != curDir
[0] || step
[1] != curDir
[1]) &&
672 V
.OnBoard(x
+ step
[0], y
+ step
[1]) &&
673 this.board
[x
+ step
[0]][y
+ step
[1]] == V
.EMPTY
675 const newDirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
676 const codeStep
= V
.LANCER_DIRS
[k
];
677 return (codeStep
[0] == step
[0] && codeStep
[1] == step
[1]);
679 potentialNudges
.push(
682 [x
+ step
[0], y
+ step
[1]],
683 { c: color
, p: newDirCode
}
688 return monodirMoves
.concat(potentialNudges
);
692 getPotentialSentryMoves([x
, y
]) {
693 // The sentry moves a priori like a bishop:
694 let moves
= super.getPotentialBishopMoves([x
, y
]);
695 // ...but captures are replaced by special move, if and only if
696 // "captured" piece can move now, considered as the capturer unit.
697 // --> except is subTurn == 2, in this case I don't push anything.
698 if (this.subTurn
== 2) return moves
.filter(m
=> m
.vanish
.length
== 1);
700 if (m
.vanish
.length
== 2) {
701 // Temporarily cancel the sentry capture:
706 const color
= this.getColor(x
, y
);
707 const fMoves
= moves
.filter(m
=> {
708 // Can the pushed unit make any move? ...resulting in a non-self-check?
709 if (m
.appear
.length
== 0) {
712 let moves2
= this.getPotentialMovesFrom([m
.end
.x
, m
.end
.y
]);
713 for (let m2
of moves2
) {
715 res
= !this.underCheck(color
);
727 getPotentialJailerMoves([x
, y
]) {
728 return super.getPotentialRookMoves([x
, y
]).filter(m
=> {
729 // Remove jailer captures
730 return m
.vanish
[0].p
!= V
.JAILER
|| m
.vanish
.length
== 1;
734 getPotentialKingMoves(sq
) {
735 const moves
= this.getSlideNJumpMoves(
737 V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]),
742 ? moves
.concat(this.getCastleMoves(sq
))
748 // If in second-half of a move, we already know that a move is possible
749 if (this.subTurn
== 2) return true;
750 return super.atLeastOneMove();
754 if (moves
.length
== 0) return [];
755 const basicFilter
= (m
, c
) => {
757 const res
= !this.underCheck(c
);
761 // Disable check tests for sentry pushes,
762 // because in this case the move isn't finished
763 let movesWithoutSentryPushes
= [];
764 let movesWithSentryPushes
= [];
766 // Second condition below for special king "pass" moves
767 if (m
.appear
.length
> 0 || m
.vanish
.length
== 0)
768 movesWithoutSentryPushes
.push(m
);
769 else movesWithSentryPushes
.push(m
);
771 const color
= this.turn
;
772 const oppCol
= V
.GetOppCol(color
);
773 const filteredMoves
=
774 movesWithoutSentryPushes
.filter(m
=> basicFilter(m
, color
));
775 // If at least one full move made, everything is allowed.
776 // Else: forbid checks and captures.
780 : filteredMoves
.filter(m
=> {
781 return (m
.vanish
.length
<= 1 && basicFilter(m
, oppCol
));
783 ).concat(movesWithSentryPushes
);
787 if (this.subTurn
== 1) return super.getAllValidMoves();
789 const sentrySq
= [this.sentryPos
.x
, this.sentryPos
.y
];
790 return this.filterValid(this.getPotentialMovesFrom(sentrySq
));
793 isAttacked(sq
, color
) {
795 super.isAttacked(sq
, color
) ||
796 this.isAttackedByLancer(sq
, color
) ||
797 this.isAttackedBySentry(sq
, color
)
798 // The jailer doesn't capture.
802 isAttackedBySlideNJump([x
, y
], color
, piece
, steps
, oneStep
) {
803 for (let step
of steps
) {
804 let rx
= x
+ step
[0],
806 while (V
.OnBoard(rx
, ry
) && this.board
[rx
][ry
] == V
.EMPTY
&& !oneStep
) {
812 this.getPiece(rx
, ry
) == piece
&&
813 this.getColor(rx
, ry
) == color
&&
814 !this.isImmobilized([rx
, ry
])
822 isAttackedByPawn([x
, y
], color
) {
823 const pawnShift
= (color
== "w" ? 1 : -1);
824 if (x
+ pawnShift
>= 0 && x
+ pawnShift
< V
.size
.x
) {
825 for (let i
of [-1, 1]) {
829 this.getPiece(x
+ pawnShift
, y
+ i
) == V
.PAWN
&&
830 this.getColor(x
+ pawnShift
, y
+ i
) == color
&&
831 !this.isImmobilized([x
+ pawnShift
, y
+ i
])
840 isAttackedByLancer([x
, y
], color
) {
841 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
842 // If in this direction there are only enemy pieces and empty squares,
843 // and we meet a lancer: can he reach us?
844 // NOTE: do not stop at first lancer, there might be several!
845 let coord
= { x: x
+ step
[0], y: y
+ step
[1] };
848 V
.OnBoard(coord
.x
, coord
.y
) &&
850 this.board
[coord
.x
][coord
.y
] == V
.EMPTY
||
851 this.getColor(coord
.x
, coord
.y
) == color
855 this.getPiece(coord
.x
, coord
.y
) == V
.LANCER
&&
856 !this.isImmobilized([coord
.x
, coord
.y
])
858 lancerPos
.push({x: coord
.x
, y: coord
.y
});
863 const L
= this.sentryPush
.length
;
864 const pl
= (!!this.sentryPush
[L
-1] ? this.sentryPush
[L
-1].length : 0);
865 for (let xy
of lancerPos
) {
866 const dir
= V
.LANCER_DIRS
[this.board
[xy
.x
][xy
.y
].charAt(1)];
868 (dir
[0] == -step
[0] && dir
[1] == -step
[1]) ||
869 // If the lancer was just pushed, this is an attack too:
871 !!this.sentryPush
[L
-1] &&
872 this.sentryPush
[L
-1][pl
-1].x
== xy
.x
&&
873 this.sentryPush
[L
-1][pl
-1].y
== xy
.y
883 // Helper to check sentries attacks:
884 selfAttack([x1
, y1
], [x2
, y2
]) {
885 const color
= this.getColor(x1
, y1
);
886 const oppCol
= V
.GetOppCol(color
);
887 const sliderAttack
= (allowedSteps
, lancer
) => {
888 const deltaX
= x2
- x1
,
889 absDeltaX
= Math
.abs(deltaX
);
890 const deltaY
= y2
- y1
,
891 absDeltaY
= Math
.abs(deltaY
);
892 const step
= [ deltaX
/ absDeltaX
|| 0, deltaY
/ absDeltaY
|| 0 ];
894 // Check that the step is a priori valid:
895 (absDeltaX
!= absDeltaY
&& deltaX
!= 0 && deltaY
!= 0) ||
896 allowedSteps
.every(st
=> st
[0] != step
[0] || st
[1] != step
[1])
900 let sq
= [ x1
+ step
[0], y1
+ step
[1] ];
901 while (sq
[0] != x2
|| sq
[1] != y2
) {
903 // NOTE: no need to check OnBoard in this special case
904 (!lancer
&& this.board
[sq
[0]][sq
[1]] != V
.EMPTY
) ||
905 (!!lancer
&& this.getColor(sq
[0], sq
[1]) == oppCol
)
914 switch (this.getPiece(x1
, y1
)) {
916 // Pushed pawns move as enemy pawns
917 const shift
= (color
== 'w' ? 1 : -1);
918 return (x1
+ shift
== x2
&& Math
.abs(y1
- y2
) == 1);
921 const deltaX
= Math
.abs(x1
- x2
);
922 const deltaY
= Math
.abs(y1
- y2
);
924 deltaX
+ deltaY
== 3 &&
925 [1, 2].includes(deltaX
) &&
926 [1, 2].includes(deltaY
)
930 return sliderAttack(V
.steps
[V
.ROOK
]);
932 return sliderAttack(V
.steps
[V
.BISHOP
]);
934 return sliderAttack(V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]));
936 // Special case: as long as no enemy units stands in-between,
937 // it attacks (if it points toward the king).
938 const allowedStep
= V
.LANCER_DIRS
[this.board
[x1
][y1
].charAt(1)];
939 return sliderAttack([allowedStep
], "lancer");
941 // No sentries or jailer tests: they cannot self-capture
946 isAttackedBySentry([x
, y
], color
) {
947 // Attacked by sentry means it can self-take our king.
948 // Just check diagonals of enemy sentry(ies), and if it reaches
949 // one of our pieces: can I self-take?
950 const myColor
= V
.GetOppCol(color
);
952 for (let i
=0; i
<V
.size
.x
; i
++) {
953 for (let j
=0; j
<V
.size
.y
; j
++) {
955 this.getPiece(i
,j
) == V
.SENTRY
&&
956 this.getColor(i
,j
) == color
&&
957 !this.isImmobilized([i
, j
])
959 for (let step
of V
.steps
[V
.BISHOP
]) {
960 let sq
= [ i
+ step
[0], j
+ step
[1] ];
962 V
.OnBoard(sq
[0], sq
[1]) &&
963 this.board
[sq
[0]][sq
[1]] == V
.EMPTY
969 V
.OnBoard(sq
[0], sq
[1]) &&
970 this.getColor(sq
[0], sq
[1]) == myColor
972 candidates
.push([ sq
[0], sq
[1] ]);
978 for (let c
of candidates
)
979 if (this.selfAttack(c
, [x
, y
])) return true;
983 // Jailer doesn't capture or give check
986 if (move.appear
.length
== 0 && move.vanish
.length
== 1)
987 // The sentry is about to push a piece: subTurn goes from 1 to 2
988 this.sentryPos
= { x: move.end
.x
, y: move.end
.y
};
989 if (this.subTurn
== 2 && move.vanish
[0].p
!= V
.PAWN
) {
990 // A piece is pushed: forbid array of squares between start and end
991 // of move, included (except if it's a pawn)
993 if ([V
.KNIGHT
,V
.KING
].includes(move.vanish
[0].p
))
994 // short-range pieces: just forbid initial square
995 squares
.push({ x: move.start
.x
, y: move.start
.y
});
997 const deltaX
= move.end
.x
- move.start
.x
;
998 const deltaY
= move.end
.y
- move.start
.y
;
1000 deltaX
/ Math
.abs(deltaX
) || 0,
1001 deltaY
/ Math
.abs(deltaY
) || 0
1004 let sq
= {x: move.start
.x
, y: move.start
.y
};
1005 sq
.x
!= move.end
.x
|| sq
.y
!= move.end
.y
;
1006 sq
.x
+= step
[0], sq
.y
+= step
[1]
1008 squares
.push({ x: sq
.x
, y: sq
.y
});
1011 // Add end square as well, to know if I was pushed (useful for lancers)
1012 squares
.push({ x: move.end
.x
, y: move.end
.y
});
1013 this.sentryPush
.push(squares
);
1014 } else this.sentryPush
.push(null);
1019 move.flags
= JSON
.stringify(this.aggregateFlags());
1020 this.epSquares
.push(this.getEpSquare(move));
1021 V
.PlayOnBoard(this.board
, move);
1022 // Is it a sentry push? (useful for undo)
1023 move.sentryPush
= (this.subTurn
== 2);
1024 if (this.subTurn
== 1) this.movesCount
++;
1025 if (move.appear
.length
== 0 && move.vanish
.length
== 1) this.subTurn
= 2;
1027 // Turn changes only if not a sentry "pre-push"
1028 this.turn
= V
.GetOppCol(this.turn
);
1031 this.postPlay(move);
1035 if (move.vanish
.length
== 0 || this.subTurn
== 2)
1036 // Special pass move of the king, or sentry pre-push: nothing to update
1038 const c
= move.vanish
[0].c
;
1039 const piece
= move.vanish
[0].p
;
1040 const firstRank
= c
== "w" ? V
.size
.x
- 1 : 0;
1042 if (piece
== V
.KING
) {
1043 this.kingPos
[c
][0] = move.appear
[0].x
;
1044 this.kingPos
[c
][1] = move.appear
[0].y
;
1045 this.castleFlags
[c
] = [V
.size
.y
, V
.size
.y
];
1048 // Update castling flags if rooks are moved
1049 const oppCol
= V
.GetOppCol(c
);
1050 const oppFirstRank
= V
.size
.x
- 1 - firstRank
;
1052 move.start
.x
== firstRank
&& //our rook moves?
1053 this.castleFlags
[c
].includes(move.start
.y
)
1055 const flagIdx
= (move.start
.y
== this.castleFlags
[c
][0] ? 0 : 1);
1056 this.castleFlags
[c
][flagIdx
] = V
.size
.y
;
1058 move.end
.x
== oppFirstRank
&& //we took opponent rook?
1059 this.castleFlags
[oppCol
].includes(move.end
.y
)
1061 const flagIdx
= (move.end
.y
== this.castleFlags
[oppCol
][0] ? 0 : 1);
1062 this.castleFlags
[oppCol
][flagIdx
] = V
.size
.y
;
1067 this.epSquares
.pop();
1068 this.disaggregateFlags(JSON
.parse(move.flags
));
1069 V
.UndoOnBoard(this.board
, move);
1070 // Decrement movesCount except if the move is a sentry push
1071 if (!move.sentryPush
) this.movesCount
--;
1072 if (this.subTurn
== 2) this.subTurn
= 1;
1074 this.turn
= V
.GetOppCol(this.turn
);
1075 if (move.sentryPush
) this.subTurn
= 2;
1077 this.postUndo(move);
1081 super.postUndo(move);
1082 this.sentryPush
.pop();
1085 static get VALUES() {
1086 return Object
.assign(
1087 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
1093 const maxeval
= V
.INFINITY
;
1094 const color
= this.turn
;
1095 let moves1
= this.getAllValidMoves();
1097 if (moves1
.length
== 0)
1098 // TODO: this situation should not happen
1101 const setEval
= (move, next
) => {
1102 const score
= this.getCurrentScore();
1103 const curEval
= move.eval
;
1108 : (score
== "1-0" ? 1 : -1) * maxeval
;
1109 } else move.eval
= this.evalPosition();
1111 // "next" is defined after sentry pushes
1114 color
== 'w' && move.eval
> curEval
||
1115 color
== 'b' && move.eval
< curEval
1122 // Just search_depth == 1 (because of sentries. TODO: can do better...)
1123 moves1
.forEach(m1
=> {
1125 if (this.subTurn
== 1) setEval(m1
);
1127 // Need to play every pushes and count:
1128 const moves2
= this.getAllValidMoves();
1129 moves2
.forEach(m2
=> {
1138 moves1
.sort((a
, b
) => {
1139 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
1141 let candidates
= [0];
1142 for (let j
= 1; j
< moves1
.length
&& moves1
[j
].eval
== moves1
[0].eval
; j
++)
1144 const choice
= moves1
[candidates
[randInt(candidates
.length
)]];
1145 return (!choice
.second
? choice : [choice
, choice
.second
]);
1148 // For moves notation:
1149 static get LANCER_DIRNAMES() {
1163 // Special case "king takes jailer" is a pass move
1164 if (move.appear
.length
== 0 && move.vanish
.length
== 0) return "pass";
1165 let notation
= undefined;
1166 if (this.subTurn
== 2) {
1167 // Do not consider appear[1] (sentry) for sentry pushes
1168 const simpleMove
= {
1169 appear: [move.appear
[0]],
1170 vanish: move.vanish
,
1174 notation
= super.getNotation(simpleMove
);
1177 move.appear
.length
> 0 &&
1178 move.vanish
[0].x
== move.appear
[0].x
&&
1179 move.vanish
[0].y
== move.appear
[0].y
1181 // Lancer in-place reorientation:
1182 notation
= "L" + V
.CoordsToSquare(move.start
) + ":R";
1184 else notation
= super.getNotation(move);
1185 if (Object
.keys(V
.LANCER_DIRNAMES
).includes(move.vanish
[0].p
))
1186 // Lancer: add direction info
1187 notation
+= "=" + V
.LANCER_DIRNAMES
[move.appear
[0].p
];
1189 move.vanish
[0].p
== V
.PAWN
&&
1190 Object
.keys(V
.LANCER_DIRNAMES
).includes(move.appear
[0].p
)
1192 // Fix promotions in lancer:
1193 notation
= notation
.slice(0, -1) +
1194 "L:" + V
.LANCER_DIRNAMES
[move.appear
[0].p
];