Attempt to fix Eightpieces
[vchess.git] / client / src / variants / Eightpieces.js
CommitLineData
abbda16d 1import { randInt, sample } from "@/utils/alea";
2a8a94c9
BA
2import { ChessRules, PiPo, Move } from "@/base_rules";
3
32f6285e 4export class EightpiecesRules extends ChessRules {
7e8a7ea1 5
2a8a94c9
BA
6 static get JAILER() {
7 return "j";
8 }
9 static get SENTRY() {
10 return "s";
11 }
12 static get LANCER() {
13 return "l";
14 }
15
14edde72
BA
16 static get IMAGE_EXTENSION() {
17 // Temporarily, for the time SVG pieces are being designed:
18 return ".png";
19 }
20
ee307044 21 // Lancer directions *from white perspective*
28b32b4f
BA
22 static get LANCER_DIRS() {
23 return {
24 'c': [-1, 0], //north
25 'd': [-1, 1], //N-E
26 'e': [0, 1], //east
27 'f': [1, 1], //S-E
28 'g': [1, 0], //south
29 'h': [1, -1], //S-W
30 'm': [0, -1], //west
31 'o': [-1, -1] //N-W
32 };
33 }
34
3a2a7b5f
BA
35 static get PIECES() {
36 return ChessRules.PIECES
37 .concat([V.JAILER, V.SENTRY])
38 .concat(Object.keys(V.LANCER_DIRS));
39 }
40
28b32b4f
BA
41 getPiece(i, j) {
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;
45 return piece;
46 }
47
3a2a7b5f 48 getPpath(b, color, score, orientation) {
14edde72 49 if ([V.JAILER, V.SENTRY].includes(b[1])) return "Eightpieces/tmp_png/" + b;
3a2a7b5f 50 if (Object.keys(V.LANCER_DIRS).includes(b[1])) {
14edde72 51 if (orientation == 'w') return "Eightpieces/tmp_png/" + b;
3a2a7b5f
BA
52 // Find opposite direction for adequate display:
53 let oppDir = '';
54 switch (b[1]) {
55 case 'c':
56 oppDir = 'g';
57 break;
58 case 'g':
59 oppDir = 'c';
60 break;
61 case 'd':
62 oppDir = 'h';
63 break;
64 case 'h':
65 oppDir = 'd';
66 break;
67 case 'e':
68 oppDir = 'm';
69 break;
70 case 'm':
71 oppDir = 'e';
72 break;
73 case 'f':
74 oppDir = 'o';
75 break;
76 case 'o':
77 oppDir = 'f';
78 break;
79 }
14edde72 80 return "Eightpieces/tmp_png/" + b[0] + oppDir;
3a2a7b5f 81 }
14edde72
BA
82 // TODO: after we have SVG pieces, remove the folder and next prefix:
83 return "Eightpieces/tmp_png/" + b;
90e814b6
BA
84 }
85
c7550017
BA
86 getPPpath(m, orientation) {
87 return (
88 this.getPpath(
89 m.appear[0].c + m.appear[0].p,
90 null,
91 null,
92 orientation
93 )
94 );
3a2a7b5f
BA
95 }
96
90e814b6
BA
97 static ParseFen(fen) {
98 const fenParts = fen.split(" ");
3a2a7b5f
BA
99 return Object.assign(
100 ChessRules.ParseFen(fen),
101 { sentrypush: fenParts[5] }
102 );
103 }
104
105 static IsGoodFen(fen) {
106 if (!ChessRules.IsGoodFen(fen)) return false;
107 const fenParsed = V.ParseFen(fen);
108 // 5) Check sentry push (if any)
109 if (
110 fenParsed.sentrypush != "-" &&
61656127 111 !fenParsed.sentrypush.match(/^([a-h][1-8]){2,2}$/)
3a2a7b5f
BA
112 ) {
113 return false;
114 }
115 return true;
90e814b6
BA
116 }
117
118 getFen() {
6b7b2cf7 119 return super.getFen() + " " + this.getSentrypushFen();
90e814b6
BA
120 }
121
122 getFenForRepeat() {
6b7b2cf7 123 return super.getFenForRepeat() + "_" + this.getSentrypushFen();
90e814b6
BA
124 }
125
6b7b2cf7
BA
126 getSentrypushFen() {
127 const L = this.sentryPush.length;
128 if (!this.sentryPush[L-1]) return "-";
90e814b6 129 let res = "";
0a9cef13
BA
130 const spL = this.sentryPush[L-1].length;
131 // Condensate path: just need initial and final squares:
132 return [0, spL - 1]
133 .map(i => V.CoordsToSquare(this.sentryPush[L-1][i]))
61656127 134 .join("");
2a8a94c9
BA
135 }
136
28b32b4f
BA
137 setOtherVariables(fen) {
138 super.setOtherVariables(fen);
139 // subTurn == 2 only when a sentry moved, and is about to push something
140 this.subTurn = 1;
afbf3ca7
BA
141 // Sentry position just after a "capture" (subTurn from 1 to 2)
142 this.sentryPos = null;
28b32b4f 143 // Stack pieces' forbidden squares after a sentry move at each turn
90e814b6 144 const parsedFen = V.ParseFen(fen);
6b7b2cf7 145 if (parsedFen.sentrypush == "-") this.sentryPush = [null];
90e814b6 146 else {
0a9cef13 147 // Expand init + dest squares into a full path:
61656127
BA
148 const init = V.SquareToCoords(parsedFen.sentrypush.substr(0, 2)),
149 dest = V.SquareToCoords(parsedFen.sentrypush.substr(2));
0a9cef13
BA
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
156 });
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 });
161 x += step[0];
162 y += step[1];
163 }
164 }
165 newPath.push(dest);
166 this.sentryPush = [newPath];
90e814b6 167 }
2a8a94c9
BA
168 }
169
4313762d
BA
170 static GenRandInitFen(options) {
171 if (options.randomness == 0)
d54f6261 172 return "jfsqkbnr/pppppppp/8/8/8/8/PPPPPPPP/JDSQKBNR w 0 ahah - -";
9842aca2 173
4313762d 174 const baseFen = ChessRules.GenRandInitFen(options);
0fb43db7
BA
175 const fenParts = baseFen.split(' ');
176 const posParts = fenParts[0].split('/');
9842aca2 177
0fb43db7
BA
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)
abbda16d
BA
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 ['w', 'b'].forEach(c => {
185 ['r', 'n', 'b'].forEach(p => {
186 let pl = pieceLine[c];
187 let pos = -1;
188 if (options.randomness == 2 || c == 'b')
189 pos = (randInt(2) == 0 ? pl.indexOf(p) : pl.lastIndexOf(p));
190 else pos = posBlack[p];
191 pieceLine[c] =
192 pieceLine[c].substr(0, pos) + mapP[p] + pieceLine[c].substr(pos+1);
193 if (options.randomness == 1 && c == 'b') posBlack[p] = pos;
194 });
195 });
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
208 .replace('s', 'b');
209 .replace('t', 's');
9842aca2 210 }
9842aca2 211 }
0fb43db7 212
9842aca2 213 return (
abbda16d
BA
214 pieceLine['b'] + "/" +
215 posParts.slice(1, 7).join('/') + "/" +
216 pieceLine['w'].toUpperCase() + " " +
217 fenParts.slice(1, 5).join(' ') + " -"
9842aca2 218 );
2a8a94c9
BA
219 }
220
b0a0468a
BA
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]);
226 }
227
6b7b2cf7
BA
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]];
234 if (
235 V.OnBoard(i, j) &&
236 this.board[i][j] != V.EMPTY &&
237 this.getColor(i, j) == oppCol
238 ) {
bfed878d 239 if (this.getPiece(i, j) == V.JAILER) return [i, j];
6b7b2cf7
BA
240 }
241 }
242 return null;
243 }
244
bfed878d
BA
245 canIplay(side, [x, y]) {
246 return (
2c5d7b20
BA
247 (this.subTurn == 1 && this.turn == side && this.getColor(x, y) == side)
248 ||
bfed878d
BA
249 (this.subTurn == 2 && x == this.sentryPos.x && y == this.sentryPos.y)
250 );
251 }
252
3a2a7b5f 253 getPotentialMovesFrom([x, y]) {
d9a7a1e4
BA
254 const piece = this.getPiece(x, y);
255 const L = this.sentryPush.length;
1b56b736 256 // At subTurn == 2, jailers aren't effective (Jeff K)
3a2a7b5f
BA
257 if (this.subTurn == 1) {
258 const jsq = this.isImmobilized([x, y]);
259 if (!!jsq) {
260 let moves = [];
261 // Special pass move if king:
d9a7a1e4 262 if (piece == V.KING) {
3a2a7b5f
BA
263 moves.push(
264 new Move({
265 appear: [],
266 vanish: [],
267 start: { x: x, y: y },
268 end: { x: jsq[0], y: jsq[1] }
269 })
270 );
271 }
d9a7a1e4
BA
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 => {
277 moves.push(
278 new Move({
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] }
283 })
284 );
285 });
286 }
3a2a7b5f
BA
287 return moves;
288 }
289 }
bfed878d 290 let moves = [];
d9a7a1e4 291 switch (piece) {
6b7b2cf7 292 case V.JAILER:
bfed878d
BA
293 moves = this.getPotentialJailerMoves([x, y]);
294 break;
6b7b2cf7 295 case V.SENTRY:
bfed878d
BA
296 moves = this.getPotentialSentryMoves([x, y]);
297 break;
13102cab 298 case V.LANCER:
bfed878d
BA
299 moves = this.getPotentialLancerMoves([x, y]);
300 break;
6b7b2cf7 301 default:
bfed878d
BA
302 moves = super.getPotentialMovesFrom([x, y]);
303 break;
6b7b2cf7 304 }
bfed878d 305 if (!!this.sentryPush[L-1]) {
d9a7a1e4
BA
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];
bfed878d
BA
310 moves = moves.filter(m => {
311 if (
312 m.vanish[0].p != V.PAWN &&
d9a7a1e4 313 m.start.x == finalPushedSq.x && m.start.y == finalPushedSq.y &&
bfed878d
BA
314 this.sentryPush[L-1].some(sq => sq.x == m.end.x && sq.y == m.end.y)
315 ) {
316 return false;
317 }
318 return true;
319 });
0fb43db7
BA
320 }
321 else if (this.subTurn == 2) {
b0a0468a
BA
322 // Put back the sentinel on board:
323 const color = this.turn;
bfed878d 324 moves.forEach(m => {
b0a0468a 325 m.appear.push({x: x, y: y, p: V.SENTRY, c: color});
bfed878d
BA
326 });
327 }
328 return moves;
6b7b2cf7
BA
329 }
330
ee307044 331 getPotentialPawnMoves([x, y]) {
bfed878d 332 const color = this.getColor(x, y);
ee307044
BA
333 let moves = [];
334 const [sizeX, sizeY] = [V.size.x, V.size.y];
89781a55 335 let shiftX = (color == "w" ? -1 : 1);
b0a0468a 336 if (this.subTurn == 2) shiftX *= -1;
89781a55 337 const firstRank = color == "w" ? sizeX - 1 : 0;
ee307044
BA
338 const startRank = color == "w" ? sizeX - 2 : 1;
339 const lastRank = color == "w" ? 0 : sizeX - 1;
340
b0a0468a
BA
341 // Pawns might be pushed on 1st rank and attempt to move again:
342 if (!V.OnBoard(x + shiftX, y)) return [];
343
7f1df0d9
BA
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 =
349 lastRank == 0
350 ? ['e', 'f', 'g', 'h', 'm']
351 : ['c', 'd', 'e', 'm', 'o'];
352 finalPieces =
353 allowedLancerDirs
354 .concat([V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.SENTRY, V.JAILER]);
355 }
ee307044
BA
356 if (this.board[x + shiftX][y] == V.EMPTY) {
357 // One square forward
358 for (let piece of finalPieces) {
359 moves.push(
360 this.getBasicMove([x, y], [x + shiftX, y], {
361 c: color,
362 p: piece
363 })
364 );
365 }
366 if (
89781a55
BA
367 // 2-squares jumps forbidden if pawn push
368 this.subTurn == 1 &&
369 [startRank, firstRank].includes(x) &&
ee307044
BA
370 this.board[x + 2 * shiftX][y] == V.EMPTY
371 ) {
372 // Two squares jump
373 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
374 }
375 }
376 // Captures
377 for (let shiftY of [-1, 1]) {
378 if (
379 y + shiftY >= 0 &&
380 y + shiftY < sizeY &&
381 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
382 this.canTake([x, y], [x + shiftX, y + shiftY])
383 ) {
384 for (let piece of finalPieces) {
385 moves.push(
386 this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
387 c: color,
388 p: piece
389 })
390 );
6b7b2cf7 391 }
ee307044
BA
392 }
393 }
394
89781a55 395 // En passant: only on subTurn == 1
ee307044 396 const Lep = this.epSquares.length;
3a2a7b5f 397 const epSquare = this.epSquares[Lep - 1];
ee307044 398 if (
89781a55 399 this.subTurn == 1 &&
ee307044
BA
400 !!epSquare &&
401 epSquare.x == x + shiftX &&
402 Math.abs(epSquare.y - y) == 1
403 ) {
404 let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
405 enpassantMove.vanish.push({
406 x: x,
407 y: epSquare.y,
408 p: "p",
409 c: this.getColor(x, epSquare.y)
6b7b2cf7 410 });
ee307044 411 moves.push(enpassantMove);
6b7b2cf7 412 }
6b7b2cf7 413
ee307044 414 return moves;
9842aca2
BA
415 }
416
1b56b736
BA
417 doClick(square) {
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;
422 if (
423 this.subTurn == 2 ||
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]
428 ) {
429 return null;
430 }
431 // Stuck lancer?
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])) {
435 let choices = [];
436 Object.keys(V.LANCER_DIRS).forEach(k => {
437 const dir = V.LANCER_DIRS[k];
438 if (
439 (dir[0] != step[0] || dir[1] != step[1]) &&
440 V.OnBoard(x + dir[0], y + dir[1])
441 ) {
442 choices.push(
443 new Move({
444 vanish: [
445 new PiPo({
446 x: x,
447 y: y,
448 c: color,
449 p: orientation
450 })
451 ],
452 appear: [
453 new PiPo({
454 x: x,
455 y: y,
456 c: color,
457 p: k
458 })
459 ],
460 start: { x: x, y : y },
461 end: { x: -1, y: -1 }
462 })
463 );
464 }
465 });
466 return choices;
467 }
468 return null;
469 }
470
3a2a7b5f 471 // Obtain all lancer moves in "step" direction
afbf3ca7 472 getPotentialLancerMoves_aux([x, y], step, tr) {
9842aca2
BA
473 let moves = [];
474 // Add all moves to vacant squares until opponent is met:
b0a0468a
BA
475 const color = this.getColor(x, y);
476 const oppCol =
477 this.subTurn == 1
478 ? V.GetOppCol(color)
479 // at subTurn == 2, consider own pieces as opponent
480 : color;
9842aca2
BA
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)
afbf3ca7 484 moves.push(this.getBasicMove([x, y], sq, tr));
9842aca2
BA
485 sq[0] += step[0];
486 sq[1] += step[1];
487 }
488 if (V.OnBoard(sq[0], sq[1]))
489 // Add capturing move
afbf3ca7 490 moves.push(this.getBasicMove([x, y], sq, tr));
9842aca2 491 return moves;
6b7b2cf7
BA
492 }
493
494 getPotentialLancerMoves([x, y]) {
9842aca2
BA
495 let moves = [];
496 // Add all lancer possible orientations, similar to pawn promotions.
497 // Except if just after a push: allow all movements from init square then
ee307044 498 const L = this.sentryPush.length;
89781a55 499 const color = this.getColor(x, y);
1b56b736 500 const dirCode = this.board[x][y][1];
737a5daf 501 const curDir = V.LANCER_DIRS[dirCode];
ee307044 502 if (!!this.sentryPush[L-1]) {
9842aca2 503 // Maybe I was pushed
ee307044 504 const pl = this.sentryPush[L-1].length;
9842aca2 505 if (
ee307044
BA
506 this.sentryPush[L-1][pl-1].x == x &&
507 this.sentryPush[L-1][pl-1].y == y
9842aca2
BA
508 ) {
509 // I was pushed: allow all directions (for this move only), but
3a2a7b5f
BA
510 // do not change direction after moving, *except* if I keep the
511 // same orientation in which I was pushed.
1b56b736
BA
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];
518 if (
519 (dir[0] != curDir[0] || dir[1] != curDir[1]) &&
520 V.OnBoard(x + dir[0], y + dir[1])
521 ) {
522 reorientMoves.push(
523 new Move({
524 vanish: [
525 new PiPo({
526 x: x,
527 y: y,
528 c: color,
529 p: dirCode
530 })
531 ],
532 appear: [
533 new PiPo({
534 x: x,
535 y: y,
536 c: color,
537 p: k
538 })
539 ],
540 start: { x: x, y : y },
541 end: { x: kp[0], y: kp[1] }
542 })
543 );
544 }
545 });
546 Array.prototype.push.apply(moves, reorientMoves);
547 }
9842aca2 548 Object.values(V.LANCER_DIRS).forEach(step => {
afbf3ca7 549 const dirCode = Object.keys(V.LANCER_DIRS).find(k => {
3a2a7b5f
BA
550 return (
551 V.LANCER_DIRS[k][0] == step[0] &&
552 V.LANCER_DIRS[k][1] == step[1]
553 );
afbf3ca7 554 });
3a2a7b5f
BA
555 const dirMoves =
556 this.getPotentialLancerMoves_aux(
557 [x, y],
558 step,
559 { p: dirCode, c: color }
560 );
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 => {
33974019
BA
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));
570 mk.appear[0].p = k;
571 chooseMoves.push(mk);
572 }
3a2a7b5f
BA
573 });
574 });
575 Array.prototype.push.apply(moves, chooseMoves);
33974019
BA
576 }
577 else Array.prototype.push.apply(moves, dirMoves);
9842aca2
BA
578 });
579 return moves;
580 }
581 }
582 // I wasn't pushed: standard lancer move
9842aca2
BA
583 const monodirMoves =
584 this.getPotentialLancerMoves_aux([x, y], V.LANCER_DIRS[dirCode]);
bfed878d
BA
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 => {
33974019
BA
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));
593 mk.appear[0].p = k;
594 moves.push(mk);
595 }
bfed878d 596 });
9842aca2 597 });
bfed878d 598 return moves;
33974019
BA
599 }
600 else {
737a5daf 601 // I'm pushed: add potential nudges, except for current orientation
3a2a7b5f
BA
602 let potentialNudges = [];
603 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
604 if (
737a5daf 605 (step[0] != curDir[0] || step[1] != curDir[1]) &&
3a2a7b5f
BA
606 V.OnBoard(x + step[0], y + step[1]) &&
607 this.board[x + step[0]][y + step[1]] == V.EMPTY
608 ) {
89781a55
BA
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]);
612 });
3a2a7b5f
BA
613 potentialNudges.push(
614 this.getBasicMove(
615 [x, y],
89781a55
BA
616 [x + step[0], y + step[1]],
617 { c: color, p: newDirCode }
3a2a7b5f
BA
618 )
619 );
620 }
621 }
622 return monodirMoves.concat(potentialNudges);
623 }
6b7b2cf7
BA
624 }
625
626 getPotentialSentryMoves([x, y]) {
627 // The sentry moves a priori like a bishop:
628 let moves = super.getPotentialBishopMoves([x, y]);
ee307044
BA
629 // ...but captures are replaced by special move, if and only if
630 // "captured" piece can move now, considered as the capturer unit.
afbf3ca7
BA
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);
9842aca2
BA
633 moves.forEach(m => {
634 if (m.vanish.length == 2) {
635 // Temporarily cancel the sentry capture:
636 m.appear.pop();
637 m.vanish.pop();
638 }
639 });
bfed878d 640 const color = this.getColor(x, y);
ee307044 641 const fMoves = moves.filter(m => {
b0a0468a 642 // Can the pushed unit make any move? ...resulting in a non-self-check?
bfed878d
BA
643 if (m.appear.length == 0) {
644 let res = false;
645 this.play(m);
b0a0468a 646 let moves2 = this.getPotentialMovesFrom([m.end.x, m.end.y]);
bfed878d
BA
647 for (let m2 of moves2) {
648 this.play(m2);
649 res = !this.underCheck(color);
650 this.undo(m2);
651 if (res) break;
652 }
653 this.undo(m);
654 return res;
655 }
656 return true;
ee307044 657 });
ee307044 658 return fMoves;
6b7b2cf7
BA
659 }
660
661 getPotentialJailerMoves([x, y]) {
ee307044
BA
662 return super.getPotentialRookMoves([x, y]).filter(m => {
663 // Remove jailer captures
664 return m.vanish[0].p != V.JAILER || m.vanish.length == 1;
665 });
6b7b2cf7
BA
666 }
667
b0a0468a
BA
668 getPotentialKingMoves(sq) {
669 const moves = this.getSlideNJumpMoves(
4313762d 670 sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]), 1);
b0a0468a
BA
671 return (
672 this.subTurn == 1
673 ? moves.concat(this.getCastleMoves(sq))
674 : moves
675 );
676 }
677
3a2a7b5f
BA
678 atLeastOneMove() {
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();
682 }
683
ee307044 684 filterValid(moves) {
b0a0468a
BA
685 if (moves.length == 0) return [];
686 const basicFilter = (m, c) => {
687 this.play(m);
688 const res = !this.underCheck(c);
689 this.undo(m);
690 return res;
691 };
bfed878d
BA
692 // Disable check tests for sentry pushes,
693 // because in this case the move isn't finished
694 let movesWithoutSentryPushes = [];
695 let movesWithSentryPushes = [];
696 moves.forEach(m => {
b0a0468a
BA
697 // Second condition below for special king "pass" moves
698 if (m.appear.length > 0 || m.vanish.length == 0)
699 movesWithoutSentryPushes.push(m);
bfed878d
BA
700 else movesWithSentryPushes.push(m);
701 });
b0a0468a
BA
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.
708 return (
709 this.movesCount >= 2
710 ? filteredMoves
711 : filteredMoves.filter(m => {
d958cc68 712 return (m.vanish.length <= 1 && basicFilter(m, oppCol));
b0a0468a
BA
713 })
714 ).concat(movesWithSentryPushes);
bfed878d
BA
715 }
716
717 getAllValidMoves() {
718 if (this.subTurn == 1) return super.getAllValidMoves();
719 // Sentry push:
afbf3ca7 720 const sentrySq = [this.sentryPos.x, this.sentryPos.y];
bfed878d 721 return this.filterValid(this.getPotentialMovesFrom(sentrySq));
ee307044
BA
722 }
723
68e19a44 724 isAttacked(sq, color) {
afbf3ca7 725 return (
68e19a44
BA
726 super.isAttacked(sq, color) ||
727 this.isAttackedByLancer(sq, color) ||
728 this.isAttackedBySentry(sq, color)
9dca2c93 729 // The jailer doesn't capture.
afbf3ca7
BA
730 );
731 }
732
68e19a44 733 isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
b0a0468a
BA
734 for (let step of steps) {
735 let rx = x + step[0],
736 ry = y + step[1];
737 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
738 rx += step[0];
739 ry += step[1];
740 }
741 if (
742 V.OnBoard(rx, ry) &&
68e19a44
BA
743 this.getPiece(rx, ry) == piece &&
744 this.getColor(rx, ry) == color &&
b0a0468a
BA
745 !this.isImmobilized([rx, ry])
746 ) {
747 return true;
748 }
749 }
750 return false;
751 }
752
68e19a44
BA
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]) {
757 if (
758 y + i >= 0 &&
759 y + i < V.size.y &&
760 this.getPiece(x + pawnShift, y + i) == V.PAWN &&
761 this.getColor(x + pawnShift, y + i) == color &&
762 !this.isImmobilized([x + pawnShift, y + i])
763 ) {
764 return true;
b0a0468a
BA
765 }
766 }
767 }
768 return false;
769 }
770
68e19a44 771 isAttackedByLancer([x, y], color) {
afbf3ca7
BA
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] };
777 let lancerPos = [];
778 while (
779 V.OnBoard(coord.x, coord.y) &&
780 (
781 this.board[coord.x][coord.y] == V.EMPTY ||
68e19a44 782 this.getColor(coord.x, coord.y) == color
afbf3ca7
BA
783 )
784 ) {
b0a0468a
BA
785 if (
786 this.getPiece(coord.x, coord.y) == V.LANCER &&
787 !this.isImmobilized([coord.x, coord.y])
788 ) {
3a2a7b5f 789 lancerPos.push({x: coord.x, y: coord.y});
b0a0468a 790 }
3a2a7b5f
BA
791 coord.x += step[0];
792 coord.y += step[1];
afbf3ca7 793 }
737a5daf
BA
794 const L = this.sentryPush.length;
795 const pl = (!!this.sentryPush[L-1] ? this.sentryPush[L-1].length : 0);
afbf3ca7
BA
796 for (let xy of lancerPos) {
797 const dir = V.LANCER_DIRS[this.board[xy.x][xy.y].charAt(1)];
737a5daf
BA
798 if (
799 (dir[0] == -step[0] && dir[1] == -step[1]) ||
800 // If the lancer was just pushed, this is an attack too:
801 (
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
805 )
806 ) {
807 return true;
808 }
afbf3ca7
BA
809 }
810 }
811 return false;
812 }
813
814 // Helper to check sentries attacks:
815 selfAttack([x1, y1], [x2, y2]) {
816 const color = this.getColor(x1, y1);
9dca2c93 817 const oppCol = V.GetOppCol(color);
afbf3ca7 818 const sliderAttack = (allowedSteps, lancer) => {
89781a55 819 const deltaX = x2 - x1,
9b6405f5
BA
820 deltaY = y2 - y1;
821 const absDeltaX = Math.abs(deltaX),
89781a55 822 absDeltaY = Math.abs(deltaY);
518a0dc9 823 const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ];
89781a55
BA
824 if (
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])
828 ) {
afbf3ca7 829 return false;
89781a55 830 }
3a2a7b5f 831 let sq = [ x1 + step[0], y1 + step[1] ];
f14572c4 832 while (sq[0] != x2 || sq[1] != y2) {
9b6405f5
BA
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]);
837 if (
838 // Enemy sentry on the way will be gone:
839 (p != V.SENTRY || pc != oppCol) &&
840 // Lancer temporarily "changed color":
841 (!lancer || pc == color)
842 ) {
843 return false;
844 }
afbf3ca7 845 }
3a2a7b5f
BA
846 sq[0] += step[0];
847 sq[1] += step[1];
afbf3ca7
BA
848 }
849 return true;
850 };
851 switch (this.getPiece(x1, y1)) {
852 case V.PAWN: {
853 // Pushed pawns move as enemy pawns
854 const shift = (color == 'w' ? 1 : -1);
855 return (x1 + shift == x2 && Math.abs(y1 - y2) == 1);
856 }
857 case V.KNIGHT: {
858 const deltaX = Math.abs(x1 - x2);
859 const deltaY = Math.abs(y1 - y2);
860 return (
861 deltaX + deltaY == 3 &&
862 [1, 2].includes(deltaX) &&
863 [1, 2].includes(deltaY)
864 );
865 }
866 case V.ROOK:
867 return sliderAttack(V.steps[V.ROOK]);
868 case V.BISHOP:
869 return sliderAttack(V.steps[V.BISHOP]);
870 case V.QUEEN:
871 return sliderAttack(V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
872 case V.LANCER: {
2c5d7b20
BA
873 // Special case: as long as no enemy units stands in-between,
874 // it attacks (if it points toward the king).
afbf3ca7
BA
875 const allowedStep = V.LANCER_DIRS[this.board[x1][y1].charAt(1)];
876 return sliderAttack([allowedStep], "lancer");
877 }
878 // No sentries or jailer tests: they cannot self-capture
879 }
880 return false;
881 }
882
68e19a44 883 isAttackedBySentry([x, y], color) {
afbf3ca7
BA
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?
68e19a44 887 const myColor = V.GetOppCol(color);
afbf3ca7
BA
888 let candidates = [];
889 for (let i=0; i<V.size.x; i++) {
890 for (let j=0; j<V.size.y; j++) {
891 if (
892 this.getPiece(i,j) == V.SENTRY &&
68e19a44 893 this.getColor(i,j) == color &&
b0a0468a 894 !this.isImmobilized([i, j])
afbf3ca7
BA
895 ) {
896 for (let step of V.steps[V.BISHOP]) {
897 let sq = [ i + step[0], j + step[1] ];
898 while (
899 V.OnBoard(sq[0], sq[1]) &&
900 this.board[sq[0]][sq[1]] == V.EMPTY
901 ) {
902 sq[0] += step[0];
903 sq[1] += step[1];
904 }
905 if (
906 V.OnBoard(sq[0], sq[1]) &&
68e19a44 907 this.getColor(sq[0], sq[1]) == myColor
afbf3ca7 908 ) {
3a2a7b5f 909 candidates.push([ sq[0], sq[1] ]);
afbf3ca7
BA
910 }
911 }
912 }
913 }
914 }
915 for (let c of candidates)
916 if (this.selfAttack(c, [x, y])) return true;
917 return false;
918 }
919
920 // Jailer doesn't capture or give check
921
d54f6261
BA
922 prePlay(move) {
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)
929 let squares = [];
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 });
933 else {
934 const deltaX = move.end.x - move.start.x;
935 const deltaY = move.end.y - move.start.y;
936 const step = [
937 deltaX / Math.abs(deltaX) || 0,
938 deltaY / Math.abs(deltaY) || 0
939 ];
940 for (
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]
944 ) {
945 squares.push({ x: sq.x, y: sq.y });
946 }
947 }
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);
952 }
953
954 play(move) {
955 this.prePlay(move);
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;
963 else {
964 // Turn changes only if not a sentry "pre-push"
965 this.turn = V.GetOppCol(this.turn);
966 this.subTurn = 1;
967 }
968 this.postPlay(move);
969 }
970
971 postPlay(move) {
972 if (move.vanish.length == 0 || this.subTurn == 2)
973 // Special pass move of the king, or sentry pre-push: nothing to update
974 return;
975 const c = move.vanish[0].c;
976 const piece = move.vanish[0].p;
977 const firstRank = c == "w" ? V.size.x - 1 : 0;
978
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];
983 return;
984 }
985 // Update castling flags if rooks are moved
986 const oppCol = V.GetOppCol(c);
987 const oppFirstRank = V.size.x - 1 - firstRank;
988 if (
989 move.start.x == firstRank && //our rook moves?
990 this.castleFlags[c].includes(move.start.y)
991 ) {
992 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
993 this.castleFlags[c][flagIdx] = V.size.y;
994 } else if (
995 move.end.x == oppFirstRank && //we took opponent rook?
996 this.castleFlags[oppCol].includes(move.end.y)
997 ) {
998 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
999 this.castleFlags[oppCol][flagIdx] = V.size.y;
1000 }
1001 }
1002
1003 undo(move) {
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;
1010 else {
1011 this.turn = V.GetOppCol(this.turn);
1012 if (move.sentryPush) this.subTurn = 2;
1013 }
1014 this.postUndo(move);
1015 }
1016
1017 postUndo(move) {
1018 super.postUndo(move);
1019 this.sentryPush.pop();
1020 }
1021
2a8a94c9
BA
1022 static get VALUES() {
1023 return Object.assign(
28b32b4f 1024 { l: 4.8, s: 2.8, j: 3.8 }, //Jeff K. estimations
2a8a94c9
BA
1025 ChessRules.VALUES
1026 );
1027 }
6b7b2cf7 1028
afbf3ca7
BA
1029 getComputerMove() {
1030 const maxeval = V.INFINITY;
1031 const color = this.turn;
1032 let moves1 = this.getAllValidMoves();
1033
1034 if (moves1.length == 0)
1035 // TODO: this situation should not happen
1036 return null;
1037
3a2a7b5f 1038 const setEval = (move, next) => {
afbf3ca7 1039 const score = this.getCurrentScore();
3a2a7b5f 1040 const curEval = move.eval;
afbf3ca7
BA
1041 if (score != "*") {
1042 move.eval =
1043 score == "1/2"
1044 ? 0
1045 : (score == "1-0" ? 1 : -1) * maxeval;
3a2a7b5f
BA
1046 } else move.eval = this.evalPosition();
1047 if (
1048 // "next" is defined after sentry pushes
1049 !!next && (
1050 !curEval ||
1051 color == 'w' && move.eval > curEval ||
1052 color == 'b' && move.eval < curEval
1053 )
1054 ) {
1055 move.second = next;
1056 }
afbf3ca7
BA
1057 };
1058
1059 // Just search_depth == 1 (because of sentries. TODO: can do better...)
1060 moves1.forEach(m1 => {
1061 this.play(m1);
1062 if (this.subTurn == 1) setEval(m1);
1063 else {
1064 // Need to play every pushes and count:
1065 const moves2 = this.getAllValidMoves();
1066 moves2.forEach(m2 => {
1067 this.play(m2);
3a2a7b5f 1068 setEval(m1, m2);
afbf3ca7
BA
1069 this.undo(m2);
1070 });
1071 }
1072 this.undo(m1);
1073 });
1074
1075 moves1.sort((a, b) => {
1076 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
1077 });
1078 let candidates = [0];
1079 for (let j = 1; j < moves1.length && moves1[j].eval == moves1[0].eval; j++)
1080 candidates.push(j);
3a2a7b5f
BA
1081 const choice = moves1[candidates[randInt(candidates.length)]];
1082 return (!choice.second ? choice : [choice, choice.second]);
afbf3ca7
BA
1083 }
1084
f14572c4
BA
1085 // For moves notation:
1086 static get LANCER_DIRNAMES() {
1087 return {
1088 'c': "N",
1089 'd': "NE",
1090 'e': "E",
1091 'f': "SE",
1092 'g': "S",
1093 'h': "SW",
1094 'm': "W",
1095 'o': "NW"
1096 };
1097 }
1098
6b7b2cf7
BA
1099 getNotation(move) {
1100 // Special case "king takes jailer" is a pass move
1101 if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
f14572c4 1102 let notation = undefined;
b0a0468a
BA
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,
1108 start: move.start,
1109 end: move.end
1110 };
f14572c4 1111 notation = super.getNotation(simpleMove);
1b56b736
BA
1112 }
1113 else if (
1114 move.appear.length > 0 &&
1115 move.vanish[0].x == move.appear[0].x &&
1116 move.vanish[0].y == move.appear[0].y
1117 ) {
1118 // Lancer in-place reorientation:
1119 notation = "L" + V.CoordsToSquare(move.start) + ":R";
1120 }
1121 else notation = super.getNotation(move);
f14572c4
BA
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];
7e476ce4
BA
1125 else if (
1126 move.vanish[0].p == V.PAWN &&
1127 Object.keys(V.LANCER_DIRNAMES).includes(move.appear[0].p)
1128 ) {
1129 // Fix promotions in lancer:
2c5d7b20
BA
1130 notation = notation.slice(0, -1) +
1131 "L:" + V.LANCER_DIRNAMES[move.appear[0].p];
7e476ce4 1132 }
f14572c4 1133 return notation;
6b7b2cf7 1134 }
7e8a7ea1 1135
2a8a94c9 1136};