c83cf5ed48a5b2576ddd1cc3d4766b10d12da5af
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
{
17 return ChessRules
.PIECES
.concat([V
.JAILER
, V
.SENTRY
, V
.LANCER
]);
20 // Lancer directions *from white perspective*
21 static get LANCER_DIRS() {
35 const piece
= this.board
[i
][j
].charAt(1);
36 // Special lancer case: 8 possible orientations
37 if (Object
.keys(V
.LANCER_DIRS
).includes(piece
)) return V
.LANCER
;
42 if ([V
.JAILER
, V
.SENTRY
].concat(Object
.keys(V
.LANCER_DIRS
)).includes(b
[1]))
43 return "Eightpieces/" + b
;
47 static ParseFen(fen
) {
48 const fenParts
= fen
.split(" ");
49 return Object
.assign(ChessRules
.ParseFen(fen
), {
50 sentrypush: fenParts
[5]
55 return super.getFen() + " " + this.getSentrypushFen();
59 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
63 const L
= this.sentryPush
.length
;
64 if (!this.sentryPush
[L
-1]) return "-";
66 this.sentryPush
[L
-1].forEach(coords
=>
67 res
+= V
.CoordsToSquare(coords
) + ",");
68 return res
.slice(0, -1);
71 setOtherVariables(fen
) {
72 super.setOtherVariables(fen
);
73 // subTurn == 2 only when a sentry moved, and is about to push something
75 // Sentry position just after a "capture" (subTurn from 1 to 2)
76 this.sentryPos
= null;
77 // Stack pieces' forbidden squares after a sentry move at each turn
78 const parsedFen
= V
.ParseFen(fen
);
79 if (parsedFen
.sentrypush
== "-") this.sentryPush
= [null];
82 parsedFen
.sentrypush
.split(",").map(sq
=> {
83 return V
.SquareToCoords(sq
);
89 static GenRandInitFen(randomness
) {
92 return "jsfqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JSDQKBNR w 0 1111 - -";
94 let pieces
= { w: new Array(8), b: new Array(8) };
95 // Shuffle pieces on first (and last rank if randomness == 2)
96 for (let c
of ["w", "b"]) {
97 if (c
== 'b' && randomness
== 1) {
98 const lancerIdx
= pieces
['w'].findIndex(p
=> {
99 return Object
.keys(V
.LANCER_DIRS
).includes(p
);
102 pieces
['w'].slice(0, lancerIdx
)
104 .concat(pieces
['w'].slice(lancerIdx
+ 1));
108 let positions
= ArrayFun
.range(8);
110 // Get random squares for bishop and sentry
111 let randIndex
= 2 * randInt(4);
112 let bishopPos
= positions
[randIndex
];
113 // The sentry must be on a square of different color
114 let randIndex_tmp
= 2 * randInt(4) + 1;
115 let sentryPos
= positions
[randIndex_tmp
];
117 // Check if white sentry is on the same color as ours.
118 // If yes: swap bishop and sentry positions.
119 if ((pieces
['w'].indexOf('s') - sentryPos
) % 2 == 0)
120 [bishopPos
, sentryPos
] = [sentryPos
, bishopPos
];
122 positions
.splice(Math
.max(randIndex
, randIndex_tmp
), 1);
123 positions
.splice(Math
.min(randIndex
, randIndex_tmp
), 1);
125 // Get random squares for knight and lancer
126 randIndex
= randInt(6);
127 const knightPos
= positions
[randIndex
];
128 positions
.splice(randIndex
, 1);
129 randIndex
= randInt(5);
130 const lancerPos
= positions
[randIndex
];
131 positions
.splice(randIndex
, 1);
133 // Get random square for queen
134 randIndex
= randInt(4);
135 const queenPos
= positions
[randIndex
];
136 positions
.splice(randIndex
, 1);
138 // Rook, jailer and king positions are now almost fixed,
139 // only the ordering rook-> jailer or jailer->rook must be decided.
140 let rookPos
= positions
[0];
141 let jailerPos
= positions
[2];
142 const kingPos
= positions
[1];
143 if (Math
.random() < 0.5) [rookPos
, jailerPos
] = [jailerPos
, rookPos
];
145 pieces
[c
][rookPos
] = "r";
146 pieces
[c
][knightPos
] = "n";
147 pieces
[c
][bishopPos
] = "b";
148 pieces
[c
][queenPos
] = "q";
149 pieces
[c
][kingPos
] = "k";
150 pieces
[c
][sentryPos
] = "s";
151 // Lancer faces north for white, and south for black:
152 pieces
[c
][lancerPos
] = c
== 'w' ? 'c' : 'g';
153 pieces
[c
][jailerPos
] = "j";
156 pieces
["b"].join("") +
157 "/pppppppp/8/8/8/8/PPPPPPPP/" +
158 pieces
["w"].join("").toUpperCase() +
163 // Scan kings, rooks and jailers
164 scanKingsRooks(fen
) {
165 this.INIT_COL_KING
= { w: -1, b: -1 };
166 this.INIT_COL_ROOK
= { w: -1, b: -1 };
167 this.INIT_COL_JAILER
= { w: -1, b: -1 };
168 this.kingPos
= { w: [-1, -1], b: [-1, -1] };
169 const fenRows
= V
.ParseFen(fen
).position
.split("/");
170 const startRow
= { 'w': V
.size
.x
- 1, 'b': 0 };
171 for (let i
= 0; i
< fenRows
.length
; i
++) {
173 for (let j
= 0; j
< fenRows
[i
].length
; j
++) {
174 switch (fenRows
[i
].charAt(j
)) {
176 this.kingPos
["b"] = [i
, k
];
177 this.INIT_COL_KING
["b"] = k
;
180 this.kingPos
["w"] = [i
, k
];
181 this.INIT_COL_KING
["w"] = k
;
184 if (i
== startRow
['b'] && this.INIT_COL_ROOK
["b"] < 0)
185 this.INIT_COL_ROOK
["b"] = k
;
188 if (i
== startRow
['w'] && this.INIT_COL_ROOK
["w"] < 0)
189 this.INIT_COL_ROOK
["w"] = k
;
192 if (i
== startRow
['b'] && this.INIT_COL_JAILER
["b"] < 0)
193 this.INIT_COL_JAILER
["b"] = k
;
196 if (i
== startRow
['w'] && this.INIT_COL_JAILER
["w"] < 0)
197 this.INIT_COL_JAILER
["w"] = k
;
200 const num
= parseInt(fenRows
[i
].charAt(j
));
201 if (!isNaN(num
)) k
+= num
- 1;
209 // Is piece on square (x,y) immobilized?
210 isImmobilized([x
, y
]) {
211 const color
= this.getColor(x
, y
);
212 const oppCol
= V
.GetOppCol(color
);
213 for (let step
of V
.steps
[V
.ROOK
]) {
214 const [i
, j
] = [x
+ step
[0], y
+ step
[1]];
217 this.board
[i
][j
] != V
.EMPTY
&&
218 this.getColor(i
, j
) == oppCol
220 if (this.getPiece(i
, j
) == V
.JAILER
) return [i
, j
];
226 // Because of the lancers, getPiece() could be wrong:
227 // use board[x][y][1] instead (always valid).
228 getBasicMove([sx
, sy
], [ex
, ey
], tr
) {
234 c: tr
? tr
.c : this.getColor(sx
, sy
),
235 p: tr
? tr
.p : this.board
[sx
][sy
].charAt(1)
242 c: this.getColor(sx
, sy
),
243 p: this.board
[sx
][sy
].charAt(1)
248 // The opponent piece disappears if we take it
249 if (this.board
[ex
][ey
] != V
.EMPTY
) {
254 c: this.getColor(ex
, ey
),
255 p: this.board
[ex
][ey
].charAt(1)
263 canIplay(side
, [x
, y
]) {
265 (this.subTurn
== 1 && this.turn
== side
&& this.getColor(x
, y
) == side
) ||
266 (this.subTurn
== 2 && x
== this.sentryPos
.x
&& y
== this.sentryPos
.y
)
270 getPotentialMovesFrom([x
,y
]) {
271 // At subTurn == 2, jailers aren't effective (Jeff K)
272 if (this.subTurn
== 1 && !!this.isImmobilized([x
, y
])) return [];
273 if (this.subTurn
== 2) {
274 // Temporarily change pushed piece color.
275 // (Not using getPiece() because of lancers)
276 var oppCol
= this.getColor(x
, y
);
277 var color
= V
.GetOppCol(oppCol
);
278 var saveXYstate
= this.board
[x
][y
];
279 this.board
[x
][y
] = color
+ this.board
[x
][y
].charAt(1);
282 switch (this.getPiece(x
, y
)) {
284 moves
= this.getPotentialJailerMoves([x
, y
]);
287 moves
= this.getPotentialSentryMoves([x
, y
]);
290 moves
= this.getPotentialLancerMoves([x
, y
]);
293 moves
= super.getPotentialMovesFrom([x
, y
]);
296 const L
= this.sentryPush
.length
;
297 if (!!this.sentryPush
[L
-1]) {
298 // Delete moves walking back on sentry push path
299 moves
= moves
.filter(m
=> {
301 m
.vanish
[0].p
!= V
.PAWN
&&
302 this.sentryPush
[L
-1].some(sq
=> sq
.x
== m
.end
.x
&& sq
.y
== m
.end
.y
)
309 else if (this.subTurn
== 2) {
310 // Don't forget to re-add the sentry on the board:
311 // Also fix color of pushed piece afterward:
313 m
.appear
.push({x: x
, y: y
, p: V
.SENTRY
, c: color
});
314 m
.appear
[0].c
= oppCol
;
315 m
.vanish
[0].c
= oppCol
;
321 getPotentialPawnMoves([x
, y
]) {
322 const color
= this.getColor(x
, y
);
324 const [sizeX
, sizeY
] = [V
.size
.x
, V
.size
.y
];
325 const shiftX
= color
== "w" ? -1 : 1;
326 const startRank
= color
== "w" ? sizeX
- 2 : 1;
327 const lastRank
= color
== "w" ? 0 : sizeX
- 1;
330 // No promotions after pushes!
331 x
+ shiftX
== lastRank
&& this.subTurn
== 1
333 Object
.keys(V
.LANCER_DIRS
).concat(
334 [V
.ROOK
, V
.KNIGHT
, V
.BISHOP
, V
.QUEEN
, V
.SENTRY
, V
.JAILER
])
336 if (this.board
[x
+ shiftX
][y
] == V
.EMPTY
) {
337 // One square forward
338 for (let piece
of finalPieces
) {
340 this.getBasicMove([x
, y
], [x
+ shiftX
, y
], {
348 this.board
[x
+ 2 * shiftX
][y
] == V
.EMPTY
351 moves
.push(this.getBasicMove([x
, y
], [x
+ 2 * shiftX
, y
]));
355 for (let shiftY
of [-1, 1]) {
358 y
+ shiftY
< sizeY
&&
359 this.board
[x
+ shiftX
][y
+ shiftY
] != V
.EMPTY
&&
360 this.canTake([x
, y
], [x
+ shiftX
, y
+ shiftY
])
362 for (let piece
of finalPieces
) {
364 this.getBasicMove([x
, y
], [x
+ shiftX
, y
+ shiftY
], {
374 const Lep
= this.epSquares
.length
;
375 const epSquare
= this.epSquares
[Lep
- 1]; //always at least one element
378 epSquare
.x
== x
+ shiftX
&&
379 Math
.abs(epSquare
.y
- y
) == 1
381 let enpassantMove
= this.getBasicMove([x
, y
], [epSquare
.x
, epSquare
.y
]);
382 enpassantMove
.vanish
.push({
386 c: this.getColor(x
, epSquare
.y
)
388 moves
.push(enpassantMove
);
394 // Obtain all lancer moves in "step" direction,
395 // without final re-orientation.
396 getPotentialLancerMoves_aux([x
, y
], step
, tr
) {
398 // Add all moves to vacant squares until opponent is met:
399 const oppCol
= V
.GetOppCol(this.getColor(x
, y
));
400 let sq
= [x
+ step
[0], y
+ step
[1]];
401 while (V
.OnBoard(sq
[0], sq
[1]) && this.getColor(sq
[0], sq
[1]) != oppCol
) {
402 if (this.board
[sq
[0]][sq
[1]] == V
.EMPTY
)
403 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
407 if (V
.OnBoard(sq
[0], sq
[1]))
408 // Add capturing move
409 moves
.push(this.getBasicMove([x
, y
], sq
, tr
));
413 getPotentialLancerMoves([x
, y
]) {
415 // Add all lancer possible orientations, similar to pawn promotions.
416 // Except if just after a push: allow all movements from init square then
417 const L
= this.sentryPush
.length
;
418 if (!!this.sentryPush
[L
-1]) {
419 // Maybe I was pushed
420 const pl
= this.sentryPush
[L
-1].length
;
422 this.sentryPush
[L
-1][pl
-1].x
== x
&&
423 this.sentryPush
[L
-1][pl
-1].y
== y
425 // I was pushed: allow all directions (for this move only), but
426 // do not change direction after moving.
427 const color
= this.getColor(x
, y
);
428 Object
.values(V
.LANCER_DIRS
).forEach(step
=> {
429 const dirCode
= Object
.keys(V
.LANCER_DIRS
).find(k
=> {
430 return (V
.LANCER_DIRS
[k
][0] == step
[0] && V
.LANCER_DIRS
[k
][1] == step
[1]);
432 Array
.prototype.push
.apply(
434 this.getPotentialLancerMoves_aux([x
, y
], step
, { p: dirCode
, c: color
})
440 // I wasn't pushed: standard lancer move
441 const dirCode
= this.board
[x
][y
][1];
443 this.getPotentialLancerMoves_aux([x
, y
], V
.LANCER_DIRS
[dirCode
]);
444 // Add all possible orientations aftermove except if I'm being pushed
445 if (this.subTurn
== 1) {
446 monodirMoves
.forEach(m
=> {
447 Object
.keys(V
.LANCER_DIRS
).forEach(k
=> {
448 let mk
= JSON
.parse(JSON
.stringify(m
));
454 } else return monodirMoves
;
457 getPotentialSentryMoves([x
, y
]) {
458 // The sentry moves a priori like a bishop:
459 let moves
= super.getPotentialBishopMoves([x
, y
]);
460 // ...but captures are replaced by special move, if and only if
461 // "captured" piece can move now, considered as the capturer unit.
462 // --> except is subTurn == 2, in this case I don't push anything.
463 if (this.subTurn
== 2) return moves
.filter(m
=> m
.vanish
.length
== 1);
465 if (m
.vanish
.length
== 2) {
466 // Temporarily cancel the sentry capture:
471 // Can the pushed unit make any move? ...resulting in a non-self-check?
472 const color
= this.getColor(x
, y
);
473 const fMoves
= moves
.filter(m
=> {
475 if (m
.appear
.length
== 0) {
478 let moves2
= this.filterValid(
479 this.getPotentialMovesFrom([m
.end
.x
, m
.end
.y
]));
480 for (let m2
of moves2
) {
482 res
= !this.underCheck(color
);
494 getPotentialJailerMoves([x
, y
]) {
495 return super.getPotentialRookMoves([x
, y
]).filter(m
=> {
496 // Remove jailer captures
497 return m
.vanish
[0].p
!= V
.JAILER
|| m
.vanish
.length
== 1;
501 getPotentialKingMoves([x
, y
]) {
502 let moves
= super.getPotentialKingMoves([x
, y
]);
503 // Augment with pass move is the king is immobilized:
504 const jsq
= this.isImmobilized([x
, y
]);
510 start: { x: x
, y: y
},
511 end: { x: jsq
[0], y: jsq
[1] }
518 // Adapted: castle with jailer possible
519 getCastleMoves([x
, y
]) {
520 const c
= this.getColor(x
, y
);
521 const firstRank
= (c
== "w" ? V
.size
.x
- 1 : 0);
522 if (x
!= firstRank
|| y
!= this.INIT_COL_KING
[c
])
525 const oppCol
= V
.GetOppCol(c
);
528 // King, then rook or jailer:
529 const finalSquares
= [
531 [V
.size
.y
- 2, V
.size
.y
- 3]
538 if (!this.castleFlags
[c
][castleSide
]) continue;
539 // Rook (or jailer) and king are on initial position
541 const finDist
= finalSquares
[castleSide
][0] - y
;
542 let step
= finDist
/ Math
.max(1, Math
.abs(finDist
));
546 this.isAttacked([x
, i
], [oppCol
]) ||
547 (this.board
[x
][i
] != V
.EMPTY
&&
548 (this.getColor(x
, i
) != c
||
549 ![V
.KING
, V
.ROOK
].includes(this.getPiece(x
, i
))))
551 continue castlingCheck
;
554 } while (i
!= finalSquares
[castleSide
][0]);
556 step
= castleSide
== 0 ? -1 : 1;
557 const rookOrJailerPos
=
559 ? Math
.min(this.INIT_COL_ROOK
[c
], this.INIT_COL_JAILER
[c
])
560 : Math
.max(this.INIT_COL_ROOK
[c
], this.INIT_COL_JAILER
[c
]);
561 for (i
= y
+ step
; i
!= rookOrJailerPos
; i
+= step
)
562 if (this.board
[x
][i
] != V
.EMPTY
) continue castlingCheck
;
564 // Nothing on final squares, except maybe king and castling rook or jailer?
565 for (i
= 0; i
< 2; i
++) {
567 this.board
[x
][finalSquares
[castleSide
][i
]] != V
.EMPTY
&&
568 this.getPiece(x
, finalSquares
[castleSide
][i
]) != V
.KING
&&
569 finalSquares
[castleSide
][i
] != rookOrJailerPos
571 continue castlingCheck
;
575 // If this code is reached, castle is valid
576 const castlingPiece
= this.getPiece(firstRank
, rookOrJailerPos
);
580 new PiPo({ x: x
, y: finalSquares
[castleSide
][0], p: V
.KING
, c: c
}),
581 new PiPo({ x: x
, y: finalSquares
[castleSide
][1], p: castlingPiece
, c: c
})
584 new PiPo({ x: x
, y: y
, p: V
.KING
, c: c
}),
585 new PiPo({ x: x
, y: rookOrJailerPos
, p: castlingPiece
, c: c
})
588 Math
.abs(y
- rookOrJailerPos
) <= 2
589 ? { x: x
, y: rookOrJailerPos
}
590 : { x: x
, y: y
+ 2 * (castleSide
== 0 ? -1 : 1) }
599 // Disable check tests for sentry pushes,
600 // because in this case the move isn't finished
601 let movesWithoutSentryPushes
= [];
602 let movesWithSentryPushes
= [];
604 if (m
.appear
.length
> 0) movesWithoutSentryPushes
.push(m
);
605 else movesWithSentryPushes
.push(m
);
607 const filteredMoves
= super.filterValid(movesWithoutSentryPushes
);
608 // If at least one full move made, everything is allowed:
609 if (this.movesCount
>= 2)
610 return filteredMoves
.concat(movesWithSentryPushes
);
611 // Else, forbid checks and captures:
612 const oppCol
= V
.GetOppCol(this.turn
);
613 return filteredMoves
.filter(m
=> {
614 if (m
.vanish
.length
== 2 && m
.appear
.length
== 1) return false;
616 const res
= !this.underCheck(oppCol
);
619 }).concat(movesWithSentryPushes
);
623 if (this.subTurn
== 1) return super.getAllValidMoves();
625 const sentrySq
= [this.sentryPos
.x
, this.sentryPos
.y
];
626 return this.filterValid(this.getPotentialMovesFrom(sentrySq
));
629 updateVariables(move) {
631 const piece
= move.vanish
[0].p
;
632 const firstRank
= (c
== "w" ? V
.size
.x
- 1 : 0);
634 // Update king position + flags
635 if (piece
== V
.KING
) {
636 this.kingPos
[c
][0] = move.appear
[0].x
;
637 this.kingPos
[c
][1] = move.appear
[0].y
;
638 this.castleFlags
[c
] = [false, false];
642 // Update castling flags if rook or jailer moved (or is captured)
643 const oppCol
= V
.GetOppCol(c
);
644 const oppFirstRank
= V
.size
.x
- 1 - firstRank
;
648 move.start
.x
== firstRank
&&
649 this.INIT_COL_ROOK
[c
] == move.start
.y
651 if (this.INIT_COL_ROOK
[c
] > this.INIT_COL_JAILER
[c
]) flagIdx
++;
652 this.castleFlags
[c
][flagIdx
] = false;
655 move.start
.x
== firstRank
&&
656 this.INIT_COL_JAILER
[c
] == move.start
.y
658 if (this.INIT_COL_JAILER
[c
] > this.INIT_COL_ROOK
[c
]) flagIdx
++;
659 this.castleFlags
[c
][flagIdx
] = false;
661 // We took opponent's rook?
662 move.end
.x
== oppFirstRank
&&
663 this.INIT_COL_ROOK
[oppCol
] == move.end
.y
665 if (this.INIT_COL_ROOK
[oppCol
] > this.INIT_COL_JAILER
[oppCol
]) flagIdx
++;
666 this.castleFlags
[oppCol
][flagIdx
] = false;
668 // We took opponent's jailer?
669 move.end
.x
== oppFirstRank
&&
670 this.INIT_COL_JAILER
[oppCol
] == move.end
.y
672 if (this.INIT_COL_JAILER
[oppCol
] > this.INIT_COL_ROOK
[oppCol
]) flagIdx
++;
673 this.castleFlags
[oppCol
][flagIdx
] = false;
676 if (move.appear
.length
== 0 && move.vanish
.length
== 1) {
677 // The sentry is about to push a piece: subTurn goes from 1 to 2
678 this.sentryPos
= { x: move.end
.x
, y: move.end
.y
};
679 } else if (this.subTurn
== 2) {
680 // A piece is pushed: forbid array of squares between start and end
681 // of move, included (except if it's a pawn)
683 if (move.vanish
[0].p
!= V
.PAWN
) {
684 if ([V
.KNIGHT
,V
.KING
].includes(move.vanish
[0].p
))
685 // short-range pieces: just forbid initial square
686 squares
.push(move.start
);
688 const deltaX
= move.end
.x
- move.start
.x
;
689 const deltaY
= move.end
.y
- move.start
.y
;
691 deltaX
/ Math
.abs(deltaX
) || 0,
692 deltaY
/ Math
.abs(deltaY
) || 0
695 let sq
= {x: move.start
.x
, y: move.start
.y
};
696 sq
.x
!= move.end
.x
&& sq
.y
!= move.end
.y
;
697 sq
.x
+= step
[0], sq
.y
+= step
[1]
702 // Add end square as well, to know if I was pushed (useful for lancers)
703 squares
.push(move.end
);
705 this.sentryPush
.push(squares
);
706 } else this.sentryPush
.push(null);
709 // TODO: cleaner (global) update/unupdate variables logic, rename...
710 unupdateVariables(move) {
711 super.unupdateVariables(move);
712 this.sentryPush
.pop();
716 move.flags
= JSON
.stringify(this.aggregateFlags());
717 this.epSquares
.push(this.getEpSquare(move));
718 V
.PlayOnBoard(this.board
, move);
719 this.updateVariables(move);
720 // Is it a sentry push? (useful for undo)
721 move.sentryPush
= (this.subTurn
== 2);
722 if (this.subTurn
== 1) this.movesCount
++;
723 if (move.appear
.length
== 0 && move.vanish
.length
== 1) this.subTurn
= 2;
725 // Turn changes only if not a sentry "pre-push"
726 this.turn
= V
.GetOppCol(this.turn
);
732 this.epSquares
.pop();
733 this.disaggregateFlags(JSON
.parse(move.flags
));
734 V
.UndoOnBoard(this.board
, move);
735 const L
= this.sentryPush
.length
;
736 // Decrement movesCount except if the move is a sentry push
737 if (!move.sentryPush
) this.movesCount
--;
738 // Turn changes only if not undoing second part of a sentry push
739 if (!move.sentryPush
|| this.subTurn
== 1)
740 this.turn
= V
.GetOppCol(this.turn
);
741 this.unupdateVariables(move);
744 isAttacked(sq
, colors
) {
746 super.isAttacked(sq
, colors
) ||
747 this.isAttackedByLancer(sq
, colors
) ||
748 this.isAttackedBySentry(sq
, colors
)
752 isAttackedByLancer([x
, y
], colors
) {
753 for (let step
of V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
])) {
754 // If in this direction there are only enemy pieces and empty squares,
755 // and we meet a lancer: can he reach us?
756 // NOTE: do not stop at first lancer, there might be several!
757 let coord
= { x: x
+ step
[0], y: y
+ step
[1] };
760 V
.OnBoard(coord
.x
, coord
.y
) &&
762 this.board
[coord
.x
][coord
.y
] == V
.EMPTY
||
763 colors
.includes(this.getColor(coord
.x
, coord
.y
))
766 lancerPos
.push(coord
);
768 for (let xy
of lancerPos
) {
769 const dir
= V
.LANCER_DIRS
[this.board
[xy
.x
][xy
.y
].charAt(1)];
770 if (dir
[0] == -step
[0] && dir
[1] == -step
[1]) return true;
776 // Helper to check sentries attacks:
777 selfAttack([x1
, y1
], [x2
, y2
]) {
778 const color
= this.getColor(x1
, y1
);
779 const sliderAttack
= (allowedSteps
, lancer
) => {
780 const deltaX
= x2
- x1
;
781 const deltaY
= y2
- y1
;
782 const step
= [ deltaX
/ Math
.abs(deltaX
), deltaY
/ Math
.abs(deltaY
) ];
783 if (allowedStep
.every(st
=> st
[0] != step
[0] || st
[1] != step
[1]))
785 let sq
= [ x1
= step
[0], y1
+ step
[1] ];
786 while (sq
[0] != x2
&& sq
[1] != y2
) {
788 (!lancer
&& this.board
[sq
[0]][sq
[1]] != V
.EMPTY
) ||
789 (!!lancer
&& this.getColor(sq
[0], sq
[1]) != color
)
796 switch (this.getPiece(x1
, y1
)) {
798 // Pushed pawns move as enemy pawns
799 const shift
= (color
== 'w' ? 1 : -1);
800 return (x1
+ shift
== x2
&& Math
.abs(y1
- y2
) == 1);
803 const deltaX
= Math
.abs(x1
- x2
);
804 const deltaY
= Math
.abs(y1
- y2
);
806 deltaX
+ deltaY
== 3 &&
807 [1, 2].includes(deltaX
) &&
808 [1, 2].includes(deltaY
)
812 return sliderAttack(V
.steps
[V
.ROOK
]);
814 return sliderAttack(V
.steps
[V
.BISHOP
]);
816 return sliderAttack(V
.steps
[V
.ROOK
].concat(V
.steps
[V
.BISHOP
]));
818 // Special case: as long as no enemy units stands in-between, it attacks
819 // (if it points toward the king).
820 const allowedStep
= V
.LANCER_DIRS
[this.board
[x1
][y1
].charAt(1)];
821 return sliderAttack([allowedStep
], "lancer");
823 // No sentries or jailer tests: they cannot self-capture
828 isAttackedBySentry([x
, y
], colors
) {
829 // Attacked by sentry means it can self-take our king.
830 // Just check diagonals of enemy sentry(ies), and if it reaches
831 // one of our pieces: can I self-take?
832 const color
= V
.GetOppCol(colors
[0]);
834 for (let i
=0; i
<V
.size
.x
; i
++) {
835 for (let j
=0; j
<V
.size
.y
; j
++) {
837 this.getPiece(i
,j
) == V
.SENTRY
&&
838 colors
.includes(this.getColor(i
,j
))
840 for (let step
of V
.steps
[V
.BISHOP
]) {
841 let sq
= [ i
+ step
[0], j
+ step
[1] ];
843 V
.OnBoard(sq
[0], sq
[1]) &&
844 this.board
[sq
[0]][sq
[1]] == V
.EMPTY
850 V
.OnBoard(sq
[0], sq
[1]) &&
851 this.getColor(sq
[0], sq
[1]) == color
859 for (let c
of candidates
)
860 if (this.selfAttack(c
, [x
, y
])) return true;
864 // Jailer doesn't capture or give check
866 static get VALUES() {
867 return Object
.assign(
868 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
874 const maxeval
= V
.INFINITY
;
875 const color
= this.turn
;
876 let moves1
= this.getAllValidMoves();
878 if (moves1
.length
== 0)
879 // TODO: this situation should not happen
882 const setEval
= (move) => {
883 const score
= this.getCurrentScore();
888 : (score
== "1-0" ? 1 : -1) * maxeval
;
889 } else move[i
].eval
= this.evalPosition();
892 // Just search_depth == 1 (because of sentries. TODO: can do better...)
893 moves1
.forEach(m1
=> {
895 if (this.subTurn
== 1) setEval(m1
);
897 // Need to play every pushes and count:
898 const moves2
= this.getAllValidMoves();
899 moves2
.forEach(m2
=> {
908 moves1
.sort((a
, b
) => {
909 return (color
== "w" ? 1 : -1) * (b
.eval
- a
.eval
);
911 let candidates
= [0];
912 for (let j
= 1; j
< moves1
.length
&& moves1
[j
].eval
== moves1
[0].eval
; j
++)
914 return moves1
[candidates
[randInt(candidates
.length
)]];
918 // Special case "king takes jailer" is a pass move
919 if (move.appear
.length
== 0 && move.vanish
.length
== 0) return "pass";
920 return super.getNotation(move);