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