Save current state (unfinished, untested)
[vchess.git] / public / javascripts / base_rules.js
CommitLineData
92342261
BA
1// (Orthodox) Chess rules are defined in ChessRules class.
2// Variants generally inherit from it, and modify some parts.
3
1d184b4c
BA
4class 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
098e8468 16// TODO: for animation, moves should contains "moving" and "fading" maybe...
1d184b4c
BA
17class 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)
32class ChessRules
33{
1a788978
BA
34 //////////////
35 // MISC UTILS
36
1d184b4c
BA
37 // Path to pieces
38 static getPpath(b)
39 {
40 return b; //usual pieces in pieces/ folder
41 }
1a788978 42
1d184b4c
BA
43 // Turn "wb" into "B" (for FEN)
44 static board2fen(b)
45 {
46 return b[0]=='w' ? b[1].toUpperCase() : b[1];
47 }
1a788978 48
1d184b4c
BA
49 // Turn "p" into "bp" (for board)
50 static fen2board(f)
51 {
52 return f.charCodeAt()<=90 ? "w"+f.toLowerCase() : "b"+f;
53 }
54
7931e479
BA
55 // Check if FEN describe a position
56 static IsGoodFen(fen)
57 {
1a788978 58 const fenParsed = V.ParseFen(fen);
7931e479 59 // 1) Check position
1a788978 60 const position = fenParsed.position;
7931e479
BA
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)
1a788978
BA
83 if (!!fenParsed.flags && !V.IsGoodFlags(fenParsed.flags))
84 return false;
7931e479 85 // 3) Check turn (if present)
1a788978
BA
86 if (!!fenParsed.turn && !["w","b"].includes(fenParsed.turn))
87 return false;
88 // 4) Check enpassant (if present)
89 if (!!fenParsed.enpassant)
7931e479 90 {
1a788978
BA
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)
7931e479
BA
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
1a788978
BA
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)
1d184b4c 321 {
1a788978 322 const rows = position.split("/");
0b7d99ec 323 let board = doubleArray(V.size.x, V.size.y, "");
1d184b4c
BA
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 {
7931e479
BA
329 const character = rows[i][indexInRow];
330 const num = parseInt(character);
1d184b4c
BA
331 if (!isNaN(num))
332 j += num; //just shift j
333 else //something at position i,j
0b7d99ec 334 board[i][j++] = V.fen2board(character);
1d184b4c
BA
335 }
336 }
337 return board;
338 }
339
dda21a71 340 // Extract (relevant) flags from fen
7931e479 341 setFlags(fenflags)
1d184b4c
BA
342 {
343 // white a-castle, h-castle, black a-castle, h-castle
7931e479
BA
344 this.castleFlags = {'w': [true,true], 'b': [true,true]};
345 if (!fenflags)
346 return;
1d184b4c 347 for (let i=0; i<4; i++)
7931e479
BA
348 this.castleFlags[i < 2 ? 'w' : 'b'][i%2] = (fenflags.charAt(i) == '1');
349 }
350
1a788978
BA
351 //////////////////
352 // INITIALIZATION
353
354 // Fen string fully describes the game state
355 constructor(fen, moves)
7931e479 356 {
1a788978
BA
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);
1d184b4c
BA
362 }
363
1a788978
BA
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 }
1d184b4c 420
1a788978
BA
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 }
0b7d99ec 426
1a788978
BA
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 }
1d184b4c 432
1a788978
BA
433 // Get opponent color
434 getOppCol(color)
435 {
436 return (color=="w" ? "b" : "w");
437 }
1d184b4c 438
1a788978
BA
439 get lastMove()
440 {
1d184b4c 441 const L = this.moves.length;
0b7d99ec 442 return (L>0 ? this.moves[L-1] : null);
1d184b4c 443 }
0b7d99ec 444
1a788978 445 // Pieces codes (for a clearer code)
1d184b4c
BA
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
7931e479 453 // For FEN checking:
1a788978
BA
454 static get PIECES()
455 {
7931e479
BA
456 return [V.PAWN,V.ROOK,V.KNIGHT,V.BISHOP,V.QUEEN,V.KING];
457 }
458
1d184b4c 459 // Empty square
1a788978 460 static get EMPTY() { return ""; }
1d184b4c
BA
461
462 // Some pieces movements
1a788978
BA
463 static get steps()
464 {
1d184b4c
BA
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] ],
1d184b4c
BA
469 };
470 }
471
1a788978 472 ////////////////////
1d184b4c
BA
473 // MOVES GENERATION
474
475 // All possible moves from selected square (assumption: color is OK)
476 getPotentialMovesFrom([x,y])
477 {
1d184b4c
BA
478 switch (this.getPiece(x,y))
479 {
0b7d99ec 480 case V.PAWN:
46302e64 481 return this.getPotentialPawnMoves([x,y]);
0b7d99ec 482 case V.ROOK:
46302e64 483 return this.getPotentialRookMoves([x,y]);
0b7d99ec 484 case V.KNIGHT:
46302e64 485 return this.getPotentialKnightMoves([x,y]);
0b7d99ec 486 case V.BISHOP:
46302e64 487 return this.getPotentialBishopMoves([x,y]);
0b7d99ec 488 case V.QUEEN:
46302e64 489 return this.getPotentialQueenMoves([x,y]);
0b7d99ec 490 case V.KING:
46302e64 491 return this.getPotentialKingMoves([x,y]);
1d184b4c
BA
492 }
493 }
494
495 // Build a regular move from its initial and destination squares; tr: transformation
46302e64 496 getBasicMove([sx,sy], [ex,ey], tr)
1d184b4c 497 {
2526c041 498 let mv = new Move({
1d184b4c
BA
499 appear: [
500 new PiPo({
501 x: ex,
502 y: ey,
46302e64
BA
503 c: !!tr ? tr.c : this.getColor(sx,sy),
504 p: !!tr ? tr.p : this.getPiece(sx,sy)
1d184b4c
BA
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
0b7d99ec 518 if (this.board[ex][ey] != V.EMPTY)
1d184b4c
BA
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")
46302e64 533 getSlideNJumpMoves([x,y], steps, oneStep)
1d184b4c 534 {
46302e64 535 const color = this.getColor(x,y);
2526c041 536 let moves = [];
1d184b4c
BA
537 outerLoop:
538 for (let step of steps)
539 {
46302e64
BA
540 let i = x + step[0];
541 let j = y + step[1];
0b7d99ec 542 while (V.OnBoard(i,j) && this.board[i][j] == V.EMPTY)
1d184b4c 543 {
46302e64 544 moves.push(this.getBasicMove([x,y], [i,j]));
1d184b4c
BA
545 if (oneStep !== undefined)
546 continue outerLoop;
547 i += step[0];
548 j += step[1];
549 }
0b7d99ec 550 if (V.OnBoard(i,j) && this.canTake([x,y], [i,j]))
46302e64 551 moves.push(this.getBasicMove([x,y], [i,j]));
1d184b4c
BA
552 }
553 return moves;
554 }
555
dda21a71 556 // What are the pawn moves from square x,y ?
46302e64 557 getPotentialPawnMoves([x,y])
1d184b4c 558 {
2526c041
BA
559 const color = this.turn;
560 let moves = [];
0b7d99ec 561 const [sizeX,sizeY] = [V.size.x,V.size.y];
2526c041 562 const shift = (color == "w" ? -1 : 1);
cf130369
BA
563 const firstRank = (color == 'w' ? sizeX-1 : 0);
564 const startRank = (color == "w" ? sizeX-2 : 1);
565 const lastRank = (color == "w" ? 0 : sizeX-1);
1d184b4c
BA
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 {
46302e64 572 moves.push(this.getBasicMove([x,y], [x+shift,y]));
1221ac47 573 // Next condition because variants with pawns on 1st rank allow them to jump
2526c041 574 if ([startRank,firstRank].includes(x) && this.board[x+2*shift][y] == V.EMPTY)
1d184b4c
BA
575 {
576 // Two squares jump
46302e64 577 moves.push(this.getBasicMove([x,y], [x+2*shift,y]));
1d184b4c
BA
578 }
579 }
580 // Captures
0b7d99ec
BA
581 if (y>0 && this.board[x+shift][y-1] != V.EMPTY
582 && this.canTake([x,y], [x+shift,y-1]))
1221ac47 583 {
46302e64 584 moves.push(this.getBasicMove([x,y], [x+shift,y-1]));
1221ac47 585 }
0b7d99ec
BA
586 if (y<sizeY-1 && this.board[x+shift][y+1] != V.EMPTY
587 && this.canTake([x,y], [x+shift,y+1]))
1221ac47 588 {
46302e64 589 moves.push(this.getBasicMove([x,y], [x+shift,y+1]));
1221ac47 590 }
1d184b4c
BA
591 }
592
593 if (x+shift == lastRank)
594 {
595 // Promotion
68f5ccc8 596 const pawnColor = this.getColor(x,y); //can be different for checkered
1d184b4c
BA
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)
68f5ccc8 601 moves.push(this.getBasicMove([x,y], [x+shift,y], {c:pawnColor,p:p}));
1d184b4c 602 // Captures
0b7d99ec
BA
603 if (y>0 && this.board[x+shift][y-1] != V.EMPTY
604 && this.canTake([x,y], [x+shift,y-1]))
1221ac47 605 {
68f5ccc8 606 moves.push(this.getBasicMove([x,y], [x+shift,y-1], {c:pawnColor,p:p}));
1221ac47 607 }
0b7d99ec
BA
608 if (y<sizeY-1 && this.board[x+shift][y+1] != V.EMPTY
609 && this.canTake([x,y], [x+shift,y+1]))
1221ac47 610 {
68f5ccc8 611 moves.push(this.getBasicMove([x,y], [x+shift,y+1], {c:pawnColor,p:p}));
1221ac47 612 }
1d184b4c
BA
613 });
614 }
615
616 // En passant
617 const Lep = this.epSquares.length;
0b7d99ec 618 const epSquare = (Lep>0 ? this.epSquares[Lep-1] : undefined);
1d184b4c
BA
619 if (!!epSquare && epSquare.x == x+shift && Math.abs(epSquare.y - y) == 1)
620 {
0b7d99ec
BA
621 const epStep = epSquare.y - y;
622 let enpassantMove = this.getBasicMove([x,y], [x+shift,y+epStep]);
1d184b4c
BA
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 ?
46302e64 636 getPotentialRookMoves(sq)
1d184b4c 637 {
0b7d99ec 638 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]);
1d184b4c
BA
639 }
640
641 // What are the knight moves from square x,y ?
46302e64 642 getPotentialKnightMoves(sq)
1d184b4c 643 {
0b7d99ec 644 return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep");
1d184b4c
BA
645 }
646
647 // What are the bishop moves from square x,y ?
46302e64 648 getPotentialBishopMoves(sq)
1d184b4c 649 {
0b7d99ec 650 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]);
1d184b4c
BA
651 }
652
653 // What are the queen moves from square x,y ?
46302e64 654 getPotentialQueenMoves(sq)
1d184b4c 655 {
a37076f1 656 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
1d184b4c
BA
657 }
658
659 // What are the king moves from square x,y ?
46302e64 660 getPotentialKingMoves(sq)
1d184b4c
BA
661 {
662 // Initialize with normal moves
a37076f1
BA
663 let moves = this.getSlideNJumpMoves(sq,
664 V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
46302e64 665 return moves.concat(this.getCastleMoves(sq));
1d184b4c
BA
666 }
667
46302e64 668 getCastleMoves([x,y])
1d184b4c 669 {
46302e64 670 const c = this.getColor(x,y);
0b7d99ec 671 if (x != (c=="w" ? V.size.x-1 : 0) || y != this.INIT_COL_KING[c])
1d184b4c
BA
672 return []; //x isn't first rank, or king has moved (shortcut)
673
1d184b4c
BA
674 // Castling ?
675 const oppCol = this.getOppCol(c);
676 let moves = [];
677 let i = 0;
0b7d99ec 678 const finalSquares = [ [2,3], [V.size.y-2,V.size.y-3] ]; //king, then rook
1d184b4c
BA
679 castlingCheck:
680 for (let castleSide=0; castleSide < 2; castleSide++) //large, then small
681 {
2526c041 682 if (!this.castleFlags[c][castleSide])
1d184b4c
BA
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 {
cf130369 690 if (this.isAttacked([x,i], [oppCol]) || (this.board[x][i] != V.EMPTY &&
1d184b4c
BA
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
1a788978 735 ////////////////////
1d184b4c
BA
736 // MOVES VALIDATION
737
1d184b4c
BA
738 getPossibleMovesFrom(sq)
739 {
740 // Assuming color is right (already checked)
741 return this.filterValid( this.getPotentialMovesFrom(sq) );
742 }
743
92342261 744 // TODO: promotions (into R,B,N,Q) should be filtered only once
1d184b4c
BA
745 filterValid(moves)
746 {
747 if (moves.length == 0)
748 return [];
b8121223 749 return moves.filter(m => { return !this.underCheck(m); });
1d184b4c
BA
750 }
751
752 // Search for all valid moves considering current turn (for engine and game end)
46302e64 753 getAllValidMoves()
1d184b4c 754 {
46302e64 755 const color = this.turn;
1d184b4c 756 const oppCol = this.getOppCol(color);
c6052161 757 let potentialMoves = [];
0b7d99ec 758 for (let i=0; i<V.size.x; i++)
1d184b4c 759 {
0b7d99ec 760 for (let j=0; j<V.size.y; j++)
1d184b4c 761 {
92342261 762 // Next condition "!= oppCol" = harmless hack to work with checkered variant
0b7d99ec 763 if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol)
1d184b4c
BA
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 }
9de73b71 771
e64a4eff 772 // Stop at the first move found
46302e64 773 atLeastOneMove()
e64a4eff 774 {
46302e64 775 const color = this.turn;
e64a4eff 776 const oppCol = this.getOppCol(color);
0b7d99ec 777 for (let i=0; i<V.size.x; i++)
e64a4eff 778 {
0b7d99ec 779 for (let j=0; j<V.size.y; j++)
e64a4eff 780 {
0b7d99ec 781 if (this.board[i][j] != V.EMPTY && this.getColor(i,j) != oppCol)
e64a4eff
BA
782 {
783 const moves = this.getPotentialMovesFrom([i,j]);
784 if (moves.length > 0)
785 {
9de73b71 786 for (let k=0; k<moves.length; k++)
e64a4eff 787 {
9de73b71 788 if (this.filterValid([moves[k]]).length > 0)
e64a4eff
BA
789 return true;
790 }
791 }
792 }
793 }
794 }
795 return false;
796 }
1d184b4c 797
1a788978 798 // Check if pieces of color in array 'colors' are attacking (king) on square x,y
46302e64 799 isAttacked(sq, colors)
1d184b4c 800 {
46302e64
BA
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));
1d184b4c
BA
807 }
808
dda21a71 809 // Is square x,y attacked by 'colors' pawns ?
46302e64 810 isAttackedByPawn([x,y], colors)
1d184b4c 811 {
46302e64 812 for (let c of colors)
1d184b4c 813 {
46302e64 814 let pawnShift = (c=="w" ? 1 : -1);
0b7d99ec 815 if (x+pawnShift>=0 && x+pawnShift<V.size.x)
1d184b4c 816 {
46302e64 817 for (let i of [-1,1])
1d184b4c 818 {
0b7d99ec 819 if (y+i>=0 && y+i<V.size.y && this.getPiece(x+pawnShift,y+i)==V.PAWN
46302e64
BA
820 && this.getColor(x+pawnShift,y+i)==c)
821 {
822 return true;
823 }
1d184b4c
BA
824 }
825 }
826 }
827 return false;
828 }
829
dda21a71 830 // Is square x,y attacked by 'colors' rooks ?
46302e64 831 isAttackedByRook(sq, colors)
1d184b4c 832 {
0b7d99ec 833 return this.isAttackedBySlideNJump(sq, colors, V.ROOK, V.steps[V.ROOK]);
1d184b4c
BA
834 }
835
dda21a71 836 // Is square x,y attacked by 'colors' knights ?
46302e64 837 isAttackedByKnight(sq, colors)
1d184b4c 838 {
46302e64 839 return this.isAttackedBySlideNJump(sq, colors,
0b7d99ec 840 V.KNIGHT, V.steps[V.KNIGHT], "oneStep");
1d184b4c
BA
841 }
842
dda21a71 843 // Is square x,y attacked by 'colors' bishops ?
46302e64 844 isAttackedByBishop(sq, colors)
1d184b4c 845 {
0b7d99ec 846 return this.isAttackedBySlideNJump(sq, colors, V.BISHOP, V.steps[V.BISHOP]);
1d184b4c
BA
847 }
848
dda21a71 849 // Is square x,y attacked by 'colors' queens ?
46302e64 850 isAttackedByQueen(sq, colors)
1d184b4c 851 {
a37076f1
BA
852 return this.isAttackedBySlideNJump(sq, colors, V.QUEEN,
853 V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
1d184b4c
BA
854 }
855
dda21a71 856 // Is square x,y attacked by 'colors' king(s) ?
46302e64 857 isAttackedByKing(sq, colors)
1d184b4c 858 {
a37076f1
BA
859 return this.isAttackedBySlideNJump(sq, colors, V.KING,
860 V.steps[V.ROOK].concat(V.steps[V.BISHOP]), "oneStep");
1d184b4c
BA
861 }
862
1221ac47 863 // Generic method for non-pawn pieces ("sliding or jumping"):
dda21a71 864 // is x,y attacked by a piece of color in array 'colors' ?
46302e64 865 isAttackedBySlideNJump([x,y], colors, piece, steps, oneStep)
1d184b4c
BA
866 {
867 for (let step of steps)
868 {
869 let rx = x+step[0], ry = y+step[1];
0b7d99ec 870 while (V.OnBoard(rx,ry) && this.board[rx][ry] == V.EMPTY && !oneStep)
1d184b4c
BA
871 {
872 rx += step[0];
873 ry += step[1];
874 }
0b7d99ec
BA
875 if (V.OnBoard(rx,ry) && this.getPiece(rx,ry) === piece
876 && colors.includes(this.getColor(rx,ry)))
1d184b4c
BA
877 {
878 return true;
879 }
880 }
881 return false;
882 }
883
dda21a71 884 // Is current player under check after his move ?
46302e64 885 underCheck(move)
1d184b4c 886 {
46302e64 887 const color = this.turn;
1d184b4c 888 this.play(move);
cf130369 889 let res = this.isAttacked(this.kingPos[color], [this.getOppCol(color)]);
1d184b4c
BA
890 this.undo(move);
891 return res;
892 }
893
1a788978
BA
894 /////////////////
895 // MOVES PLAYING
4b5fe306 896
1d184b4c
BA
897 // Apply a move on board
898 static PlayOnBoard(board, move)
899 {
900 for (let psq of move.vanish)
0b7d99ec 901 board[psq.x][psq.y] = V.EMPTY;
1d184b4c
BA
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)
0b7d99ec 909 board[psq.x][psq.y] = V.EMPTY;
1d184b4c
BA
910 for (let psq of move.vanish)
911 board[psq.x][psq.y] = psq.c + psq.p;
912 }
913
d3334c3a 914 // Before move is played, update variables + flags
1d184b4c
BA
915 updateVariables(move)
916 {
917 const piece = this.getPiece(move.start.x,move.start.y);
0b7d99ec
BA
918 const c = this.turn;
919 const firstRank = (c == "w" ? V.size.x-1 : 0);
1d184b4c
BA
920
921 // Update king position + flags
0b7d99ec 922 if (piece == V.KING && move.appear.length > 0)
1d184b4c
BA
923 {
924 this.kingPos[c][0] = move.appear[0].x;
925 this.kingPos[c][1] = move.appear[0].y;
2526c041 926 this.castleFlags[c] = [false,false];
1d184b4c
BA
927 return;
928 }
929 const oppCol = this.getOppCol(c);
0b7d99ec 930 const oppFirstRank = (V.size.x-1) - firstRank;
1d184b4c
BA
931 if (move.start.x == firstRank //our rook moves?
932 && this.INIT_COL_ROOK[c].includes(move.start.y))
933 {
2526c041
BA
934 const flagIdx = (move.start.y == this.INIT_COL_ROOK[c][0] ? 0 : 1);
935 this.castleFlags[c][flagIdx] = false;
1d184b4c
BA
936 }
937 else if (move.end.x == oppFirstRank //we took opponent rook?
aea1443e 938 && this.INIT_COL_ROOK[oppCol].includes(move.end.y))
1d184b4c 939 {
2526c041
BA
940 const flagIdx = (move.end.y == this.INIT_COL_ROOK[oppCol][0] ? 0 : 1);
941 this.castleFlags[oppCol][flagIdx] = false;
1d184b4c
BA
942 }
943 }
944
1a788978
BA
945 // After move is undo-ed *and flags resetted*, un-update other variables
946 // TODO: more symmetry, by storing flags increment in move (?!)
d3334c3a 947 unupdateVariables(move)
1d184b4c 948 {
d3334c3a
BA
949 // (Potentially) Reset king position
950 const c = this.getColor(move.start.x,move.start.y);
0b7d99ec 951 if (this.getPiece(move.start.x,move.start.y) == V.KING)
d3334c3a
BA
952 this.kingPos[c] = [move.start.x, move.start.y];
953 }
1d184b4c 954
d3334c3a
BA
955 play(move, ingame)
956 {
a3c86ec9
BA
957 // DEBUG:
958// if (!this.states) this.states = [];
1a788978 959// if (!ingame) this.states.push(this.getFen());
a3c86ec9 960
dfb4afc1 961 if (!!ingame)
6752407b 962 move.notation = [this.getNotation(move), this.getLongNotation(move)];
dfb4afc1 963
1a788978 964 move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
d3334c3a
BA
965 this.updateVariables(move);
966 this.moves.push(move);
1d184b4c 967 this.epSquares.push( this.getEpSquare(move) );
8ddc00a0 968 this.turn = this.getOppCol(this.turn);
0b7d99ec 969 V.PlayOnBoard(this.board, move);
331fc58c
BA
970
971 if (!!ingame)
1a788978
BA
972 {
973 // Hash of current game state *after move*, to detect repetitions
974 move.hash = hex_md5(this.getFen();
975 }
1d184b4c
BA
976 }
977
cd4cad04 978 undo(move)
1d184b4c 979 {
0b7d99ec 980 V.UndoOnBoard(this.board, move);
8ddc00a0 981 this.turn = this.getOppCol(this.turn);
1d184b4c 982 this.epSquares.pop();
d3334c3a
BA
983 this.moves.pop();
984 this.unupdateVariables(move);
1a788978 985 this.disaggregateFlags(JSON.parse(move.flags));
a3c86ec9
BA
986
987 // DEBUG:
1a788978 988// if (this.getFen() != this.states[this.states.length-1])
a3c86ec9
BA
989// debugger;
990// this.states.pop();
1d184b4c
BA
991 }
992
1a788978 993 ///////////////
1d184b4c
BA
994 // END OF GAME
995
331fc58c 996 // Check for 3 repetitions (position + flags + turn)
1af36beb 997 checkRepetition()
1d184b4c 998 {
15c1295a
BA
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 }
331fc58c 1013 return Object.values(this.hashStates).some(elt => { return (elt >= 3); });
1af36beb 1014 }
1d184b4c 1015
dda21a71 1016 // Is game over ? And if yes, what is the score ?
1af36beb
BA
1017 checkGameOver()
1018 {
1019 if (this.checkRepetition())
1020 return "1/2";
1021
1022 if (this.atLeastOneMove()) // game not over
1d184b4c 1023 return "*";
1d184b4c
BA
1024
1025 // Game over
46302e64 1026 return this.checkGameEnd();
1d184b4c
BA
1027 }
1028
46302e64
BA
1029 // No moves are possible: compute score
1030 checkGameEnd()
1d184b4c 1031 {
46302e64 1032 const color = this.turn;
1d184b4c 1033 // No valid move: stalemate or checkmate?
cf130369 1034 if (!this.isAttacked(this.kingPos[color], [this.getOppCol(color)]))
1d184b4c
BA
1035 return "1/2";
1036 // OK, checkmate
1037 return color == "w" ? "0-1" : "1-0";
1038 }
1039
1a788978
BA
1040 ///////////////
1041 // ENGINE PLAY
1d184b4c
BA
1042
1043 // Pieces values
1a788978
BA
1044 static get VALUES()
1045 {
1d184b4c
BA
1046 return {
1047 'p': 1,
1048 'r': 5,
1049 'n': 3,
1050 'b': 3,
1051 'q': 9,
1052 'k': 1000
1053 };
1054 }
1055
1a788978
BA
1056 // "Checkmate" (unreachable eval)
1057 static get INFINITY() { return 9999; }
9e42b4dd 1058
1a788978
BA
1059 // At this value or above, the game is over
1060 static get THRESHOLD_MATE() { return V.INFINITY; }
9e42b4dd 1061
1a788978
BA
1062 // Search depth: 2 for high branching factor, 4 for small (Loser chess, eg.)
1063 static get SEARCH_DEPTH() { return 3; }
3c09dc49 1064
1d184b4c 1065 // Assumption: at least one legal move
a6abf094
BA
1066 // NOTE: works also for extinction chess because depth is 3...
1067 getComputerMove()
1d184b4c 1068 {
0b7d99ec 1069 const maxeval = V.INFINITY;
46302e64 1070 const color = this.turn;
15952ada
BA
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");
a6abf094
BA
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]);
0b7d99ec
BA
1079 let finish = (Math.abs(this.evalPosition()) >= V.THRESHOLD_MATE);
1080 if (!finish && !this.atLeastOneMove())
1081 {
1a788978 1082 // Test mate (for other variants)
0b7d99ec
BA
1083 const score = this.checkGameEnd();
1084 if (score != "1/2")
1085 finish = true;
1086 }
a6abf094
BA
1087 this.undo(moves1[i]);
1088 if (finish)
1089 return moves1[i];
1090 }
1d184b4c 1091
06ddfe34 1092 // Rank moves using a min-max at depth 2
1d184b4c
BA
1093 for (let i=0; i<moves1.length; i++)
1094 {
9e42b4dd 1095 moves1[i].eval = (color=="w" ? -1 : 1) * maxeval; //very low, I'm checkmated
1d184b4c 1096 this.play(moves1[i]);
68f5ccc8
BA
1097 let eval2 = undefined;
1098 if (this.atLeastOneMove())
1d184b4c 1099 {
68f5ccc8
BA
1100 eval2 = (color=="w" ? 1 : -1) * maxeval; //initialized with checkmate value
1101 // Second half-move:
15952ada 1102 let moves2 = this.getAllValidMoves("computer");
68f5ccc8
BA
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 {
1a788978 1111 // Working with scores is more accurate (necessary for Loser variant)
68f5ccc8
BA
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);
1d184b4c 1124 }
92342261
BA
1125 if ((color=="w" && eval2 > moves1[i].eval)
1126 || (color=="b" && eval2 < moves1[i].eval))
1127 {
1d184b4c 1128 moves1[i].eval = eval2;
92342261 1129 }
1d184b4c
BA
1130 this.undo(moves1[i]);
1131 }
1132 moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
1133
3c09dc49
BA
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
e82cd979
BA
1139 // From here, depth >= 3: may take a while, so we control time
1140 const timeStart = Date.now();
1141
a6abf094 1142 // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...)
0b7d99ec 1143 if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE)
e64a4eff 1144 {
9e42b4dd
BA
1145 for (let i=0; i<moves1.length; i++)
1146 {
e82cd979
BA
1147 if (Date.now()-timeStart >= 5000) //more than 5 seconds
1148 return currentBest; //depth 2 at least
9e42b4dd
BA
1149 this.play(moves1[i]);
1150 // 0.1 * oldEval : heuristic to avoid some bad moves (not all...)
3c09dc49 1151 moves1[i].eval = 0.1*moves1[i].eval +
0b7d99ec 1152 this.alphabeta(V.SEARCH_DEPTH-1, -maxeval, maxeval);
9e42b4dd
BA
1153 this.undo(moves1[i]);
1154 }
1155 moves1.sort( (a,b) => { return (color=="w" ? 1 : -1) * (b.eval - a.eval); });
e64a4eff 1156 }
3c09dc49
BA
1157 else
1158 return currentBest;
68f5ccc8 1159 //console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
1d184b4c 1160
3c09dc49 1161 candidates = [0];
1d184b4c
BA
1162 for (let j=1; j<moves1.length && moves1[j].eval == moves1[0].eval; j++)
1163 candidates.push(j);
1d184b4c
BA
1164 return moves1[_.sample(candidates, 1)];
1165 }
1166
46302e64 1167 alphabeta(depth, alpha, beta)
1d184b4c 1168 {
0b7d99ec 1169 const maxeval = V.INFINITY;
46302e64
BA
1170 const color = this.turn;
1171 if (!this.atLeastOneMove())
1d184b4c 1172 {
46302e64 1173 switch (this.checkGameEnd())
1d184b4c 1174 {
68f5ccc8
BA
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);
1d184b4c
BA
1180 }
1181 }
1182 if (depth == 0)
1183 return this.evalPosition();
15952ada 1184 const moves = this.getAllValidMoves("computer");
9e42b4dd 1185 let v = color=="w" ? -maxeval : maxeval;
1d184b4c
BA
1186 if (color == "w")
1187 {
1188 for (let i=0; i<moves.length; i++)
1189 {
1190 this.play(moves[i]);
46302e64 1191 v = Math.max(v, this.alphabeta(depth-1, alpha, beta));
1d184b4c
BA
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]);
46302e64 1203 v = Math.min(v, this.alphabeta(depth-1, alpha, beta));
1d184b4c
BA
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 {
1d184b4c 1215 let evaluation = 0;
a6abf094 1216 // Just count material for now
0b7d99ec 1217 for (let i=0; i<V.size.x; i++)
1d184b4c 1218 {
0b7d99ec 1219 for (let j=0; j<V.size.y; j++)
1d184b4c 1220 {
0b7d99ec 1221 if (this.board[i][j] != V.EMPTY)
1d184b4c
BA
1222 {
1223 const sign = this.getColor(i,j) == "w" ? 1 : -1;
0b7d99ec 1224 evaluation += sign * V.VALUES[this.getPiece(i,j)];
1d184b4c
BA
1225 }
1226 }
1227 }
1228 return evaluation;
1229 }
1230
1a788978
BA
1231 /////////////////////////
1232 // MOVES + GAME NOTATION
1233 /////////////////////////
1d184b4c
BA
1234
1235 // Context: just before move is played, turn hasn't changed
1236 getNotation(move)
1237 {
0b7d99ec 1238 if (move.appear.length == 2 && move.appear[0].p == V.KING) //castle
15952ada 1239 return (move.end.y < move.start.y ? "0-0-0" : "0-0");
1d184b4c
BA
1240
1241 // Translate final square
1a788978 1242 const finalSquare = V.CoordsToSquare(move.end);
1d184b4c 1243
270968d6 1244 const piece = this.getPiece(move.start.x, move.start.y);
0b7d99ec 1245 if (piece == V.PAWN)
1d184b4c
BA
1246 {
1247 // Pawn move
1248 let notation = "";
5bfb0956 1249 if (move.vanish.length > move.appear.length)
1d184b4c
BA
1250 {
1251 // Capture
270968d6 1252 const startColumn = String.fromCharCode(97 + move.start.y);
1d184b4c
BA
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
f3c10e18
BA
1265 return piece.toUpperCase() +
1266 (move.vanish.length > move.appear.length ? "x" : "") + finalSquare;
1d184b4c
BA
1267 }
1268 }
dfb4afc1 1269
6752407b
BA
1270 // Complete the usual notation, may be required for de-ambiguification
1271 getLongNotation(move)
1272 {
1a788978
BA
1273 // Not encoding move. But short+long is enough
1274 return V.CoordsToSquare(move.start) + V.CoordsToSquare(move.end);
6752407b
BA
1275 }
1276
dfb4afc1 1277 // The score is already computed when calling this function
01a135e2 1278 getPGN(mycolor, score, fenStart, mode)
dfb4afc1
BA
1279 {
1280 let pgn = "";
1281 pgn += '[Site "vchess.club"]<br>';
5e622704 1282 const opponent = mode=="human" ? "Anonymous" : "Computer";
0f51ef98 1283 pgn += '[Variant "' + variant + '"]<br>';
c794dbb8 1284 pgn += '[Date "' + getDate(new Date()) + '"]<br>';
01a135e2
BA
1285 pgn += '[White "' + (mycolor=='w'?'Myself':opponent) + '"]<br>';
1286 pgn += '[Black "' + (mycolor=='b'?'Myself':opponent) + '"]<br>';
04449c97
BA
1287 pgn += '[FenStart "' + fenStart + '"]<br>';
1288 pgn += '[Fen "' + this.getFen() + '"]<br>';
762b7c9c 1289 pgn += '[Result "' + score + '"]<br><br>';
dfb4afc1 1290
6752407b 1291 // Standard PGN
dfb4afc1
BA
1292 for (let i=0; i<this.moves.length; i++)
1293 {
1294 if (i % 2 == 0)
1295 pgn += ((i/2)+1) + ".";
6752407b 1296 pgn += this.moves[i].notation[0] + " ";
dfb4afc1 1297 }
97fc8bf7 1298 pgn += "<br><br>";
dfb4afc1 1299
6752407b
BA
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 }
6752407b 1307
dfb4afc1
BA
1308 return pgn;
1309 }
1d184b4c 1310}