Save current state (unfinished, untested)
[vchess.git] / public / javascripts / base_rules.js
1 // (Orthodox) Chess rules are defined in ChessRules class.
2 // Variants generally inherit from it, and modify some parts.
3
4 class PiPo //Piece+Position
5 {
6 // o: {piece[p], color[c], posX[x], posY[y]}
7 constructor(o)
8 {
9 this.p = o.p;
10 this.c = o.c;
11 this.x = o.x;
12 this.y = o.y;
13 }
14 }
15
16 // TODO: for animation, moves should contains "moving" and "fading" maybe...
17 class Move
18 {
19 // o: {appear, vanish, [start,] [end,]}
20 // appear,vanish = arrays of PiPo
21 // start,end = coordinates to apply to trigger move visually (think castle)
22 constructor(o)
23 {
24 this.appear = o.appear;
25 this.vanish = o.vanish;
26 this.start = !!o.start ? o.start : {x:o.vanish[0].x, y:o.vanish[0].y};
27 this.end = !!o.end ? o.end : {x:o.appear[0].x, y:o.appear[0].y};
28 }
29 }
30
31 // NOTE: x coords = top to bottom; y = left to right (from white player perspective)
32 class ChessRules
33 {
34 //////////////
35 // MISC UTILS
36
37 // Path to pieces
38 static getPpath(b)
39 {
40 return b; //usual pieces in pieces/ folder
41 }
42
43 // Turn "wb" into "B" (for FEN)
44 static board2fen(b)
45 {
46 return b[0]=='w' ? b[1].toUpperCase() : b[1];
47 }
48
49 // Turn "p" into "bp" (for board)
50 static fen2board(f)
51 {
52 return f.charCodeAt()<=90 ? "w"+f.toLowerCase() : "b"+f;
53 }
54
55 // Check if FEN describe a position
56 static IsGoodFen(fen)
57 {
58 const fenParsed = V.ParseFen(fen);
59 // 1) Check position
60 const position = fenParsed.position;
61 const rows = position.split("/");
62 if (rows.length != V.size.x)
63 return false;
64 for (let row of rows)
65 {
66 let sumElts = 0;
67 for (let i=0; i<row.length; i++)
68 {
69 if (V.PIECES.includes(row[i].toLowerCase()))
70 sumElts++;
71 else
72 {
73 const num = parseInt(row[i]);
74 if (isNaN(num))
75 return false;
76 sumElts += num;
77 }
78 }
79 if (sumElts != V.size.y)
80 return false;
81 }
82 // 2) Check flags (if present)
83 if (!!fenParsed.flags && !V.IsGoodFlags(fenParsed.flags))
84 return false;
85 // 3) Check turn (if present)
86 if (!!fenParsed.turn && !["w","b"].includes(fenParsed.turn))
87 return false;
88 // 4) Check enpassant (if present)
89 if (!!fenParsed.enpassant)
90 {
91 const ep = V.SquareToCoords(fenParsed.enpassant);
92 if (ep.y < 0 || ep.y > V.size.y || isNaN(ep.x) || ep.x < 0 || ep.x > V.size.x)
93 return false;
94 }
95 return true;
96 }
97
98 // For FEN checking
99 static IsGoodFlags(flags)
100 {
101 return !!flags.match(/^[01]{4,4}$/);
102 }
103
104 // a4 --> {x:3,y:0}
105 static SquareToCoords(sq)
106 {
107 return {
108 x: V.size.x - parseInt(sq.substr(1)),
109 y: sq[0].charCodeAt() - 97
110 };
111 }
112
113 // {x:0,y:4} --> e8
114 static CoordsToSquare(coords)
115 {
116 return String.fromCharCode(97 + coords.y) + (V.size.x - coords.x);
117 }
118
119 // Aggregates flags into one object
120 aggregateFlags()
121 {
122 return this.castleFlags;
123 }
124
125 // Reverse operation
126 disaggregateFlags(flags)
127 {
128 this.castleFlags = flags;
129 }
130
131 // En-passant square, if any
132 getEpSquare(moveOrSquare)
133 {
134 if (!moveOrSquare)
135 return undefined;
136 if (typeof moveOrSquare === "string")
137 {
138 const square = moveOrSquare;
139 if (square == "-")
140 return undefined;
141 return {
142 x: square[0].charCodeAt()-97,
143 y: V.size.x-parseInt(square[1])
144 };
145 }
146 // Argument is a move:
147 const move = moveOrSquare;
148 const [sx,sy,ex] = [move.start.x,move.start.y,move.end.x];
149 if (this.getPiece(sx,sy) == V.PAWN && Math.abs(sx - ex) == 2)
150 {
151 return {
152 x: (sx + ex)/2,
153 y: sy
154 };
155 }
156 return undefined; //default
157 }
158
159 // Can thing on square1 take thing on square2
160 canTake([x1,y1], [x2,y2])
161 {
162 return this.getColor(x1,y1) !== this.getColor(x2,y2);
163 }
164
165 // Is (x,y) on the chessboard?
166 static OnBoard(x,y)
167 {
168 return (x>=0 && x<V.size.x && y>=0 && y<V.size.y);
169 }
170
171 // Used in interface: 'side' arg == player color
172 canIplay(side, [x,y])
173 {
174 return (this.turn == side && this.getColor(x,y) == side);
175 }
176
177 // On which squares is opponent under check after our move ? (for interface)
178 getCheckSquares(move)
179 {
180 this.play(move);
181 const color = this.turn; //opponent
182 let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)])
183 ? [JSON.parse(JSON.stringify(this.kingPos[color]))] //need to duplicate!
184 : [];
185 this.undo(move);
186 return res;
187 }
188
189 /////////////
190 // FEN UTILS
191
192 // Setup the initial random (assymetric) position
193 static GenRandInitFen()
194 {
195 let pieces = { "w": new Array(8), "b": new Array(8) };
196 // Shuffle pieces on first and last rank
197 for (let c of ["w","b"])
198 {
199 let positions = _.range(8);
200
201 // Get random squares for bishops
202 let randIndex = 2 * _.random(3);
203 let bishop1Pos = positions[randIndex];
204 // The second bishop must be on a square of different color
205 let randIndex_tmp = 2 * _.random(3) + 1;
206 let bishop2Pos = positions[randIndex_tmp];
207 // Remove chosen squares
208 positions.splice(Math.max(randIndex,randIndex_tmp), 1);
209 positions.splice(Math.min(randIndex,randIndex_tmp), 1);
210
211 // Get random squares for knights
212 randIndex = _.random(5);
213 let knight1Pos = positions[randIndex];
214 positions.splice(randIndex, 1);
215 randIndex = _.random(4);
216 let knight2Pos = positions[randIndex];
217 positions.splice(randIndex, 1);
218
219 // Get random square for queen
220 randIndex = _.random(3);
221 let queenPos = positions[randIndex];
222 positions.splice(randIndex, 1);
223
224 // Rooks and king positions are now fixed, because of the ordering rook-king-rook
225 let rook1Pos = positions[0];
226 let kingPos = positions[1];
227 let rook2Pos = positions[2];
228
229 // Finally put the shuffled pieces in the board array
230 pieces[c][rook1Pos] = 'r';
231 pieces[c][knight1Pos] = 'n';
232 pieces[c][bishop1Pos] = 'b';
233 pieces[c][queenPos] = 'q';
234 pieces[c][kingPos] = 'k';
235 pieces[c][bishop2Pos] = 'b';
236 pieces[c][knight2Pos] = 'n';
237 pieces[c][rook2Pos] = 'r';
238 }
239 return pieces["b"].join("") +
240 "/pppppppp/8/8/8/8/PPPPPPPP/" +
241 pieces["w"].join("").toUpperCase() +
242 " w 1111 -"; //add turn + flags + enpassant
243 }
244
245 // "Parse" FEN: just return untransformed string data
246 static ParseFen(fen)
247 {
248 const fenParts = fen.split(" ");
249 return {
250 position: fenParts[0],
251 turn: fenParts[1],
252 flags: fenParts[2],
253 enpassant: fenParts[3],
254 };
255 }
256
257 // Return current fen (game state)
258 getFen()
259 {
260 return this.getBaseFen() + " " + this.turn + " " +
261 this.getFlagsFen() + " " + this.getEnpassantFen();
262 }
263
264 // Position part of the FEN string
265 getBaseFen()
266 {
267 let position = "";
268 for (let i=0; i<V.size.x; i++)
269 {
270 let emptyCount = 0;
271 for (let j=0; j<V.size.y; j++)
272 {
273 if (this.board[i][j] == V.EMPTY)
274 emptyCount++;
275 else
276 {
277 if (emptyCount > 0)
278 {
279 // Add empty squares in-between
280 position += emptyCount;
281 emptyCount = 0;
282 }
283 fen += V.board2fen(this.board[i][j]);
284 }
285 }
286 if (emptyCount > 0)
287 {
288 // "Flush remainder"
289 position += emptyCount;
290 }
291 if (i < V.size.x - 1)
292 position += "/"; //separate rows
293 }
294 return position;
295 }
296
297 // Flags part of the FEN string
298 getFlagsFen()
299 {
300 let flags = "";
301 // Add castling flags
302 for (let i of ['w','b'])
303 {
304 for (let j=0; j<2; j++)
305 flags += (this.castleFlags[i][j] ? '1' : '0');
306 }
307 return flags;
308 }
309
310 // Enpassant part of the FEN string
311 getEnpassantFen()
312 {
313 const L = this.epSquares.length;
314 if (L == 0)
315 return "-"; //no en-passant
316 return V.CoordsToSquare(this.epSquares[L-1]);
317 }
318
319 // Turn position fen into double array ["wb","wp","bk",...]
320 static GetBoard(position)
321 {
322 const rows = position.split("/");
323 let board = doubleArray(V.size.x, V.size.y, "");
324 for (let i=0; i<rows.length; i++)
325 {
326 let j = 0;
327 for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++)
328 {
329 const character = rows[i][indexInRow];
330 const num = parseInt(character);
331 if (!isNaN(num))
332 j += num; //just shift j
333 else //something at position i,j
334 board[i][j++] = V.fen2board(character);
335 }
336 }
337 return board;
338 }
339
340 // Extract (relevant) flags from fen
341 setFlags(fenflags)
342 {
343 // white a-castle, h-castle, black a-castle, h-castle
344 this.castleFlags = {'w': [true,true], 'b': [true,true]};
345 if (!fenflags)
346 return;
347 for (let i=0; i<4; i++)
348 this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (fenflags.charAt(i) == '1');
349 }
350
351 //////////////////
352 // INITIALIZATION
353
354 // Fen string fully describes the game state
355 constructor(fen, moves)
356 {
357 this.moves = moves;
358 const fenParsed = V.ParseFen(fen);
359 this.board = V.GetBoard(fenParsed.position);
360 this.turn = (fenParsed.turn || "w");
361 this.setOtherVariables(fen);
362 }
363
364 // Some additional variables from FEN (variant dependant)
365 setOtherVariables(fen)
366 {
367 // Set flags and enpassant:
368 const parsedFen = V.ParseFen(fen);
369 this.setFlags(fenParsed.flags);
370 this.epSquares = [ V.SquareToCoords(parsedFen.enpassant) ];
371 // Search for king and rooks positions:
372 this.INIT_COL_KING = {'w':-1, 'b':-1};
373 this.INIT_COL_ROOK = {'w':[-1,-1], 'b':[-1,-1]};
374 this.kingPos = {'w':[-1,-1], 'b':[-1,-1]}; //squares of white and black king
375 const fenRows = parsedFen.position.split("/");
376 for (let i=0; i<fenRows.length; i++)
377 {
378 let k = 0; //column index on board
379 for (let j=0; j<fenRows[i].length; j++)
380 {
381 switch (fenRows[i].charAt(j))
382 {
383 case 'k':
384 this.kingPos['b'] = [i,k];
385 this.INIT_COL_KING['b'] = k;
386 break;
387 case 'K':
388 this.kingPos['w'] = [i,k];
389 this.INIT_COL_KING['w'] = k;
390 break;
391 case 'r':
392 if (this.INIT_COL_ROOK['b'][0] < 0)
393 this.INIT_COL_ROOK['b'][0] = k;
394 else
395 this.INIT_COL_ROOK['b'][1] = k;
396 break;
397 case 'R':
398 if (this.INIT_COL_ROOK['w'][0] < 0)
399 this.INIT_COL_ROOK['w'][0] = k;
400 else
401 this.INIT_COL_ROOK['w'][1] = k;
402 break;
403 default:
404 const num = parseInt(fenRows[i].charAt(j));
405 if (!isNaN(num))
406 k += (num-1);
407 }
408 k++;
409 }
410 }
411 }
412
413 /////////////////////
414 // GETTERS & SETTERS
415
416 static get size()
417 {
418 return {x:8, y:8};
419 }
420
421 // Color of thing on suqare (i,j). 'undefined' if square is empty
422 getColor(i,j)
423 {
424 return this.board[i][j].charAt(0);
425 }
426
427 // Piece type on square (i,j). 'undefined' if square is empty
428 getPiece(i,j)
429 {
430 return this.board[i][j].charAt(1);
431 }
432
433 // Get opponent color
434 getOppCol(color)
435 {
436 return (color=="w" ? "b" : "w");
437 }
438
439 get lastMove()
440 {
441 const L = this.moves.length;
442 return (L>0 ? this.moves[L-1] : null);
443 }
444
445 // Pieces codes (for a clearer code)
446 static get PAWN() { return 'p'; }
447 static get ROOK() { return 'r'; }
448 static get KNIGHT() { return 'n'; }
449 static get BISHOP() { return 'b'; }
450 static get QUEEN() { return 'q'; }
451 static get KING() { return 'k'; }
452
453 // For FEN checking:
454 static get PIECES()
455 {
456 return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING];
457 }
458
459 // Empty square
460 static get EMPTY() { return ""; }
461
462 // Some pieces movements
463 static get steps()
464 {
465 return {
466 'r': [ [-1,0],[1,0],[0,-1],[0,1] ],
467 'n': [ [-1,-2],[-1,2],[1,-2],[1,2],[-2,-1],[-2,1],[2,-1],[2,1] ],
468 'b': [ [-1,-1],[-1,1],[1,-1],[1,1] ],
469 };
470 }
471
472 ////////////////////
473 // MOVES GENERATION
474
475 // All possible moves from selected square (assumption: color is OK)
476 getPotentialMovesFrom([x,y])
477 {
478 switch (this.getPiece(x,y))
479 {
480 case V.PAWN:
481 return this.getPotentialPawnMoves([x,y]);
482 case V.ROOK:
483 return this.getPotentialRookMoves([x,y]);
484 case V.KNIGHT:
485 return this.getPotentialKnightMoves([x,y]);
486 case V.BISHOP:
487 return this.getPotentialBishopMoves([x,y]);
488 case V.QUEEN:
489 return this.getPotentialQueenMoves([x,y]);
490 case V.KING:
491 return this.getPotentialKingMoves([x,y]);
492 }
493 }
494
495 // Build a regular move from its initial and destination squares; tr: transformation
496 getBasicMove([sx,sy], [ex,ey], tr)
497 {
498 let mv = new Move({
499 appear: [
500 new PiPo({
501 x: ex,
502 y: ey,
503 c: !!tr ? tr.c : this.getColor(sx,sy),
504 p: !!tr ? tr.p : this.getPiece(sx,sy)
505 })
506 ],
507 vanish: [
508 new PiPo({
509 x: sx,
510 y: sy,
511 c: this.getColor(sx,sy),
512 p: this.getPiece(sx,sy)
513 })
514 ]
515 });
516
517 // The opponent piece disappears if we take it
518 if (this.board[ex][ey] != V.EMPTY)
519 {
520 mv.vanish.push(
521 new PiPo({
522 x: ex,
523 y: ey,
524 c: this.getColor(ex,ey),
525 p: this.getPiece(ex,ey)
526 })
527 );
528 }
529 return mv;
530 }
531
532 // Generic method to find possible moves of non-pawn pieces ("sliding or jumping")
533 getSlideNJumpMoves([x,y], steps, oneStep)
534 {
535 const color = this.getColor(x,y);
536 let moves = [];
537 outerLoop:
538 for (let step of steps)
539 {
540 let i = x + step[0];
541 let j = y + step[1];
542 while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
543 {
544 moves.push(this.getBasicMove([x,y], [i,j]));
545 if (oneStep !== undefined)
546 continue outerLoop;
547 i += step[0];
548 j += step[1];
549 }
550 if (V.OnBoard(i,j) && this.canTake([x,y], [i,j]))
551 moves.push(this.getBasicMove([x,y], [i,j]));
552 }
553 return moves;
554 }
555
556 // What are the pawn moves from square x,y ?
557 getPotentialPawnMoves([x,y])
558 {
559 const color = this.turn;
560 let moves = [];
561 const [sizeX,sizeY] = [V.size.x,V.size.y];
562 const shift = (color == "w" ? -1 : 1);
563 const firstRank = (color == 'w' ? sizeX-1 : 0);
564 const startRank = (color == "w" ? sizeX-2 : 1);
565 const lastRank = (color == "w" ? 0 : sizeX-1);
566
567 if (x+shift >= 0 && x+shift < sizeX && x+shift != lastRank)
568 {
569 // Normal moves
570 if (this.board[x+shift][y] == V.EMPTY)
571 {
572 moves.push(this.getBasicMove([x,y], [x+shift,y]));
573 // Next condition because variants with pawns on 1st rank allow them to jump
574 if ([startRank,firstRank].includes(x) && this.board[x+2*shift][y] == V.EMPTY)
575 {
576 // Two squares jump
577 moves.push(this.getBasicMove([x,y], [x+2*shift,y]));
578 }
579 }
580 // Captures
581 if (y>0 && this.board[x+shift][y-1] != V.EMPTY
582 && this.canTake([x,y], [x+shift,y-1]))
583 {
584 moves.push(this.getBasicMove([x,y], [x+shift,y-1]));
585 }
586 if (y<sizeY-1 && this.board[x+shift][y+1] != V.EMPTY
587 && this.canTake([x,y], [x+shift,y+1]))
588 {
589 moves.push(this.getBasicMove([x,y], [x+shift,y+1]));
590 }
591 }
592
593 if (x+shift == lastRank)
594 {
595 // Promotion
596 const pawnColor = this.getColor(x,y); //can be different for checkered
597 let promotionPieces = [V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN];
598 promotionPieces.forEach(p => {
599 // Normal move
600 if (this.board[x+shift][y] == V.EMPTY)
601 moves.push(this.getBasicMove([x,y], [x+shift,y], {c:pawnColor,p:p}));
602 // Captures
603 if (y>0 && this.board[x+shift][y-1] != V.EMPTY
604 && this.canTake([x,y], [x+shift,y-1]))
605 {
606 moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:pawnColor,p:p}));
607 }
608 if (y<sizeY-1 && this.board[x+shift][y+1] != V.EMPTY
609 && this.canTake([x,y], [x+shift,y+1]))
610 {
611 moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:pawnColor,p:p}));
612 }
613 });
614 }
615
616 // En passant
617 const Lep = this.epSquares.length;
618 const epSquare = (Lep>0 ? this.epSquares[Lep-1] : undefined);
619 if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1)
620 {
621 const epStep = epSquare.y - y;
622 let enpassantMove = this.getBasicMove([x,y], [x+shift,y+epStep]);
623 enpassantMove.vanish.push({
624 x: x,
625 y: y+epStep,
626 p: 'p',
627 c: this.getColor(x,y+epStep)
628 });
629 moves.push(enpassantMove);
630 }
631
632 return moves;
633 }
634
635 // What are the rook moves from square x,y ?
636 getPotentialRookMoves(sq)
637 {
638 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]);
639 }
640
641 // What are the knight moves from square x,y ?
642 getPotentialKnightMoves(sq)
643 {
644 return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep");
645 }
646
647 // What are the bishop moves from square x,y ?
648 getPotentialBishopMoves(sq)
649 {
650 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]);
651 }
652
653 // What are the queen moves from square x,y ?
654 getPotentialQueenMoves(sq)
655 {
656 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
657 }
658
659 // What are the king moves from square x,y ?
660 getPotentialKingMoves(sq)
661 {
662 // Initialize with normal moves
663 let moves = this.getSlideNJumpMoves(sq,
664 V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
665 return moves.concat(this.getCastleMoves(sq));
666 }
667
668 getCastleMoves([x,y])
669 {
670 const c = this.getColor(x,y);
671 if (x != (c=="w" ? V.size.x-1 : 0) || y != this.INIT_COL_KING[c])
672 return []; //x isn't first rank, or king has moved (shortcut)
673
674 // Castling ?
675 const oppCol = this.getOppCol(c);
676 let moves = [];
677 let i = 0;
678 const finalSquares = [ [2,3], [V.size.y-2,V.size.y-3] ]; //king, then rook
679 castlingCheck:
680 for (let castleSide=0; castleSide < 2; castleSide++) //large, then small
681 {
682 if (!this.castleFlags[c][castleSide])
683 continue;
684 // If this code is reached, rooks and king are on initial position
685
686 // Nothing on the path of the king (and no checks; OK also if y==finalSquare)?
687 let step = finalSquares[castleSide][0] < y ? -1 : 1;
688 for (i=y; i!=finalSquares[castleSide][0]; i+=step)
689 {
690 if (this.isAttacked([x,i], [oppCol]) || (this.board[x][i] != V.EMPTY &&
691 // NOTE: next check is enough, because of chessboard constraints
692 (this.getColor(x,i) != c || ![V.KING,V.ROOK].includes(this.getPiece(x,i)))))
693 {
694 continue castlingCheck;
695 }
696 }
697
698 // Nothing on the path to the rook?
699 step = castleSide == 0 ? -1 : 1;
700 for (i = y + step; i != this.INIT_COL_ROOK[c][castleSide]; i += step)
701 {
702 if (this.board[x][i] != V.EMPTY)
703 continue castlingCheck;
704 }
705 const rookPos = this.INIT_COL_ROOK[c][castleSide];
706
707 // Nothing on final squares, except maybe king and castling rook?
708 for (i=0; i<2; i++)
709 {
710 if (this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
711 this.getPiece(x,finalSquares[castleSide][i]) != V.KING &&
712 finalSquares[castleSide][i] != rookPos)
713 {
714 continue castlingCheck;
715 }
716 }
717
718 // If this code is reached, castle is valid
719 moves.push( new Move({
720 appear: [
721 new PiPo({x:x,y:finalSquares[castleSide][0],p:V.KING,c:c}),
722 new PiPo({x:x,y:finalSquares[castleSide][1],p:V.ROOK,c:c})],
723 vanish: [
724 new PiPo({x:x,y:y,p:V.KING,c:c}),
725 new PiPo({x:x,y:rookPos,p:V.ROOK,c:c})],
726 end: Math.abs(y - rookPos) <= 2
727 ? {x:x, y:rookPos}
728 : {x:x, y:y + 2 * (castleSide==0 ? -1 : 1)}
729 }) );
730 }
731
732 return moves;
733 }
734
735 ////////////////////
736 // MOVES VALIDATION
737
738 getPossibleMovesFrom(sq)
739 {
740 // Assuming color is right (already checked)
741 return this.filterValid( this.getPotentialMovesFrom(sq) );
742 }
743
744 // TODO: promotions (into R,B,N,Q) should be filtered only once
745 filterValid(moves)
746 {
747 if (moves.length == 0)
748 return [];
749 return moves.filter(m => { return !this.underCheck(m); });
750 }
751
752 // Search for all valid moves considering current turn (for engine and game end)
753 getAllValidMoves()
754 {
755 const color = this.turn;
756 const oppCol = this.getOppCol(color);
757 let potentialMoves = [];
758 for (let i=0; i<V.size.x; i++)
759 {
760 for (let j=0; j<V.size.y; j++)
761 {
762 // Next condition "!= oppCol" = harmless hack to work with checkered variant
763 if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol)
764 Array.prototype.push.apply(potentialMoves, this.getPotentialMovesFrom([i,j]));
765 }
766 }
767 // NOTE: prefer lazy undercheck tests, letting the king being taken?
768 // No: if happen on last 1/2 move, could lead to forbidden moves, wrong evals
769 return this.filterValid(potentialMoves);
770 }
771
772 // Stop at the first move found
773 atLeastOneMove()
774 {
775 const color = this.turn;
776 const oppCol = this.getOppCol(color);
777 for (let i=0; i<V.size.x; i++)
778 {
779 for (let j=0; j<V.size.y; j++)
780 {
781 if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol)
782 {
783 const moves = this.getPotentialMovesFrom([i,j]);
784 if (moves.length > 0)
785 {
786 for (let k=0; k<moves.length; k++)
787 {
788 if (this.filterValid([moves[k]]).length > 0)
789 return true;
790 }
791 }
792 }
793 }
794 }
795 return false;
796 }
797
798 // Check if pieces of color in array 'colors' are attacking (king) on square x,y
799 isAttacked(sq, colors)
800 {
801 return (this.isAttackedByPawn(sq, colors)
802 || this.isAttackedByRook(sq, colors)
803 || this.isAttackedByKnight(sq, colors)
804 || this.isAttackedByBishop(sq, colors)
805 || this.isAttackedByQueen(sq, colors)
806 || this.isAttackedByKing(sq, colors));
807 }
808
809 // Is square x,y attacked by 'colors' pawns ?
810 isAttackedByPawn([x,y], colors)
811 {
812 for (let c of colors)
813 {
814 let pawnShift = (c=="w" ? 1 : -1);
815 if (x+pawnShift>=0 && x+pawnShift<V.size.x)
816 {
817 for (let i of [-1,1])
818 {
819 if (y+i>=0 && y+i<V.size.y && this.getPiece(x+pawnShift,y+i)==V.PAWN
820 && this.getColor(x+pawnShift,y+i)==c)
821 {
822 return true;
823 }
824 }
825 }
826 }
827 return false;
828 }
829
830 // Is square x,y attacked by 'colors' rooks ?
831 isAttackedByRook(sq, colors)
832 {
833 return this.isAttackedBySlideNJump(sq, colors, V.ROOK, V.steps[V.ROOK]);
834 }
835
836 // Is square x,y attacked by 'colors' knights ?
837 isAttackedByKnight(sq, colors)
838 {
839 return this.isAttackedBySlideNJump(sq, colors,
840 V.KNIGHT, V.steps[V.KNIGHT], "oneStep");
841 }
842
843 // Is square x,y attacked by 'colors' bishops ?
844 isAttackedByBishop(sq, colors)
845 {
846 return this.isAttackedBySlideNJump(sq, colors, V.BISHOP, V.steps[V.BISHOP]);
847 }
848
849 // Is square x,y attacked by 'colors' queens ?
850 isAttackedByQueen(sq, colors)
851 {
852 return this.isAttackedBySlideNJump(sq, colors, V.QUEEN,
853 V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
854 }
855
856 // Is square x,y attacked by 'colors' king(s) ?
857 isAttackedByKing(sq, colors)
858 {
859 return this.isAttackedBySlideNJump(sq, colors, V.KING,
860 V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
861 }
862
863 // Generic method for non-pawn pieces ("sliding or jumping"):
864 // is x,y attacked by a piece of color in array 'colors' ?
865 isAttackedBySlideNJump([x,y], colors, piece, steps, oneStep)
866 {
867 for (let step of steps)
868 {
869 let rx = x+step[0], ry = y+step[1];
870 while (V.OnBoard(rx,ry) && this.board[rx][ry] == V.EMPTY && !oneStep)
871 {
872 rx += step[0];
873 ry += step[1];
874 }
875 if (V.OnBoard(rx,ry) && this.getPiece(rx,ry) === piece
876 && colors.includes(this.getColor(rx,ry)))
877 {
878 return true;
879 }
880 }
881 return false;
882 }
883
884 // Is current player under check after his move ?
885 underCheck(move)
886 {
887 const color = this.turn;
888 this.play(move);
889 let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
890 this.undo(move);
891 return res;
892 }
893
894 /////////////////
895 // MOVES PLAYING
896
897 // Apply a move on board
898 static PlayOnBoard(board, move)
899 {
900 for (let psq of move.vanish)
901 board[psq.x][psq.y] = V.EMPTY;
902 for (let psq of move.appear)
903 board[psq.x][psq.y] = psq.c + psq.p;
904 }
905 // Un-apply the played move
906 static UndoOnBoard(board, move)
907 {
908 for (let psq of move.appear)
909 board[psq.x][psq.y] = V.EMPTY;
910 for (let psq of move.vanish)
911 board[psq.x][psq.y] = psq.c + psq.p;
912 }
913
914 // Before move is played, update variables + flags
915 updateVariables(move)
916 {
917 const piece = this.getPiece(move.start.x,move.start.y);
918 const c = this.turn;
919 const firstRank = (c == "w" ? V.size.x-1 : 0);
920
921 // Update king position + flags
922 if (piece == V.KING && move.appear.length > 0)
923 {
924 this.kingPos[c][0] = move.appear[0].x;
925 this.kingPos[c][1] = move.appear[0].y;
926 this.castleFlags[c] = [false,false];
927 return;
928 }
929 const oppCol = this.getOppCol(c);
930 const oppFirstRank = (V.size.x-1) - firstRank;
931 if (move.start.x == firstRank //our rook moves?
932 && this.INIT_COL_ROOK[c].includes(move.start.y))
933 {
934 const flagIdx = (move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1);
935 this.castleFlags[c][flagIdx] = false;
936 }
937 else if (move.end.x == oppFirstRank //we took opponent rook?
938 && this.INIT_COL_ROOK[oppCol].includes(move.end.y))
939 {
940 const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1);
941 this.castleFlags[oppCol][flagIdx] = false;
942 }
943 }
944
945 // After move is undo-ed *and flags resetted*, un-update other variables
946 // TODO: more symmetry, by storing flags increment in move (?!)
947 unupdateVariables(move)
948 {
949 // (Potentially) Reset king position
950 const c = this.getColor(move.start.x,move.start.y);
951 if (this.getPiece(move.start.x,move.start.y) == V.KING)
952 this.kingPos[c] = [move.start.x, move.start.y];
953 }
954
955 play(move, ingame)
956 {
957 // DEBUG:
958 // if (!this.states) this.states = [];
959 // if (!ingame) this.states.push(this.getFen());
960
961 if (!!ingame)
962 move.notation = [this.getNotation(move), this.getLongNotation(move)];
963
964 move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
965 this.updateVariables(move);
966 this.moves.push(move);
967 this.epSquares.push( this.getEpSquare(move) );
968 this.turn = this.getOppCol(this.turn);
969 V.PlayOnBoard(this.board, move);
970
971 if (!!ingame)
972 {
973 // Hash of current game state *after move*, to detect repetitions
974 move.hash = hex_md5(this.getFen();
975 }
976 }
977
978 undo(move)
979 {
980 V.UndoOnBoard(this.board, move);
981 this.turn = this.getOppCol(this.turn);
982 this.epSquares.pop();
983 this.moves.pop();
984 this.unupdateVariables(move);
985 this.disaggregateFlags(JSON.parse(move.flags));
986
987 // DEBUG:
988 // if (this.getFen() != this.states[this.states.length-1])
989 // debugger;
990 // this.states.pop();
991 }
992
993 ///////////////
994 // END OF GAME
995
996 // Check for 3 repetitions (position + flags + turn)
997 checkRepetition()
998 {
999 if (!this.hashStates)
1000 this.hashStates = {};
1001 const startIndex =
1002 Object.values(this.hashStates).reduce((a,b) => { return a+b; }, 0)
1003 // Update this.hashStates with last move (or all moves if continuation)
1004 // NOTE: redundant storage, but faster and moderate size
1005 for (let i=startIndex; i<this.moves.length; i++)
1006 {
1007 const move = this.moves[i];
1008 if (!this.hashStates[move.hash])
1009 this.hashStates[move.hash] = 1;
1010 else
1011 this.hashStates[move.hash]++;
1012 }
1013 return Object.values(this.hashStates).some(elt => { return (elt >= 3); });
1014 }
1015
1016 // Is game over ? And if yes, what is the score ?
1017 checkGameOver()
1018 {
1019 if (this.checkRepetition())
1020 return "1/2";
1021
1022 if (this.atLeastOneMove()) // game not over
1023 return "*";
1024
1025 // Game over
1026 return this.checkGameEnd();
1027 }
1028
1029 // No moves are possible: compute score
1030 checkGameEnd()
1031 {
1032 const color = this.turn;
1033 // No valid move: stalemate or checkmate?
1034 if (!this.isAttacked(this.kingPos[color], [this.getOppCol(color)]))
1035 return "1/2";
1036 // OK, checkmate
1037 return color == "w" ? "0-1" : "1-0";
1038 }
1039
1040 ///////////////
1041 // ENGINE PLAY
1042
1043 // Pieces values
1044 static get VALUES()
1045 {
1046 return {
1047 'p': 1,
1048 'r': 5,
1049 'n': 3,
1050 'b': 3,
1051 'q': 9,
1052 'k': 1000
1053 };
1054 }
1055
1056 // "Checkmate" (unreachable eval)
1057 static get INFINITY() { return 9999; }
1058
1059 // At this value or above, the game is over
1060 static get THRESHOLD_MATE() { return V.INFINITY; }
1061
1062 // Search depth: 2 for high branching factor, 4 for small (Loser chess, eg.)
1063 static get SEARCH_DEPTH() { return 3; }
1064
1065 // Assumption: at least one legal move
1066 // NOTE: works also for extinction chess because depth is 3...
1067 getComputerMove()
1068 {
1069 const maxeval = V.INFINITY;
1070 const color = this.turn;
1071 // Some variants may show a bigger moves list to the human (Switching),
1072 // thus the argument "computer" below (which is generally ignored)
1073 let moves1 = this.getAllValidMoves("computer");
1074
1075 // Can I mate in 1 ? (for Magnetic & Extinction)
1076 for (let i of _.shuffle(_.range(moves1.length)))
1077 {
1078 this.play(moves1[i]);
1079 let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE);
1080 if (!finish && !this.atLeastOneMove())
1081 {
1082 // Test mate (for other variants)
1083 const score = this.checkGameEnd();
1084 if (score != "1/2")
1085 finish = true;
1086 }
1087 this.undo(moves1[i]);
1088 if (finish)
1089 return moves1[i];
1090 }
1091
1092 // Rank moves using a min-max at depth 2
1093 for (let i=0; i<moves1.length; i++)
1094 {
1095 moves1[i].eval = (color=="w" ? -1 : 1) * maxeval; //very low, I'm checkmated
1096 this.play(moves1[i]);
1097 let eval2 = undefined;
1098 if (this.atLeastOneMove())
1099 {
1100 eval2 = (color=="w" ? 1 : -1) * maxeval; //initialized with checkmate value
1101 // Second half-move:
1102 let moves2 = this.getAllValidMoves("computer");
1103 for (let j=0; j<moves2.length; j++)
1104 {
1105 this.play(moves2[j]);
1106 let evalPos = undefined;
1107 if (this.atLeastOneMove())
1108 evalPos = this.evalPosition()
1109 else
1110 {
1111 // Working with scores is more accurate (necessary for Loser variant)
1112 const score = this.checkGameEnd();
1113 evalPos = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
1114 }
1115 if ((color == "w" && evalPos < eval2) || (color=="b" && evalPos > eval2))
1116 eval2 = evalPos;
1117 this.undo(moves2[j]);
1118 }
1119 }
1120 else
1121 {
1122 const score = this.checkGameEnd();
1123 eval2 = (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
1124 }
1125 if ((color=="w" && eval2 > moves1[i].eval)
1126 || (color=="b" && eval2 < moves1[i].eval))
1127 {
1128 moves1[i].eval = eval2;
1129 }
1130 this.undo(moves1[i]);
1131 }
1132 moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
1133
1134 let candidates = [0]; //indices of candidates moves
1135 for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
1136 candidates.push(j);
1137 let currentBest = moves1[_.sample(candidates, 1)];
1138
1139 // From here, depth >= 3: may take a while, so we control time
1140 const timeStart = Date.now();
1141
1142 // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...)
1143 if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE)
1144 {
1145 for (let i=0; i<moves1.length; i++)
1146 {
1147 if (Date.now()-timeStart >= 5000) //more than 5 seconds
1148 return currentBest; //depth 2 at least
1149 this.play(moves1[i]);
1150 // 0.1 * oldEval : heuristic to avoid some bad moves (not all...)
1151 moves1[i].eval = 0.1*moves1[i].eval +
1152 this.alphabeta(V.SEARCH_DEPTH-1, -maxeval, maxeval);
1153 this.undo(moves1[i]);
1154 }
1155 moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
1156 }
1157 else
1158 return currentBest;
1159 //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
1160
1161 candidates = [0];
1162 for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
1163 candidates.push(j);
1164 return moves1[_.sample(candidates, 1)];
1165 }
1166
1167 alphabeta(depth, alpha, beta)
1168 {
1169 const maxeval = V.INFINITY;
1170 const color = this.turn;
1171 if (!this.atLeastOneMove())
1172 {
1173 switch (this.checkGameEnd())
1174 {
1175 case "1/2":
1176 return 0;
1177 default:
1178 const score = this.checkGameEnd();
1179 return (score=="1/2" ? 0 : (score=="1-0" ? 1 : -1) * maxeval);
1180 }
1181 }
1182 if (depth == 0)
1183 return this.evalPosition();
1184 const moves = this.getAllValidMoves("computer");
1185 let v = color=="w" ? -maxeval : maxeval;
1186 if (color == "w")
1187 {
1188 for (let i=0; i<moves.length; i++)
1189 {
1190 this.play(moves[i]);
1191 v = Math.max(v, this.alphabeta(depth-1, alpha, beta));
1192 this.undo(moves[i]);
1193 alpha = Math.max(alpha, v);
1194 if (alpha >= beta)
1195 break; //beta cutoff
1196 }
1197 }
1198 else //color=="b"
1199 {
1200 for (let i=0; i<moves.length; i++)
1201 {
1202 this.play(moves[i]);
1203 v = Math.min(v, this.alphabeta(depth-1, alpha, beta));
1204 this.undo(moves[i]);
1205 beta = Math.min(beta, v);
1206 if (alpha >= beta)
1207 break; //alpha cutoff
1208 }
1209 }
1210 return v;
1211 }
1212
1213 evalPosition()
1214 {
1215 let evaluation = 0;
1216 // Just count material for now
1217 for (let i=0; i<V.size.x; i++)
1218 {
1219 for (let j=0; j<V.size.y; j++)
1220 {
1221 if (this.board[i][j] != V.EMPTY)
1222 {
1223 const sign = this.getColor(i,j) == "w" ? 1 : -1;
1224 evaluation += sign * V.VALUES[this.getPiece(i,j)];
1225 }
1226 }
1227 }
1228 return evaluation;
1229 }
1230
1231 /////////////////////////
1232 // MOVES + GAME NOTATION
1233 /////////////////////////
1234
1235 // Context: just before move is played, turn hasn't changed
1236 getNotation(move)
1237 {
1238 if (move.appear.length == 2 && move.appear[0].p == V.KING) //castle
1239 return (move.end.y < move.start.y ? "0-0-0" : "0-0");
1240
1241 // Translate final square
1242 const finalSquare = V.CoordsToSquare(move.end);
1243
1244 const piece = this.getPiece(move.start.x, move.start.y);
1245 if (piece == V.PAWN)
1246 {
1247 // Pawn move
1248 let notation = "";
1249 if (move.vanish.length > move.appear.length)
1250 {
1251 // Capture
1252 const startColumn = String.fromCharCode(97 + move.start.y);
1253 notation = startColumn + "x" + finalSquare;
1254 }
1255 else //no capture
1256 notation = finalSquare;
1257 if (move.appear.length > 0 && piece != move.appear[0].p) //promotion
1258 notation += "=" + move.appear[0].p.toUpperCase();
1259 return notation;
1260 }
1261
1262 else
1263 {
1264 // Piece movement
1265 return piece.toUpperCase() +
1266 (move.vanish.length > move.appear.length ? "x" : "") + finalSquare;
1267 }
1268 }
1269
1270 // Complete the usual notation, may be required for de-ambiguification
1271 getLongNotation(move)
1272 {
1273 // Not encoding move. But short+long is enough
1274 return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
1275 }
1276
1277 // The score is already computed when calling this function
1278 getPGN(mycolor, score, fenStart, mode)
1279 {
1280 let pgn = "";
1281 pgn += '[Site "vchess.club"]<br>';
1282 const opponent = mode=="human" ? "Anonymous" : "Computer";
1283 pgn += '[Variant "' + variant + '"]<br>';
1284 pgn += '[Date "' + getDate(new Date()) + '"]<br>';
1285 pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]<br>';
1286 pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]<br>';
1287 pgn += '[FenStart "' + fenStart + '"]<br>';
1288 pgn += '[Fen "' + this.getFen() + '"]<br>';
1289 pgn += '[Result "' + score + '"]<br><br>';
1290
1291 // Standard PGN
1292 for (let i=0; i<this.moves.length; i++)
1293 {
1294 if (i % 2 == 0)
1295 pgn += ((i/2)+1) + ".";
1296 pgn += this.moves[i].notation[0] + " ";
1297 }
1298 pgn += "<br><br>";
1299
1300 // "Complete moves" PGN (helping in ambiguous cases)
1301 for (let i=0; i<this.moves.length; i++)
1302 {
1303 if (i % 2 == 0)
1304 pgn += ((i/2)+1) + ".";
1305 pgn += this.moves[i].notation[1] + " ";
1306 }
1307
1308 return pgn;
1309 }
1310 }