1 import { randInt
, sample
} from "@/utils/alea";
2 import { ChessRules
, PiPo
, Move
} from "@/base_rules";
4 export class EightpiecesRules
extends ChessRules
{
16 static get IMAGE_EXTENSION() {
17 // Temporarily, for the time SVG pieces are being designed:
21 // Lancer directions *from white perspective*
22 static get LANCER_DIRS() {
36 return ChessRules
.PIECES
37 .concat([V
.JAILER
, V
.SENTRY
])
38 .concat(Object
.keys(V
.LANCER_DIRS
));
42 const piece
= this.board
[i
][j
].charAt(1);
43 // Special lancer case: 8 possible orientations
44 if (Object
.keys(V
.LANCER_DIRS
).includes(piece
)) return V
.LANCER
;
48 getPpath(b
, color
, score
, orientation
) {
49 if ([V
.JAILER
, V
.SENTRY
].includes(b
[1])) return "Eightpieces/tmp_png/" + b
;
50 if (Object
.keys(V
.LANCER_DIRS
).includes(b
[1])) {
51 if (orientation
== 'w') return "Eightpieces/tmp_png/" + b
;
52 // Find opposite direction for adequate display:
80 return "Eightpieces/tmp_png/" + b
[0] + oppDir
;
82 // TODO: after we have SVG pieces, remove the folder and next prefix:
83 return "Eightpieces/tmp_png/" + b
;
86 getPPpath(m
, orientation
) {
89 m
.appear
[0].c
+ m
.appear
[0].p
,
97 static ParseFen(fen
) {
98 const fenParts
= fen
.split(" ");
100 ChessRules
.ParseFen(fen
),
101 { sentrypush: fenParts
[5] }
105 static IsGoodFen(fen
) {
106 if (!ChessRules
.IsGoodFen(fen
)) return false;
107 const fenParsed
= V
.ParseFen(fen
);
108 // 5) Check sentry push (if any)
110 fenParsed
.sentrypush
!= "-" &&
111 !fenParsed
.sentrypush
.match(/^([a-h][1-8]){2,2}$/)
119 return super.getFen() + " " + this.getSentrypushFen();
123 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
127 const L
= this.sentryPush
.length
;
128 if (!this.sentryPush
[L
-1]) return "-";
130 const spL
= this.sentryPush
[L
-1].length
;
131 // Condensate path: just need initial and final squares:
133 .map(i
=> V
.CoordsToSquare(this.sentryPush
[L
-1][i
]))
137 setOtherVariables(fen
) {
138 super.setOtherVariables(fen
);
139 // subTurn == 2 only when a sentry moved, and is about to push something
141 // Sentry position just after a "capture" (subTurn from 1 to 2)
142 this.sentryPos
= null;
143 // Stack pieces' forbidden squares after a sentry move at each turn
144 const parsedFen
= V
.ParseFen(fen
);
145 if (parsedFen
.sentrypush
== "-") this.sentryPush
= [null];
147 // Expand init + dest squares into a full path:
148 const init
= V
.SquareToCoords(parsedFen
.sentrypush
.substr(0, 2)),
149 dest
= V
.SquareToCoords(parsedFen
.sentrypush
.substr(2));
150 let newPath
= [init
];
151 const delta
= ['x', 'y'].map(i
=> Math
.abs(dest
[i
] - init
[i
]));
152 // Check that it's not a knight movement:
153 if (delta
[0] == 0 || delta
[1] == 0 || delta
[0] == delta
[1]) {
154 const step
= ['x', 'y'].map((i
, idx
) => {
155 return (dest
[i
] - init
[i
]) / delta
[idx
] || 0
157 let x
= init
.x
+ step
[0],
158 y
= init
.y
+ step
[1];
159 while (x
!= dest
.x
|| y
!= dest
.y
) {
160 newPath
.push({ x: x
, y: y
});
166 this.sentryPush
= [newPath
];
170 static GenRandInitFen(options
) {
171 if (options
.randomness
== 0)
172 return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -";
174 const baseFen
= ChessRules
.GenRandInitFen(options
);
175 const fenParts
= baseFen
.split(' ');
176 const posParts
= fenParts
[0].split('/');
178 // Replace one bishop by sentry, so that sentries on different colors
179 // Also replace one random rook by jailer,
180 // and one random knight by lancer (facing north/south)
181 let pieceLine
= { b: posParts
[0], w: posParts
[7].toLowerCase() };
182 let posBlack
= { r: -1, n: -1, b: -1 };
183 const mapP
= { r: 'j', n: 'l', b: 's' };
184 ['b', 'w'].forEach(c
=> {
185 ['r', 'n', 'b'].forEach(p
=> {
186 let pl
= pieceLine
[c
];
188 if (options
.randomness
== 2 || c
== 'b')
189 pos
= (randInt(2) == 0 ? pl
.indexOf(p
) : pl
.lastIndexOf(p
));
190 else pos
= posBlack
[p
];
192 pieceLine
[c
].substr(0, pos
) + mapP
[p
] + pieceLine
[c
].substr(pos
+1);
193 if (options
.randomness
== 1 && c
== 'b') posBlack
[p
] = pos
;
196 // Rename 'l' into 'g' (black) or 'c' (white)
197 pieceLine
['w'] = pieceLine
['w'].replace('l', 'c');
198 pieceLine
['b'] = pieceLine
['b'].replace('l', 'g');
199 if (options
.randomness
== 2) {
200 const ws
= pieceLine
['w'].indexOf('s');
201 const bs
= pieceLine
['b'].indexOf('s');
202 if (ws
% 2 != bs
% 2) {
203 // Fix sentry: should be on different colors.
204 // => move sentry on other bishop for random color
205 const c
= sample(['w', 'b'], 1);
206 pieceLine
[c
] = pieceLine
[c
]
207 .replace('b', 't') //tmp
214 pieceLine
['b'] + "/" +
215 posParts
.slice(1, 7).join('/') + "/" +
216 pieceLine
['w'].toUpperCase() + " " +
217 fenParts
.slice(1, 5).join(' ') + " -"
221 canTake([x1
, y1
], [x2
, y2
]) {
222 if (this.subTurn
== 2)
223 // Only self captures on this subturn:
224 return this.getColor(x1
, y1
) == this.getColor(x2
, y2
);
225 return super.canTake([x1
, y1
], [x2
, y2
]);
228 // Is piece on square (x,y) immobilized?
229 isImmobilized([x
, y
]) {
230 const color
= this.getColor(x
, y
);
231 const oppCol
= V
.GetOppCol(color
);
232 for (let step
of V
.steps
[V
.ROOK
]) {
233 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
236 this.board
[i
][j
] != V
.EMPTY
&&
237 this.getColor(i
, j
) == oppCol
239 if (this.getPiece(i
, j
) == V
.JAILER
) return [i
, j
];
245 canIplay(side
, [x
, y
]) {
247 (this.subTurn
== 1 && this.turn
== side
&& this.getColor(x
, y
) == side
)
249 (this.subTurn
== 2 && x
== this.sentryPos
.x
&& y
== this.sentryPos
.y
)
253 getPotentialMovesFrom([x
, y
]) {
254 const piece
= this.getPiece(x
, y
);
255 const L
= this.sentryPush
.length
;
256 // At subTurn == 2, jailers aren't effective (Jeff K)
257 if (this.subTurn
== 1) {
258 const jsq
= this.isImmobilized([x
, y
]);
261 // Special pass move if king:
262 if (piece
== V
.KING
) {
267 start: { x: x
, y: y
},
268 end: { x: jsq
[0], y: jsq
[1] }
272 else if (piece
== V
.LANCER
&& !!this.sentryPush
[L
-1]) {
273 // A pushed lancer next to the jailer: reorient
274 const color
= this.getColor(x
, y
);
275 const curDir
= this.board
[x
][y
].charAt(1);
276 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
279 appear: [{ x: x
, y: y
, c: color
, p: k
}],
280 vanish: [{ x: x
, y: y
, c: color
, p: curDir
}],
281 start: { x: x
, y: y
},
282 end: { x: jsq
[0], y: jsq
[1] }
293 moves
= this.getPotentialJailerMoves([x
, y
]);
296 moves
= this.getPotentialSentryMoves([x
, y
]);
299 moves
= this.getPotentialLancerMoves([x
, y
]);
302 moves
= super.getPotentialMovesFrom([x
, y
]);
305 if (!!this.sentryPush
[L
-1]) {
306 // Delete moves walking back on sentry push path,
307 // only if not a pawn, and the piece is the pushed one.
308 const pl
= this.sentryPush
[L
-1].length
;
309 const finalPushedSq
= this.sentryPush
[L
-1][pl
-1];
310 moves
= moves
.filter(m
=> {
312 m
.vanish
[0].p
!= V
.PAWN
&&
313 m
.start
.x
== finalPushedSq
.x
&& m
.start
.y
== finalPushedSq
.y
&&
314 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
321 else if (this.subTurn
== 2) {
322 // Put back the sentinel on board:
323 const color
= this.turn
;
325 m
.appear
.push({x: x
, y: y
, p: V
.SENTRY
, c: color
});
331 getPotentialPawnMoves([x
, y
]) {
332 const color
= this.getColor(x
, y
);
334 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
335 let shiftX
= (color
== "w" ? -1 : 1);
336 if (this.subTurn
== 2) shiftX
*= -1;
337 const firstRank
= color
== "w" ? sizeX
- 1 : 0;
338 const startRank
= color
== "w" ? sizeX
- 2 : 1;
339 const lastRank
= color
== "w" ? 0 : sizeX
- 1;
341 // Pawns might be pushed on 1st rank and attempt to move again:
342 if (!V
.OnBoard(x
+ shiftX
, y
)) return [];
344 // A push cannot put a pawn on last rank (it goes backward)
345 let finalPieces
= [V
.PAWN
];
346 if (x
+ shiftX
== lastRank
) {
347 // Only allow direction facing inside board:
348 const allowedLancerDirs
=
350 ? ['e', 'f', 'g', 'h', 'm']
351 : ['c', 'd', 'e', 'm', 'o'];
354 .concat([V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.SENTRY
, V
.JAILER
]);
356 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
357 // One square forward
358 for (let piece
of finalPieces
) {
360 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
367 // 2-squares jumps forbidden if pawn push
369 [startRank
, firstRank
].includes(x
) &&
370 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
373 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
377 for (let shiftY
of [-1, 1]) {
380 y
+ shiftY
< sizeY
&&
381 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
382 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
384 for (let piece
of finalPieces
) {
386 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
395 // En passant: only on subTurn == 1
396 const Lep
= this.epSquares
.length
;
397 const epSquare
= this.epSquares
[Lep
- 1];
401 epSquare
.x
== x
+ shiftX
&&
402 Math
.abs(epSquare
.y
- y
) == 1
404 let enpassantMove
= this.getBasicMove([x
, y
], [epSquare
.x
, epSquare
.y
]);
405 enpassantMove
.vanish
.push({
409 c: this.getColor(x
, epSquare
.y
)
411 moves
.push(enpassantMove
);
418 if (isNaN(square
[0])) return null;
419 const L
= this.sentryPush
.length
;
420 const [x
, y
] = [square
[0], square
[1]];
421 const color
= this.turn
;
424 this.board
[x
][y
] == V
.EMPTY
||
425 this.getPiece(x
, y
) != V
.LANCER
||
426 this.getColor(x
, y
) != color
||
427 !!this.sentryPush
[L
-1]
432 const orientation
= this.board
[x
][y
][1];
433 const step
= V
.LANCER_DIRS
[orientation
];
434 if (!V
.OnBoard(x
+ step
[0], y
+ step
[1])) {
436 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
437 const dir
= V
.LANCER_DIRS
[k
];
439 (dir
[0] != step
[0] || dir
[1] != step
[1]) &&
440 V
.OnBoard(x
+ dir
[0], y
+ dir
[1])
460 start: { x: x
, y : y
},
461 end: { x: -1, y: -1 }
471 // Obtain all lancer moves in "step" direction
472 getPotentialLancerMoves_aux([x
, y
], step
, tr
) {
474 // Add all moves to vacant squares until opponent is met:
475 const color
= this.getColor(x
, y
);
479 // at subTurn == 2, consider own pieces as opponent
481 let sq
= [x
+ step
[0], y
+ step
[1]];
482 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
483 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
484 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
488 if (V
.OnBoard(sq
[0], sq
[1]))
489 // Add capturing move
490 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
494 getPotentialLancerMoves([x
, y
]) {
496 // Add all lancer possible orientations, similar to pawn promotions.
497 // Except if just after a push: allow all movements from init square then
498 const L
= this.sentryPush
.length
;
499 const color
= this.getColor(x
, y
);
500 const dirCode
= this.board
[x
][y
][1];
501 const curDir
= V
.LANCER_DIRS
[dirCode
];
502 if (!!this.sentryPush
[L
-1]) {
503 // Maybe I was pushed
504 const pl
= this.sentryPush
[L
-1].length
;
506 this.sentryPush
[L
-1][pl
-1].x
== x
&&
507 this.sentryPush
[L
-1][pl
-1].y
== y
509 // I was pushed: allow all directions (for this move only), but
510 // do not change direction after moving, *except* if I keep the
511 // same orientation in which I was pushed.
512 // Also allow simple reorientation ("capturing king"):
513 if (!V
.OnBoard(x
+ curDir
[0], y
+ curDir
[1])) {
514 const kp
= this.kingPos
[color
];
515 let reorientMoves
= [];
516 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
517 const dir
= V
.LANCER_DIRS
[k
];
519 (dir
[0] != curDir
[0] || dir
[1] != curDir
[1]) &&
520 V
.OnBoard(x
+ dir
[0], y
+ dir
[1])
540 start: { x: x
, y : y
},
541 end: { x: kp
[0], y: kp
[1] }
546 Array
.prototype.push
.apply(moves
, reorientMoves
);
548 Object
.values(V
.LANCER_DIRS
).forEach(step
=> {
549 const dirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
551 V
.LANCER_DIRS
[k
][0] == step
[0] &&
552 V
.LANCER_DIRS
[k
][1] == step
[1]
556 this.getPotentialLancerMoves_aux(
559 { p: dirCode
, c: color
}
561 if (curDir
[0] == step
[0] && curDir
[1] == step
[1]) {
562 // Keeping same orientation: can choose after
563 let chooseMoves
= [];
564 dirMoves
.forEach(m
=> {
565 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
566 const newDir
= V
.LANCER_DIRS
[k
];
567 // Prevent orientations toward outer board:
568 if (V
.OnBoard(m
.end
.x
+ newDir
[0], m
.end
.y
+ newDir
[1])) {
569 let mk
= JSON
.parse(JSON
.stringify(m
));
571 chooseMoves
.push(mk
);
575 Array
.prototype.push
.apply(moves
, chooseMoves
);
577 else Array
.prototype.push
.apply(moves
, dirMoves
);
582 // I wasn't pushed: standard lancer move
584 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
585 // Add all possible orientations aftermove except if I'm being pushed
586 if (this.subTurn
== 1) {
587 monodirMoves
.forEach(m
=> {
588 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
589 const newDir
= V
.LANCER_DIRS
[k
];
590 // Prevent orientations toward outer board:
591 if (V
.OnBoard(m
.end
.x
+ newDir
[0], m
.end
.y
+ newDir
[1])) {
592 let mk
= JSON
.parse(JSON
.stringify(m
));
601 // I'm pushed: add potential nudges, except for current orientation
602 let potentialNudges
= [];
603 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
605 (step
[0] != curDir
[0] || step
[1] != curDir
[1]) &&
606 V
.OnBoard(x
+ step
[0], y
+ step
[1]) &&
607 this.board
[x
+ step
[0]][y
+ step
[1]] == V
.EMPTY
609 const newDirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
610 const codeStep
= V
.LANCER_DIRS
[k
];
611 return (codeStep
[0] == step
[0] && codeStep
[1] == step
[1]);
613 potentialNudges
.push(
616 [x
+ step
[0], y
+ step
[1]],
617 { c: color
, p: newDirCode
}
622 return monodirMoves
.concat(potentialNudges
);
626 getPotentialSentryMoves([x
, y
]) {
627 // The sentry moves a priori like a bishop:
628 let moves
= super.getPotentialBishopMoves([x
, y
]);
629 // ...but captures are replaced by special move, if and only if
630 // "captured" piece can move now, considered as the capturer unit.
631 // --> except is subTurn == 2, in this case I don't push anything.
632 if (this.subTurn
== 2) return moves
.filter(m
=> m
.vanish
.length
== 1);
634 if (m
.vanish
.length
== 2) {
635 // Temporarily cancel the sentry capture:
640 const color
= this.getColor(x
, y
);
641 const fMoves
= moves
.filter(m
=> {
642 // Can the pushed unit make any move? ...resulting in a non-self-check?
643 if (m
.appear
.length
== 0) {
646 let moves2
= this.getPotentialMovesFrom([m
.end
.x
, m
.end
.y
]);
647 for (let m2
of moves2
) {
649 res
= !this.underCheck(color
);
661 getPotentialJailerMoves([x
, y
]) {
662 return super.getPotentialRookMoves([x
, y
]).filter(m
=> {
663 // Remove jailer captures
664 return m
.vanish
[0].p
!= V
.JAILER
|| m
.vanish
.length
== 1;
668 getPotentialKingMoves(sq
) {
669 const moves
= this.getSlideNJumpMoves(
670 sq
, V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]), 1);
673 ? moves
.concat(this.getCastleMoves(sq
))
679 // If in second-half of a move, we already know that a move is possible
680 if (this.subTurn
== 2) return true;
681 return super.atLeastOneMove();
685 if (moves
.length
== 0) return [];
686 const basicFilter
= (m
, c
) => {
688 const res
= !this.underCheck(c
);
692 // Disable check tests for sentry pushes,
693 // because in this case the move isn't finished
694 let movesWithoutSentryPushes
= [];
695 let movesWithSentryPushes
= [];
697 // Second condition below for special king "pass" moves
698 if (m
.appear
.length
> 0 || m
.vanish
.length
== 0)
699 movesWithoutSentryPushes
.push(m
);
700 else movesWithSentryPushes
.push(m
);
702 const color
= this.turn
;
703 const oppCol
= V
.GetOppCol(color
);
704 const filteredMoves
=
705 movesWithoutSentryPushes
.filter(m
=> basicFilter(m
, color
));
706 // If at least one full move made, everything is allowed.
707 // Else: forbid checks and captures.
711 : filteredMoves
.filter(m
=> {
712 return (m
.vanish
.length
<= 1 && basicFilter(m
, oppCol
));
714 ).concat(movesWithSentryPushes
);
718 if (this.subTurn
== 1) return super.getAllValidMoves();
720 const sentrySq
= [this.sentryPos
.x
, this.sentryPos
.y
];
721 return this.filterValid(this.getPotentialMovesFrom(sentrySq
));
724 isAttacked(sq
, color
) {
726 super.isAttacked(sq
, color
) ||
727 this.isAttackedByLancer(sq
, color
) ||
728 this.isAttackedBySentry(sq
, color
)
729 // The jailer doesn't capture.
733 isAttackedBySlideNJump([x
, y
], color
, piece
, steps
, oneStep
) {
734 for (let step
of steps
) {
735 let rx
= x
+ step
[0],
737 while (V
.OnBoard(rx
, ry
) && this.board
[rx
][ry
] == V
.EMPTY
&& !oneStep
) {
743 this.getPiece(rx
, ry
) == piece
&&
744 this.getColor(rx
, ry
) == color
&&
745 !this.isImmobilized([rx
, ry
])
753 isAttackedByPawn([x
, y
], color
) {
754 const pawnShift
= (color
== "w" ? 1 : -1);
755 if (x
+ pawnShift
>= 0 && x
+ pawnShift
< V
.size
.x
) {
756 for (let i
of [-1, 1]) {
760 this.getPiece(x
+ pawnShift
, y
+ i
) == V
.PAWN
&&
761 this.getColor(x
+ pawnShift
, y
+ i
) == color
&&
762 !this.isImmobilized([x
+ pawnShift
, y
+ i
])
771 isAttackedByLancer([x
, y
], color
) {
772 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
773 // If in this direction there are only enemy pieces and empty squares,
774 // and we meet a lancer: can he reach us?
775 // NOTE: do not stop at first lancer, there might be several!
776 let coord
= { x: x
+ step
[0], y: y
+ step
[1] };
779 V
.OnBoard(coord
.x
, coord
.y
) &&
781 this.board
[coord
.x
][coord
.y
] == V
.EMPTY
||
782 this.getColor(coord
.x
, coord
.y
) == color
786 this.getPiece(coord
.x
, coord
.y
) == V
.LANCER
&&
787 !this.isImmobilized([coord
.x
, coord
.y
])
789 lancerPos
.push({x: coord
.x
, y: coord
.y
});
794 const L
= this.sentryPush
.length
;
795 const pl
= (!!this.sentryPush
[L
-1] ? this.sentryPush
[L
-1].length : 0);
796 for (let xy
of lancerPos
) {
797 const dir
= V
.LANCER_DIRS
[this.board
[xy
.x
][xy
.y
].charAt(1)];
799 (dir
[0] == -step
[0] && dir
[1] == -step
[1]) ||
800 // If the lancer was just pushed, this is an attack too:
802 !!this.sentryPush
[L
-1] &&
803 this.sentryPush
[L
-1][pl
-1].x
== xy
.x
&&
804 this.sentryPush
[L
-1][pl
-1].y
== xy
.y
814 // Helper to check sentries attacks:
815 selfAttack([x1
, y1
], [x2
, y2
]) {
816 const color
= this.getColor(x1
, y1
);
817 const oppCol
= V
.GetOppCol(color
);
818 const sliderAttack
= (allowedSteps
, lancer
) => {
819 const deltaX
= x2
- x1
,
821 const absDeltaX
= Math
.abs(deltaX
),
822 absDeltaY
= Math
.abs(deltaY
);
823 const step
= [ deltaX
/ absDeltaX
|| 0, deltaY
/ absDeltaY
|| 0 ];
825 // Check that the step is a priori valid:
826 (absDeltaX
!= absDeltaY
&& deltaX
!= 0 && deltaY
!= 0) ||
827 allowedSteps
.every(st
=> st
[0] != step
[0] || st
[1] != step
[1])
831 let sq
= [ x1
+ step
[0], y1
+ step
[1] ];
832 while (sq
[0] != x2
|| sq
[1] != y2
) {
833 // NOTE: no need to check OnBoard in this special case
834 if (this.board
[sq
[0]][sq
[1]] != V
.EMPTY
) {
835 const p
= this.getPiece(sq
[0], sq
[1]);
836 const pc
= this.getColor(sq
[0], sq
[1]);
838 // Enemy sentry on the way will be gone:
839 (p
!= V
.SENTRY
|| pc
!= oppCol
) &&
840 // Lancer temporarily "changed color":
841 (!lancer
|| pc
== color
)
851 switch (this.getPiece(x1
, y1
)) {
853 // Pushed pawns move as enemy pawns
854 const shift
= (color
== 'w' ? 1 : -1);
855 return (x1
+ shift
== x2
&& Math
.abs(y1
- y2
) == 1);
858 const deltaX
= Math
.abs(x1
- x2
);
859 const deltaY
= Math
.abs(y1
- y2
);
861 deltaX
+ deltaY
== 3 &&
862 [1, 2].includes(deltaX
) &&
863 [1, 2].includes(deltaY
)
867 return sliderAttack(V
.steps
[V
.ROOK
]);
869 return sliderAttack(V
.steps
[V
.BISHOP
]);
871 return sliderAttack(V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]));
873 // Special case: as long as no enemy units stands in-between,
874 // it attacks (if it points toward the king).
875 const allowedStep
= V
.LANCER_DIRS
[this.board
[x1
][y1
].charAt(1)];
876 return sliderAttack([allowedStep
], "lancer");
878 // No sentries or jailer tests: they cannot self-capture
883 isAttackedBySentry([x
, y
], color
) {
884 // Attacked by sentry means it can self-take our king.
885 // Just check diagonals of enemy sentry(ies), and if it reaches
886 // one of our pieces: can I self-take?
887 const myColor
= V
.GetOppCol(color
);
889 for (let i
=0; i
<V
.size
.x
; i
++) {
890 for (let j
=0; j
<V
.size
.y
; j
++) {
892 this.getPiece(i
,j
) == V
.SENTRY
&&
893 this.getColor(i
,j
) == color
&&
894 !this.isImmobilized([i
, j
])
896 for (let step
of V
.steps
[V
.BISHOP
]) {
897 let sq
= [ i
+ step
[0], j
+ step
[1] ];
899 V
.OnBoard(sq
[0], sq
[1]) &&
900 this.board
[sq
[0]][sq
[1]] == V
.EMPTY
906 V
.OnBoard(sq
[0], sq
[1]) &&
907 this.getColor(sq
[0], sq
[1]) == myColor
909 candidates
.push([ sq
[0], sq
[1] ]);
915 for (let c
of candidates
)
916 if (this.selfAttack(c
, [x
, y
])) return true;
920 // Jailer doesn't capture or give check
923 if (move.appear
.length
== 0 && move.vanish
.length
== 1)
924 // The sentry is about to push a piece: subTurn goes from 1 to 2
925 this.sentryPos
= { x: move.end
.x
, y: move.end
.y
};
926 if (this.subTurn
== 2 && move.vanish
[0].p
!= V
.PAWN
) {
927 // A piece is pushed: forbid array of squares between start and end
928 // of move, included (except if it's a pawn)
930 if ([V
.KNIGHT
,V
.KING
].includes(move.vanish
[0].p
))
931 // short-range pieces: just forbid initial square
932 squares
.push({ x: move.start
.x
, y: move.start
.y
});
934 const deltaX
= move.end
.x
- move.start
.x
;
935 const deltaY
= move.end
.y
- move.start
.y
;
937 deltaX
/ Math
.abs(deltaX
) || 0,
938 deltaY
/ Math
.abs(deltaY
) || 0
941 let sq
= {x: move.start
.x
, y: move.start
.y
};
942 sq
.x
!= move.end
.x
|| sq
.y
!= move.end
.y
;
943 sq
.x
+= step
[0], sq
.y
+= step
[1]
945 squares
.push({ x: sq
.x
, y: sq
.y
});
948 // Add end square as well, to know if I was pushed (useful for lancers)
949 squares
.push({ x: move.end
.x
, y: move.end
.y
});
950 this.sentryPush
.push(squares
);
951 } else this.sentryPush
.push(null);
956 move.flags
= JSON
.stringify(this.aggregateFlags());
957 this.epSquares
.push(this.getEpSquare(move));
958 V
.PlayOnBoard(this.board
, move);
959 // Is it a sentry push? (useful for undo)
960 move.sentryPush
= (this.subTurn
== 2);
961 if (this.subTurn
== 1) this.movesCount
++;
962 if (move.appear
.length
== 0 && move.vanish
.length
== 1) this.subTurn
= 2;
964 // Turn changes only if not a sentry "pre-push"
965 this.turn
= V
.GetOppCol(this.turn
);
972 if (move.vanish
.length
== 0 || this.subTurn
== 2)
973 // Special pass move of the king, or sentry pre-push: nothing to update
975 const c
= move.vanish
[0].c
;
976 const piece
= move.vanish
[0].p
;
977 const firstRank
= c
== "w" ? V
.size
.x
- 1 : 0;
979 if (piece
== V
.KING
) {
980 this.kingPos
[c
][0] = move.appear
[0].x
;
981 this.kingPos
[c
][1] = move.appear
[0].y
;
982 this.castleFlags
[c
] = [V
.size
.y
, V
.size
.y
];
985 // Update castling flags if rooks are moved
986 const oppCol
= V
.GetOppCol(c
);
987 const oppFirstRank
= V
.size
.x
- 1 - firstRank
;
989 move.start
.x
== firstRank
&& //our rook moves?
990 this.castleFlags
[c
].includes(move.start
.y
)
992 const flagIdx
= (move.start
.y
== this.castleFlags
[c
][0] ? 0 : 1);
993 this.castleFlags
[c
][flagIdx
] = V
.size
.y
;
995 move.end
.x
== oppFirstRank
&& //we took opponent rook?
996 this.castleFlags
[oppCol
].includes(move.end
.y
)
998 const flagIdx
= (move.end
.y
== this.castleFlags
[oppCol
][0] ? 0 : 1);
999 this.castleFlags
[oppCol
][flagIdx
] = V
.size
.y
;
1004 this.epSquares
.pop();
1005 this.disaggregateFlags(JSON
.parse(move.flags
));
1006 V
.UndoOnBoard(this.board
, move);
1007 // Decrement movesCount except if the move is a sentry push
1008 if (!move.sentryPush
) this.movesCount
--;
1009 if (this.subTurn
== 2) this.subTurn
= 1;
1011 this.turn
= V
.GetOppCol(this.turn
);
1012 if (move.sentryPush
) this.subTurn
= 2;
1014 this.postUndo(move);
1018 super.postUndo(move);
1019 this.sentryPush
.pop();
1022 static get VALUES() {
1023 return Object
.assign(
1024 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
1030 const maxeval
= V
.INFINITY
;
1031 const color
= this.turn
;
1032 let moves1
= this.getAllValidMoves();
1034 if (moves1
.length
== 0)
1035 // TODO: this situation should not happen
1038 const setEval
= (move, next
) => {
1039 const score
= this.getCurrentScore();
1040 const curEval
= move.eval
;
1045 : (score
== "1-0" ? 1 : -1) * maxeval
;
1046 } else move.eval
= this.evalPosition();
1048 // "next" is defined after sentry pushes
1051 color
== 'w' && move.eval
> curEval
||
1052 color
== 'b' && move.eval
< curEval
1059 // Just search_depth == 1 (because of sentries. TODO: can do better...)
1060 moves1
.forEach(m1
=> {
1062 if (this.subTurn
== 1) setEval(m1
);
1064 // Need to play every pushes and count:
1065 const moves2
= this.getAllValidMoves();
1066 moves2
.forEach(m2
=> {
1075 moves1
.sort((a
, b
) => {
1076 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
1078 let candidates
= [0];
1079 for (let j
= 1; j
< moves1
.length
&& moves1
[j
].eval
== moves1
[0].eval
; j
++)
1081 const choice
= moves1
[candidates
[randInt(candidates
.length
)]];
1082 return (!choice
.second
? choice : [choice
, choice
.second
]);
1085 // For moves notation:
1086 static get LANCER_DIRNAMES() {
1100 // Special case "king takes jailer" is a pass move
1101 if (move.appear
.length
== 0 && move.vanish
.length
== 0) return "pass";
1102 let notation
= undefined;
1103 if (this.subTurn
== 2) {
1104 // Do not consider appear[1] (sentry) for sentry pushes
1105 const simpleMove
= {
1106 appear: [move.appear
[0]],
1107 vanish: move.vanish
,
1111 notation
= super.getNotation(simpleMove
);
1114 move.appear
.length
> 0 &&
1115 move.vanish
[0].x
== move.appear
[0].x
&&
1116 move.vanish
[0].y
== move.appear
[0].y
1118 // Lancer in-place reorientation:
1119 notation
= "L" + V
.CoordsToSquare(move.start
) + ":R";
1121 else notation
= super.getNotation(move);
1122 if (Object
.keys(V
.LANCER_DIRNAMES
).includes(move.vanish
[0].p
))
1123 // Lancer: add direction info
1124 notation
+= "=" + V
.LANCER_DIRNAMES
[move.appear
[0].p
];
1126 move.vanish
[0].p
== V
.PAWN
&&
1127 Object
.keys(V
.LANCER_DIRNAMES
).includes(move.appear
[0].p
)
1129 // Fix promotions in lancer:
1130 notation
= notation
.slice(0, -1) +
1131 "L:" + V
.LANCER_DIRNAMES
[move.appear
[0].p
];