Experimental board size auto-adjust
[vchess.git] / client / src / variants / Pacosako.js
CommitLineData
173f11dc
BA
1import { ChessRules, PiPo, Move } from "@/base_rules";
2import { randInt } from "@/utils/alea";
3
4export class PacosakoRules extends ChessRules {
5
89a6214b
BA
6 static get Options() {
7 return {
8 select: ChessRules.Options.select,
9 check: [
10 {
11 label: "pacoplay mode",
12 variable: "pacoplay",
13 defaut: false
14 }
15 ]
16 };
17 }
18
bc1e1f2a
BA
19 static AbbreviateOptions(opts) {
20 return (opts["pacoplay"] ? "PP" : "");
21 }
22
173f11dc
BA
23 static get IMAGE_EXTENSION() {
24 return ".png";
25 }
26
27 // Unions (left = white if upperCase, black otherwise)
28 static get UNIONS() {
29 return {
30 a: ['p', 'p'],
31 c: ['p', 'r'],
32 d: ['p', 'n'],
33 e: ['p', 'b'],
34 f: ['p', 'q'],
35 g: ['p', 'k'],
36 h: ['r', 'r'],
37 i: ['r', 'n'],
38 j: ['r', 'b'],
39 l: ['r', 'q'],
40 m: ['r', 'k'],
41 o: ['n', 'n'],
42 s: ['n', 'b'],
43 t: ['n', 'q'],
44 u: ['n', 'k'],
45 v: ['b', 'b'],
46 w: ['b', 'q'],
47 x: ['b', 'k'],
48 y: ['q', 'q'],
059f0aa2 49 z: ['q', 'k'],
d982fffc 50 '@': ['k', 'k']
173f11dc
BA
51 };
52 }
53
6cc34165 54 static fen2board(f) {
d982fffc
BA
55 // Arobase is character 64
56 return f.charCodeAt() <= 90 ? "w" + f.toLowerCase() : "b" + f;
6cc34165
BA
57 }
58
173f11dc
BA
59 static IsGoodPosition(position) {
60 if (position.length == 0) return false;
61 const rows = position.split("/");
62 if (rows.length != V.size.x) return false;
d982fffc 63 let kingSymb = ['k', 'g', 'm', 'u', 'x', 'z', '@'];
173f11dc
BA
64 let kings = { 'k': 0, 'K': 0 };
65 for (let row of rows) {
66 let sumElts = 0;
67 for (let i = 0; i < row.length; i++) {
d982fffc 68 if (!!(row[i].toLowerCase().match(/[a-z@]/))) {
173f11dc
BA
69 sumElts++;
70 if (kingSymb.includes(row[i])) kings['k']++;
059f0aa2
BA
71 // Not "else if", if two kings dancing together
72 if (kingSymb.some(s => row[i] == s.toUpperCase())) kings['K']++;
173f11dc
BA
73 }
74 else {
75 const num = parseInt(row[i], 10);
76 if (isNaN(num) || num <= 0) return false;
77 sumElts += num;
78 }
79 }
80 if (sumElts != V.size.y) return false;
81 }
82 // Both kings should be on board. Exactly one per color.
83 if (Object.values(kings).some(v => v != 1)) return false;
84 return true;
85 }
86
87 getPpath(b) {
88 return "Pacosako/" + b;
89 }
90
4a209313 91 getPPpath(m) {
173f11dc
BA
92 if (ChessRules.PIECES.includes(m.appear[0].p)) return super.getPPpath(m);
93 // For an union, show only relevant piece:
94 // The color must be deduced from the move: reaching final rank of who?
4a209313
BA
95 const color = (m.appear[0].x == 0 ? 'w' : 'b');
96 const up = this.getUnionPieces(m.appear[0].c, m.appear[0].p);
97 return "Pacosako/" + color + up[color];
173f11dc
BA
98 }
99
100 canTake([x1, y1], [x2, y2]) {
4a209313
BA
101 const p1 = this.board[x1][y1].charAt(1);
102 if (!(ChessRules.PIECES.includes(p1))) return false;
103 const p2 = this.board[x2][y2].charAt(1);
104 if (!(ChessRules.PIECES.includes(p2))) return true;
105 const c1 = this.board[x1][y1].charAt(0);
106 const c2 = this.board[x2][y2].charAt(0);
107 return (c1 != c2);
173f11dc
BA
108 }
109
110 canIplay(side, [x, y]) {
4a209313
BA
111 return (
112 this.turn == side &&
113 (
114 !(ChessRules.PIECES.includes(this.board[x][y].charAt(1))) ||
115 this.board[x][y].charAt(0) == side
116 )
117 );
173f11dc
BA
118 }
119
120 scanKings(fen) {
121 this.kingPos = { w: [-1, -1], b: [-1, -1] };
122 const fenRows = V.ParseFen(fen).position.split("/");
123 const startRow = { 'w': V.size.x - 1, 'b': 0 };
d982fffc 124 const kingSymb = ['k', 'g', 'm', 'u', 'x', 'z', '@'];
173f11dc
BA
125 for (let i = 0; i < fenRows.length; i++) {
126 let k = 0;
127 for (let j = 0; j < fenRows[i].length; j++) {
128 const c = fenRows[i].charAt(j);
d982fffc 129 if (!!(c.toLowerCase().match(/[a-z@]/))) {
059f0aa2
BA
130 if (kingSymb.includes(c))
131 this.kingPos["b"] = [i, k];
132 // Not "else if", in case of two kings dancing together
133 if (kingSymb.some(s => c == s.toUpperCase()))
134 this.kingPos["w"] = [i, k];
135 }
173f11dc
BA
136 else {
137 const num = parseInt(fenRows[i].charAt(j), 10);
138 if (!isNaN(num)) k += num - 1;
139 }
140 k++;
141 }
142 }
143 }
144
145 setOtherVariables(fen) {
173f11dc
BA
146 // Stack of "last move" only for intermediate chaining
147 this.lastMoveEnd = [null];
4a209313
BA
148 // Local stack of non-capturing union moves:
149 this.umoves = [];
150 const umove = V.ParseFen(fen).umove;
89a6214b
BA
151 this.pacoplay = !umove; //"pacoplay.com mode" ?
152 if (!this.pacoplay) {
153 if (umove == "-") this.umoves.push(null);
154 else {
155 this.umoves.push({
156 start: ChessRules.SquareToCoords(umove.substr(0, 2)),
157 end: ChessRules.SquareToCoords(umove.substr(2))
158 });
159 }
4a209313 160 }
f3f84707
BA
161 // Local stack of positions to avoid redundant moves:
162 this.repetitions = [];
89a6214b 163 super.setOtherVariables(fen);
4a209313
BA
164 }
165
166 static IsGoodFen(fen) {
167 if (!ChessRules.IsGoodFen(fen)) return false;
168 const fenParts = fen.split(" ");
169 if (fenParts.length != 6) return false;
170 if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/))
171 return false;
172 return true;
173 }
174
059f0aa2 175 static IsGoodFlags(flags) {
89a6214b
BA
176 // 4 for castle + 16 for pawns (more permissive, for pacoplay mode)
177 return !!flags.match(/^[a-z]{4,4}[01]{0,16}$/);
059f0aa2
BA
178 }
179
180 setFlags(fenflags) {
181 super.setFlags(fenflags); //castleFlags
89a6214b 182 if (this.pacoplay) return;
059f0aa2
BA
183 this.pawnFlags = {
184 w: [...Array(8)], //pawns can move 2 squares?
185 b: [...Array(8)]
186 };
187 const flags = fenflags.substr(4); //skip first 4 letters, for castle
188 for (let c of ["w", "b"]) {
189 for (let i = 0; i < 8; i++)
190 this.pawnFlags[c][i] = flags.charAt((c == "w" ? 0 : 8) + i) == "1";
191 }
192 }
193
194 aggregateFlags() {
89a6214b 195 if (!this.pacoplay) return super.aggregateFlags();
059f0aa2
BA
196 return [this.castleFlags, this.pawnFlags];
197 }
198
199 disaggregateFlags(flags) {
89a6214b
BA
200 if (!this.pacoplay) super.disaggregateFlags(flags);
201 else {
202 this.castleFlags = flags[0];
203 this.pawnFlags = flags[1];
204 }
059f0aa2
BA
205 }
206
4a209313
BA
207 getUmove(move) {
208 if (
209 move.vanish.length == 1 &&
9d15c433
BA
210 !(ChessRules.PIECES.includes(move.appear[0].p)) &&
211 move.appear[0].p == move.vanish[0].p //not a promotion
4a209313
BA
212 ) {
213 // An union moving
214 return { start: move.start, end: move.end };
215 }
216 return null;
217 }
218
219 static ParseFen(fen) {
220 const fenParts = fen.split(" ");
221 return Object.assign(
222 ChessRules.ParseFen(fen),
223 { umove: fenParts[5] }
224 );
225 }
226
4313762d 227 static GenRandInitFen(options) {
059f0aa2 228 // Add 16 pawns flags + empty umove:
89a6214b
BA
229 const pawnFlags = (options.pacoplay ? "" : "1111111111111111");
230 return ChessRules.GenRandInitFen(options).slice(0, -2) +
231 pawnFlags + " -" + (!options.pacoplay ? " -" : "");
059f0aa2
BA
232 }
233
234 getFlagsFen() {
235 let fen = super.getFlagsFen();
89a6214b
BA
236 if (!this.pacoplay) {
237 // Add pawns flags
238 for (let c of ["w", "b"])
239 for (let i = 0; i < 8; i++) fen += (this.pawnFlags[c][i] ? "1" : "0");
240 }
059f0aa2 241 return fen;
4a209313
BA
242 }
243
244 getUmoveFen() {
245 const L = this.umoves.length;
246 return (
247 !this.umoves[L - 1]
248 ? "-"
249 : ChessRules.CoordsToSquare(this.umoves[L - 1].start) +
250 ChessRules.CoordsToSquare(this.umoves[L - 1].end)
251 );
252 }
253
254 getFen() {
89a6214b
BA
255 const umoveFen = this.pacoplay ? "" : (" " + this.getUmoveFen());
256 return super.getFen() + umoveFen;
4a209313
BA
257 }
258
259 getFenForRepeat() {
89a6214b
BA
260 const umoveFen = this.pacoplay ? "" : ("_" + this.getUmoveFen());
261 return super.getFenForRepeat() + umoveFen;
173f11dc
BA
262 }
263
264 getColor(i, j) {
265 const p = this.board[i][j].charAt(1);
266 if (ChessRules.PIECES.includes(p)) return super.getColor(i, j);
4a209313 267 return this.turn; //union: I can use it, so it's "my" color...
173f11dc
BA
268 }
269
270 getPiece(i, j, color) {
271 const p = this.board[i][j].charAt(1);
173f11dc
BA
272 if (ChessRules.PIECES.includes(p)) return p;
273 const c = this.board[i][j].charAt(0);
274 // NOTE: this.turn == HACK, but should work...
275 color = color || this.turn;
276 return V.UNIONS[p][c == color ? 0 : 1];
277 }
278
279 getUnionPieces(color, code) {
280 const pieces = V.UNIONS[code];
281 return {
282 w: pieces[color == 'w' ? 0 : 1],
283 b: pieces[color == 'b' ? 0 : 1]
284 };
285 }
286
4a209313 287 // p1: white piece, p2: black piece
173f11dc
BA
288 getUnionCode(p1, p2) {
289 let uIdx = (
290 Object.values(V.UNIONS).findIndex(v => v[0] == p1 && v[1] == p2)
291 );
292 const c = (uIdx >= 0 ? 'w' : 'b');
293 if (uIdx == -1) {
294 uIdx = (
295 Object.values(V.UNIONS).findIndex(v => v[0] == p2 && v[1] == p1)
296 );
297 }
298 return { c: c, p: Object.keys(V.UNIONS)[uIdx] };
299 }
300
301 getBasicMove([sx, sy], [ex, ey], tr) {
4a209313
BA
302 const L = this.lastMoveEnd.length;
303 const lm = this.lastMoveEnd[L-1];
304 const piece = (!!lm ? lm.p : null);
305 const initColor = (!!piece ? this.turn : this.board[sx][sy].charAt(0));
306 const initPiece = (piece || this.board[sx][sy].charAt(1));
307 const c = this.turn;
308 const oppCol = V.GetOppCol(c);
309 if (!!tr && !(ChessRules.PIECES.includes(initPiece))) {
310 // Transformation computed without taking union into account
311 const up = this.getUnionPieces(initColor, initPiece);
312 let args = [tr.p, up[oppCol]];
313 if (c == 'b') args = args.reverse();
314 const cp = this.getUnionCode(args[0], args[1]);
315 tr.c = cp.c;
316 tr.p = cp.p;
317 }
173f11dc
BA
318 // 4 cases : moving
319 // - union to free square (other cases are illegal: return null)
320 // - normal piece to free square,
321 // to enemy normal piece, or
322 // to union (releasing our piece)
323 let mv = new Move({
4a209313
BA
324 start: { x: sx, y: sy },
325 end: { x: ex, y: ey },
326 vanish: []
327 });
328 if (!piece) {
329 mv.vanish = [
173f11dc
BA
330 new PiPo({
331 x: sx,
332 y: sy,
333 c: initColor,
334 p: initPiece
335 })
4a209313
BA
336 ];
337 }
173f11dc
BA
338 // Treat free square cases first:
339 if (this.board[ex][ey] == V.EMPTY) {
340 mv.appear = [
341 new PiPo({
342 x: ex,
343 y: ey,
4a209313 344 c: !!tr ? tr.c : initColor,
173f11dc
BA
345 p: !!tr ? tr.p : initPiece
346 })
347 ];
348 return mv;
349 }
350 // Now the two cases with union / release:
351 const destColor = this.board[ex][ey].charAt(0);
352 const destPiece = this.board[ex][ey].charAt(1);
353 mv.vanish.push(
354 new PiPo({
355 x: ex,
356 y: ey,
357 c: destColor,
358 p: destPiece
359 })
360 );
361 if (ChessRules.PIECES.includes(destPiece)) {
362 // Normal piece: just create union
4a209313
BA
363 let args = [!!tr ? tr.p : initPiece, destPiece];
364 if (c == 'b') args = args.reverse();
365 const cp = this.getUnionCode(args[0], args[1]);
173f11dc
BA
366 mv.appear = [
367 new PiPo({
368 x: ex,
369 y: ey,
370 c: cp.c,
371 p: cp.p
372 })
373 ];
374 return mv;
375 }
376 // Releasing a piece in an union: keep track of released piece
377 const up = this.getUnionPieces(destColor, destPiece);
4a209313
BA
378 let args = [!!tr ? tr.p : initPiece, up[oppCol]];
379 if (c == 'b') args = args.reverse();
380 const cp = this.getUnionCode(args[0], args[1]);
173f11dc
BA
381 mv.appear = [
382 new PiPo({
383 x: ex,
384 y: ey,
385 c: cp.c,
386 p: cp.p
387 })
388 ];
9d15c433
BA
389 // In move.end, to be sent to the server
390 mv.end.released = up[c];
173f11dc
BA
391 return mv;
392 }
393
4a209313 394 getPotentialMovesFrom([x, y]) {
173f11dc
BA
395 const L = this.lastMoveEnd.length;
396 const lm = this.lastMoveEnd[L-1];
4a209313
BA
397 if (!!lm && (x != lm.x || y != lm.y)) return [];
398 const piece = (!!lm ? lm.p : this.getPiece(x, y));
173f11dc 399 if (!!lm) {
4a209313 400 var saveSquare = this.board[x][y];
173f11dc
BA
401 this.board[x][y] = this.turn + piece;
402 }
403 let baseMoves = [];
059f0aa2 404 const c = this.turn;
1006b211 405 switch (piece) {
059f0aa2
BA
406 case V.PAWN: {
407 const firstRank = (c == 'w' ? 7 : 0);
408 baseMoves = this.getPotentialPawnMoves([x, y]).filter(m => {
409 // Skip forbidden 2-squares jumps (except from first rank)
410 // Also skip unions capturing en-passant (not allowed).
411 return (
412 (
413 m.start.x == firstRank ||
414 Math.abs(m.end.x - m.start.x) == 1 ||
89a6214b
BA
415 this.pacoplay ||
416 (!this.pacoplay && this.pawnFlags[c][m.start.y])
059f0aa2
BA
417 )
418 &&
419 (
420 this.board[x][y].charAt(1) == V.PAWN ||
421 m.start.y == m.end.y
422 )
423 );
424 });
173f11dc 425 break;
059f0aa2 426 }
173f11dc
BA
427 case V.ROOK:
428 baseMoves = this.getPotentialRookMoves([x, y]);
429 break;
430 case V.KNIGHT:
431 baseMoves = this.getPotentialKnightMoves([x, y]);
432 break;
433 case V.BISHOP:
434 baseMoves = this.getPotentialBishopMoves([x, y]);
435 break;
436 case V.QUEEN:
437 baseMoves = this.getPotentialQueenMoves([x, y]);
438 break;
439 case V.KING:
440 baseMoves = this.getPotentialKingMoves([x, y]);
441 break;
442 }
443 // When a pawn in an union reaches final rank with a non-standard
444 // promotion move: apply promotion anyway
445 let moves = [];
4a209313
BA
446 const oppCol = V.GetOppCol(c);
447 const oppLastRank = (c == 'w' ? 7 : 0);
173f11dc 448 baseMoves.forEach(m => {
4a209313
BA
449 if (
450 m.end.x == oppLastRank &&
451 ['c', 'd', 'e', 'f', 'g'].includes(m.appear[0].p)
452 ) {
453 // Move to first rank, which is last rank for opponent's pawn.
454 // => Show promotion choices.
455 // Find our piece in union (not a pawn)
456 const up = this.getUnionPieces(m.appear[0].c, m.appear[0].p);
457 // merge with all potential promotion pieces + push (loop)
458 for (let promotionPiece of [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]) {
459 let args = [up[c], promotionPiece];
460 if (c == 'b') args = args.reverse();
461 const cp = this.getUnionCode(args[0], args[1]);
462 let cpMove = JSON.parse(JSON.stringify(m));
463 cpMove.appear[0].c = cp.c;
464 cpMove.appear[0].p = cp.p;
465 moves.push(cpMove);
466 }
467 }
468 else {
469 if (
470 m.vanish.length > 0 &&
471 m.vanish[0].p == V.PAWN &&
472 m.start.y != m.end.y &&
473 this.board[m.end.x][m.end.y] == V.EMPTY
474 ) {
475 if (!!lm)
476 // No en-passant inside a chaining
477 return;
478 // Fix en-passant capture: union type, maybe released piece too
479 const cs = [m.end.x + (c == 'w' ? 1 : -1), m.end.y];
4a209313
BA
480 const code = this.board[cs[0]][cs[1]].charAt(1);
481 if (code == V.PAWN) {
482 // Simple en-passant capture (usual: just form union)
483 m.appear[0].c = 'w';
484 m.appear[0].p = 'a';
485 }
486 else {
9d15c433
BA
487 // An union pawn + something just moved two squares
488 const color = this.board[cs[0]][cs[1]].charAt(0);
4a209313 489 const up = this.getUnionPieces(color, code);
9d15c433 490 m.end.released = up[c];
4a209313
BA
491 let args = [V.PAWN, up[oppCol]];
492 if (c == 'b') args = args.reverse();
493 const cp = this.getUnionCode(args[0], args[1]);
494 m.appear[0].c = cp.c;
495 m.appear[0].p = cp.p;
496 }
497 }
498 moves.push(m);
499 }
173f11dc 500 });
4a209313 501 if (!!lm) this.board[x][y] = saveSquare;
173f11dc
BA
502 return moves;
503 }
504
89a6214b
BA
505 getPotentialKingMoves(sq) {
506 if (!this.pacoplay) return super.getPotentialKingMoves(sq);
507 // Initialize with normal moves, without captures
508 let moves = [];
509 for (let s of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
510 const [i, j] = [sq[0] + s[0], sq[1] + s[1]];
511 if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY)
512 moves.push(this.getBasicMove(sq, [i, j]));
513 }
514 if (this.castleFlags[this.turn].some(v => v < V.size.y))
515 moves = moves.concat(this.getCastleMoves(sq));
516 return moves;
517 }
518
4a209313
BA
519 getEpSquare(moveOrSquare) {
520 if (typeof moveOrSquare === "string") {
521 const square = moveOrSquare;
522 if (square == "-") return undefined;
523 return V.SquareToCoords(square);
524 }
525 const move = moveOrSquare;
526 const s = move.start,
527 e = move.end;
4a209313
BA
528 if (
529 s.y == e.y &&
530 Math.abs(s.x - e.x) == 2 &&
9d15c433 531 this.getPiece(s.x, s.y, this.turn) == V.PAWN
4a209313
BA
532 ) {
533 return {
534 x: (s.x + e.x) / 2,
535 y: s.y
536 };
537 }
538 return undefined;
539 }
540
541 // Does m2 un-do m1 ? (to disallow undoing union moves)
542 oppositeMoves(m1, m2) {
543 return (
544 !!m1 &&
545 !(ChessRules.PIECES.includes(m2.appear[0].p)) &&
546 m2.vanish.length == 1 &&
9d15c433 547 !m2.end.released &&
4a209313
BA
548 m1.start.x == m2.end.x &&
549 m1.end.x == m2.start.x &&
550 m1.start.y == m2.end.y &&
551 m1.end.y == m2.start.y
552 );
553 }
554
059f0aa2
BA
555 getCastleMoves([x, y]) {
556 const c = this.getColor(x, y);
557 const oppCol = V.GetOppCol(c);
558 let moves = [];
559 const finalSquares = [ [2, 3], [6, 5] ];
560 castlingCheck: for (let castleSide = 0; castleSide < 2; castleSide++) {
561 if (this.castleFlags[c][castleSide] >= 8) continue;
562 const rookPos = this.castleFlags[c][castleSide];
0b4bca84
BA
563 const castlingColor = this.board[x][rookPos].charAt(0);
564 const castlingPiece = this.board[x][rookPos].charAt(1);
059f0aa2
BA
565
566 // Nothing on the path of the king ?
567 const finDist = finalSquares[castleSide][0] - y;
568 let step = finDist / Math.max(1, Math.abs(finDist));
569 let i = y;
570 let kingSquares = [y];
571 do {
572 if (
573 (
574 this.board[x][i] != V.EMPTY &&
575 (this.getColor(x, i) != c || ![y, rookPos].includes(i))
576 )
577 ) {
578 continue castlingCheck;
579 }
580 i += step;
581 kingSquares.push(i);
582 } while (i != finalSquares[castleSide][0]);
583 // No checks on the path of the king ?
584 if (this.isAttacked(kingSquares, oppCol)) continue castlingCheck;
585
586 // Nothing on the path to the rook?
587 step = castleSide == 0 ? -1 : 1;
588 for (i = y + step; i != rookPos; i += step) {
589 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
590 }
591
592 // Nothing on final squares, except maybe king and castling rook?
593 for (i = 0; i < 2; i++) {
594 if (
595 finalSquares[castleSide][i] != rookPos &&
596 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
597 (
598 finalSquares[castleSide][i] != y ||
599 this.getColor(x, finalSquares[castleSide][i]) != c
600 )
601 ) {
602 continue castlingCheck;
603 }
604 }
605
606 moves.push(
607 new Move({
608 appear: [
609 new PiPo({
610 x: x,
611 y: finalSquares[castleSide][0],
612 p: V.KING,
613 c: c
614 }),
615 new PiPo({
616 x: x,
617 y: finalSquares[castleSide][1],
0b4bca84
BA
618 p: castlingPiece,
619 c: castlingColor
059f0aa2
BA
620 })
621 ],
622 vanish: [
623 // King might be initially disguised (Titan...)
624 new PiPo({ x: x, y: y, p: V.KING, c: c }),
0b4bca84 625 new PiPo({ x: x, y: rookPos, p: castlingPiece, c: castlingColor })
059f0aa2
BA
626 ],
627 end:
628 Math.abs(y - rookPos) <= 2
629 ? { x: x, y: rookPos }
630 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
631 })
632 );
633 }
634
635 return moves;
636 }
637
0b4bca84
BA
638 getEnpassantCaptures(sq, shiftX) {
639 // HACK: when artificially change turn, do not consider en-passant
640 const mcMod2 = this.movesCount % 2;
641 const c = this.turn;
642 if ((c == 'w' && mcMod2 == 1) || (c == 'b' && mcMod2 == 0)) return [];
643 return super.getEnpassantCaptures(sq, shiftX);
644 }
645
059f0aa2
BA
646 isAttacked_aux(files, color, positions, fromSquare, released) {
647 // "positions" = array of FENs to detect infinite loops. Example:
648 // r1q1k2r/p1Pb1ppp/5n2/1f1p4/AV5P/P1eDP3/3B1PP1/R3K1NR,
649 // Bxd2 Bxc3 Bxb4 Bxc3 Bxb4 etc.
f7197575
BA
650 const newPos = {
651 fen: super.getBaseFen(),
652 piece: released,
653 from: fromSquare
654 };
655 if (
656 positions.some(p => {
657 return (
658 p.piece == newPos.piece &&
659 p.fen == newPos.fen &&
660 p.from == newPos.from
661 );
662 })
663 ) {
059f0aa2
BA
664 // Start of an infinite loop: exit
665 return false;
f7197575 666 }
059f0aa2
BA
667 positions.push(newPos);
668 const rank = (color == 'w' ? 0 : 7);
669 const moves = this.getPotentialMovesFrom(fromSquare);
670 if (moves.some(m => m.end.x == rank && files.includes(m.end.y)))
671 // Found an attack!
672 return true;
673 for (let m of moves) {
9d15c433 674 if (!!m.end.released) {
059f0aa2
BA
675 // Turn won't change since !!m.released
676 this.play(m);
677 const res = this.isAttacked_aux(
9d15c433 678 files, color, positions, [m.end.x, m.end.y], m.end.released);
059f0aa2
BA
679 this.undo(m);
680 if (res) return true;
681 }
682 }
4a209313
BA
683 return false;
684 }
059f0aa2
BA
685
686 isAttacked(files, color) {
687 const rank = (color == 'w' ? 0 : 7);
688 // Since it's too difficult (impossible?) to search from the square itself,
689 // let's adopt a suboptimal but working strategy: find all attacks.
690 const c = this.turn;
691 // Artificial turn change is required:
692 this.turn = color;
693 let res = false;
694 outerLoop: for (let i=0; i<8; i++) {
695 for (let j=0; j<8; j++) {
696 // Attacks must start from a normal piece, not an union.
697 // Therefore, the following test is correct.
698 if (
699 this.board[i][j] != V.EMPTY &&
700 // Do not start with king (irrelevant, and lead to infinite calls)
701 [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN].includes(
702 this.board[i][j].charAt(1)) &&
703 this.board[i][j].charAt(0) == color
704 ) {
705 // Try from here.
706 const moves = this.getPotentialMovesFrom([i, j]);
707 if (moves.some(m => m.end.x == rank && files.includes(m.end.y))) {
708 res = true;
709 break outerLoop;
710 }
711 for (let m of moves) {
9d15c433 712 if (!!m.end.released) {
059f0aa2
BA
713 // Turn won't change since !!m.released
714 this.play(m);
715 let positions = [];
716 res = this.isAttacked_aux(
9d15c433 717 files, color, positions, [m.end.x, m.end.y], m.end.released);
059f0aa2
BA
718 this.undo(m);
719 if (res) break outerLoop;
720 }
721 }
722 }
723 }
724 }
725 this.turn = c;
726 return res;
727 }
728
da9e846e
BA
729 isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
730 for (let step of steps) {
731 let rx = x + step[0],
732 ry = y + step[1];
733 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
734 rx += step[0];
735 ry += step[1];
736 }
737 if (
738 V.OnBoard(rx, ry) &&
739 this.board[rx][ry] != V.EMPTY &&
740 this.getPiece(rx, ry) == piece &&
741 this.getColor(rx, ry) == color &&
742 this.canTake([rx, ry], [x, y]) //TODO: necessary line?
743 //If not, generic method is OK
744 ) {
745 return true;
746 }
747 }
748 return false;
749 }
750
059f0aa2 751 // Do not consider checks, except to forbid castling
4a209313
BA
752 getCheckSquares() {
753 return [];
754 }
f3f84707 755
4a209313
BA
756 filterValid(moves) {
757 if (moves.length == 0) return [];
89a6214b 758 const L = (!this.pacoplay ? this.umoves.length : 0);
f3f84707 759 return moves.filter(m => {
89a6214b 760 if (L > 0 && this.oppositeMoves(this.umoves[L - 1], m)) return false;
f3f84707
BA
761 if (!m.end.released) return true;
762 // Check for repetitions:
763 V.PlayOnBoard(this.board, m);
d2af3400
BA
764 const newState = {
765 piece: m.end.released,
766 square: { x: m.end.x, y: m.end.y },
767 position: this.getBaseFen()
768 };
f3f84707
BA
769 const repet =
770 this.repetitions.some(r => {
771 return (
772 r.piece == newState.piece &&
d2af3400
BA
773 (
774 r.square.x == newState.square.x &&
1328e7dd 775 r.square.y == newState.square.y
d2af3400 776 ) &&
f3f84707
BA
777 r.position == newState.position
778 );
779 });
780 V.UndoOnBoard(this.board, m);
781 return !repet;
782 });
4a209313
BA
783 }
784
0b4bca84 785 updateCastleFlags(move, piece) {
4258b58c 786 const c = this.turn;
0b4bca84 787 const firstRank = (c == "w" ? 7 : 0);
0b4bca84
BA
788 if (piece == V.KING && move.appear.length > 0)
789 this.castleFlags[c] = [V.size.y, V.size.y];
790 else if (
791 move.start.x == firstRank &&
792 this.castleFlags[c].includes(move.start.y)
793 ) {
794 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
795 this.castleFlags[c][flagIdx] = V.size.y;
796 }
4258b58c
BA
797 else if (
798 move.end.x == firstRank &&
799 this.castleFlags[c].includes(move.end.y)
800 ) {
801 // Move to our rook: necessary normal piece, to union, releasing
802 // (or the rook was moved before!)
803 const flagIdx = (move.end.y == this.castleFlags[c][0] ? 0 : 1);
804 this.castleFlags[c][flagIdx] = V.size.y;
805 }
0b4bca84
BA
806 }
807
4258b58c
BA
808 prePlay(move) {
809 // Easier before move is played in this case (flags are saved)
810 const c = this.turn;
811 const L = this.lastMoveEnd.length;
812 const lm = this.lastMoveEnd[L-1];
8ec71052
BA
813 // NOTE: lm.p != V.KING, always.
814 const piece =
815 !!lm
c3145c37
BA
816 ? lm.p
817 : this.getPiece(move.vanish[0].x, move.vanish[0].y);
4a209313
BA
818 if (piece == V.KING)
819 this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
820 this.updateCastleFlags(move, piece);
4258b58c 821 const pawnFirstRank = (c == 'w' ? 6 : 1);
8ec71052 822 if (
89a6214b 823 !this.pacoplay &&
8ec71052
BA
824 move.start.x == pawnFirstRank &&
825 piece == V.PAWN &&
826 Math.abs(move.end.x - move.start.x) == 2
827 ) {
828 // This move turns off a 2-squares pawn flag
4258b58c 829 this.pawnFlags[c][move.start.y] = false;
8ec71052 830 }
4258b58c
BA
831 }
832
833 play(move) {
834 move.flags = JSON.stringify(this.aggregateFlags());
835 this.prePlay(move);
836 this.epSquares.push(this.getEpSquare(move));
837 // Check if the move is the last of the turn: all cases except releases
9d15c433 838 if (!move.end.released) {
4258b58c
BA
839 // No more union releases available
840 this.turn = V.GetOppCol(this.turn);
841 this.movesCount++;
842 this.lastMoveEnd.push(null);
059f0aa2 843 }
9d15c433
BA
844 else {
845 this.lastMoveEnd.push({
846 p: move.end.released,
847 x: move.end.x,
848 y: move.end.y
849 });
850 }
4258b58c 851 V.PlayOnBoard(this.board, move);
89a6214b 852 if (!this.pacoplay) this.umoves.push(this.getUmove(move));
f3f84707
BA
853 if (!move.end.released) this.repetitions = [];
854 else {
855 this.repetitions.push(
856 {
857 piece: move.end.released,
d2af3400 858 square: { x: move.end.x, y: move.end.y },
f3f84707
BA
859 position: this.getBaseFen()
860 }
861 );
862 }
4a209313
BA
863 }
864
173f11dc
BA
865 undo(move) {
866 this.epSquares.pop();
c27fcf89 867 this.disaggregateFlags(JSON.parse(move.flags));
173f11dc
BA
868 V.UndoOnBoard(this.board, move);
869 this.lastMoveEnd.pop();
9d15c433 870 if (!move.end.released) {
173f11dc
BA
871 this.turn = V.GetOppCol(this.turn);
872 this.movesCount--;
873 }
89a6214b 874 if (!this.pacoplay) this.umoves.pop();
4573adc5 875 if (!!move.end.released) this.repetitions.pop();
3cf54395 876 this.postUndo(move);
173f11dc
BA
877 }
878
4a209313
BA
879 postUndo(move) {
880 if (this.getPiece(move.start.x, move.start.y) == V.KING)
881 this.kingPos[this.turn] = [move.start.x, move.start.y];
882 }
883
173f11dc
BA
884 getCurrentScore() {
885 // Check kings: if one is dancing, the side lost
4a209313 886 // But, if both dancing, let's say it's a draw :-)
173f11dc 887 const [kpW, kpB] = [this.kingPos['w'], this.kingPos['b']];
4a209313
BA
888 const atKingPlace = [
889 this.board[kpW[0]][kpW[1]].charAt(1),
890 this.board[kpB[0]][kpB[1]].charAt(1)
891 ];
892 if (!atKingPlace.includes('k')) return "1/2";
893 if (atKingPlace[0] != 'k') return "0-1";
894 if (atKingPlace[1] != 'k') return "1-0";
173f11dc
BA
895 return "*";
896 }
897
898 getComputerMove() {
4a209313
BA
899 let initMoves = this.getAllValidMoves();
900 if (initMoves.length == 0) return null;
901 // Loop until valid move is found (no blocked pawn released...)
902 while (true) {
903 let moves = JSON.parse(JSON.stringify(initMoves));
904 let mvArray = [];
905 let mv = null;
906 // Just play random moves (for now at least. TODO?)
907 while (moves.length > 0) {
908 mv = moves[randInt(moves.length)];
909 mvArray.push(mv);
910 this.play(mv);
9d15c433 911 if (!!mv.end.released)
4a209313
BA
912 // A piece was just released from an union
913 moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]);
914 else break;
915 }
916 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
9d15c433 917 if (!mv.end.released) return (mvArray.length > 1 ? mvArray : mvArray[0]);
173f11dc 918 }
2da551a3 919 return null; //never reached
173f11dc
BA
920 }
921
922 // NOTE: evalPosition() is wrong, but unused since bot plays at random
923
924 getNotation(move) {
4a209313
BA
925 if (move.appear.length == 2 && move.appear[0].p == V.KING)
926 return (move.end.y < move.start.y ? "0-0-0" : "0-0");
927
928 const c = this.turn;
929 const L = this.lastMoveEnd.length;
930 const lm = this.lastMoveEnd[L-1];
931 let piece = null;
932 if (!lm && move.vanish.length == 0)
933 // When importing a game, the info move.released is lost
934 piece = move.appear[0].p;
935 else piece = (!!lm ? lm.p : move.vanish[0].p);
936 if (!(ChessRules.PIECES.includes(piece))) {
937 // Decode (moving) union
938 const up = this.getUnionPieces(
939 move.vanish.length > 0 ? move.vanish[0].c : move.appear[0].c, piece);
940 piece = up[c]
941 }
942
943 // Basic move notation:
944 let notation = piece.toUpperCase();
945 if (
946 this.board[move.end.x][move.end.y] != V.EMPTY ||
947 (piece == V.PAWN && move.start.y != move.end.y)
948 ) {
949 notation += "x";
950 }
951 const finalSquare = V.CoordsToSquare(move.end);
952 notation += finalSquare;
953
954 // Add potential promotion indications:
955 const firstLastRank = (c == 'w' ? [7, 0] : [0, 7]);
956 if (move.end.x == firstLastRank[1] && piece == V.PAWN) {
afcfb852
BA
957 notation += "=";
958 if (ChessRules.PIECES.includes(move.appear[0].p))
959 notation += move.appear[0].p.toUpperCase();
960 else {
961 const up = this.getUnionPieces(move.appear[0].c, move.appear[0].p);
962 notation += up[c].toUpperCase();
963 }
4a209313
BA
964 }
965 else if (
966 move.end.x == firstLastRank[0] &&
967 move.vanish.length > 0 &&
968 ['c', 'd', 'e', 'f', 'g'].includes(move.vanish[0].p)
969 ) {
970 // We promoted an opponent's pawn
971 const oppCol = V.GetOppCol(c);
972 const up = this.getUnionPieces(move.appear[0].c, move.appear[0].p);
973 notation += "=" + up[oppCol].toUpperCase();
974 }
975
976 return notation;
173f11dc
BA
977 }
978
979};