Experimental change: options replacing randomness (more general)
[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;
2da551a3
BA
25 this.start = o.start || { x: o.vanish[0].x, y: o.vanish[0].y };
26 this.end = o.end || { x: o.appear[0].x, y: o.appear[0].y };
1c9f093d 27 }
6808d7a1 28};
1d184b4c 29
2c5d7b20
BA
30// NOTE: x coords = top to bottom; y = left to right
31// (from white player perspective)
6808d7a1 32export const ChessRules = class ChessRules {
7e8a7ea1 33
1c9f093d
BA
34 //////////////
35 // MISC UTILS
36
eb2d61de
BA
37 static get Options() {
38 return {
39 select: [
40 {
41 label: "Randomness",
42 variable: "randomness",
43 defaut: 2,
44 options: [
45 { label: "Deterministic", value: 0 },
46 { label: "Symmetric random", value: 1 },
47 { label: "Asymmetric random", value: 2 }
48 ]
49 }
50 ],
51 check: []
52 };
53 }
54
55 static AbbreviateOptions(opts) {
56 return "";
57 // Randomness is a special option: (TODO?)
58 //return "R" + opts.randomness;
59 }
60
20620465 61 // Some variants don't have flags:
6808d7a1
BA
62 static get HasFlags() {
63 return true;
20620465 64 }
1c9f093d 65
3a2a7b5f
BA
66 // Or castle
67 static get HasCastle() {
68 return V.HasFlags;
69 }
70
32f6285e
BA
71 // Pawns specifications
72 static get PawnSpecs() {
73 return {
74 directions: { 'w': -1, 'b': 1 },
472c0c4f 75 initShift: { w: 1, b: 1 },
32f6285e 76 twoSquares: true,
472c0c4f 77 threeSquares: false,
32f6285e
BA
78 promotions: [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN],
79 canCapture: true,
80 captureBackward: false,
81 bidirectional: false
82 };
83 }
84
85 // En-passant captures need a stack of squares:
6808d7a1
BA
86 static get HasEnpassant() {
87 return true;
20620465
BA
88 }
89
90 // Some variants cannot have analyse mode
8477e53d 91 static get CanAnalyze() {
20620465
BA
92 return true;
93 }
933fd1f9
BA
94 // Patch: issues with javascript OOP, objects can't access static fields.
95 get canAnalyze() {
96 return V.CanAnalyze;
97 }
20620465
BA
98
99 // Some variants show incomplete information,
100 // and thus show only a partial moves list or no list at all.
101 static get ShowMoves() {
102 return "all";
103 }
933fd1f9
BA
104 get showMoves() {
105 return V.ShowMoves;
106 }
1c9f093d 107
00eef1ca
BA
108 // Sometimes moves must remain hidden until game ends
109 static get SomeHiddenMoves() {
110 return false;
111 }
112 get someHiddenMoves() {
113 return V.SomeHiddenMoves;
114 }
115
ad030c7d
BA
116 // Generally true, unless the variant includes random effects
117 static get CorrConfirm() {
118 return true;
119 }
120
5246b49d
BA
121 // Used for Monochrome variant (TODO: harmonize: !canFlip ==> showFirstTurn)
122 get showFirstTurn() {
123 return false;
124 }
125
71ef1664
BA
126 // Some variants always show the same orientation
127 static get CanFlip() {
128 return true;
129 }
130 get canFlip() {
131 return V.CanFlip;
132 }
133
107dc1bd
BA
134 // For (generally old) variants without checkered board
135 static get Monochrome() {
136 return false;
137 }
138
173f11dc 139 // Some games are drawn unusually (bottom right corner is black)
157a72c8
BA
140 static get DarkBottomRight() {
141 return false;
142 }
143
107dc1bd
BA
144 // Some variants require lines drawing
145 static get Lines() {
146 if (V.Monochrome) {
147 let lines = [];
148 // Draw all inter-squares lines
149 for (let i = 0; i <= V.size.x; i++)
150 lines.push([[i, 0], [i, V.size.y]]);
151 for (let j = 0; j <= V.size.y; j++)
152 lines.push([[0, j], [V.size.x, j]]);
153 return lines;
154 }
155 return null;
156 }
157
9a1e3abe
BA
158 // In some variants, the player who repeat a position loses
159 static get LoseOnRepetition() {
d2af3400
BA
160 return false;
161 }
f0a812b7
BA
162 // And in some others (Iceage), repetitions should be ignored:
163 static get IgnoreRepetition() {
164 return false;
165 }
809ab1a8
BA
166 loseOnRepetition() {
167 // In some variants, result depends on the position:
168 return V.LoseOnRepetition;
169 }
d2af3400
BA
170
171 // At some stages, some games could wait clicks only:
172 onlyClick() {
9a1e3abe
BA
173 return false;
174 }
175
61656127
BA
176 // Some variants use click infos:
177 doClick() {
178 return null;
179 }
180
90df90bc
BA
181 // Some variants may need to highlight squares on hover (Hamilton, Weiqi...)
182 hoverHighlight() {
183 return false;
184 }
185
14edde72
BA
186 static get IMAGE_EXTENSION() {
187 // All pieces should be in the SVG format
188 return ".svg";
189 }
190
1c9f093d 191 // Turn "wb" into "B" (for FEN)
6808d7a1
BA
192 static board2fen(b) {
193 return b[0] == "w" ? b[1].toUpperCase() : b[1];
1c9f093d
BA
194 }
195
196 // Turn "p" into "bp" (for board)
6808d7a1 197 static fen2board(f) {
6cc34165 198 return f.charCodeAt(0) <= 90 ? "w" + f.toLowerCase() : "b" + f;
1c9f093d
BA
199 }
200
68e19a44 201 // Check if FEN describes a board situation correctly
6808d7a1 202 static IsGoodFen(fen) {
1c9f093d
BA
203 const fenParsed = V.ParseFen(fen);
204 // 1) Check position
6808d7a1 205 if (!V.IsGoodPosition(fenParsed.position)) return false;
1c9f093d 206 // 2) Check turn
6808d7a1 207 if (!fenParsed.turn || !V.IsGoodTurn(fenParsed.turn)) return false;
1c9f093d 208 // 3) Check moves count
e50a8025 209 if (!fenParsed.movesCount || !(parseInt(fenParsed.movesCount, 10) >= 0))
1c9f093d
BA
210 return false;
211 // 4) Check flags
212 if (V.HasFlags && (!fenParsed.flags || !V.IsGoodFlags(fenParsed.flags)))
213 return false;
214 // 5) Check enpassant
6808d7a1
BA
215 if (
216 V.HasEnpassant &&
217 (!fenParsed.enpassant || !V.IsGoodEnpassant(fenParsed.enpassant))
218 ) {
1c9f093d
BA
219 return false;
220 }
221 return true;
222 }
223
224 // Is position part of the FEN a priori correct?
6808d7a1
BA
225 static IsGoodPosition(position) {
226 if (position.length == 0) return false;
1c9f093d 227 const rows = position.split("/");
6808d7a1 228 if (rows.length != V.size.x) return false;
6f2f9437 229 let kings = { "k": 0, "K": 0 };
6808d7a1 230 for (let row of rows) {
1c9f093d 231 let sumElts = 0;
6808d7a1 232 for (let i = 0; i < row.length; i++) {
6f2f9437 233 if (['K','k'].includes(row[i])) kings[row[i]]++;
6808d7a1
BA
234 if (V.PIECES.includes(row[i].toLowerCase())) sumElts++;
235 else {
e50a8025 236 const num = parseInt(row[i], 10);
173f11dc 237 if (isNaN(num) || num <= 0) return false;
1c9f093d
BA
238 sumElts += num;
239 }
240 }
6808d7a1 241 if (sumElts != V.size.y) return false;
1c9f093d 242 }
6f2f9437
BA
243 // Both kings should be on board. Exactly one per color.
244 if (Object.values(kings).some(v => v != 1)) return false;
1c9f093d
BA
245 return true;
246 }
247
248 // For FEN checking
6808d7a1
BA
249 static IsGoodTurn(turn) {
250 return ["w", "b"].includes(turn);
1c9f093d
BA
251 }
252
253 // For FEN checking
6808d7a1 254 static IsGoodFlags(flags) {
3a2a7b5f
BA
255 // NOTE: a little too permissive to work with more variants
256 return !!flags.match(/^[a-z]{4,4}$/);
1c9f093d
BA
257 }
258
472c0c4f 259 // NOTE: not with regexp to adapt to different board sizes. (TODO?)
6808d7a1
BA
260 static IsGoodEnpassant(enpassant) {
261 if (enpassant != "-") {
262 const ep = V.SquareToCoords(enpassant);
263 if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
1c9f093d
BA
264 }
265 return true;
266 }
267
268 // 3 --> d (column number to letter)
6808d7a1 269 static CoordToColumn(colnum) {
1c9f093d
BA
270 return String.fromCharCode(97 + colnum);
271 }
272
273 // d --> 3 (column letter to number)
6808d7a1 274 static ColumnToCoord(column) {
1c9f093d
BA
275 return column.charCodeAt(0) - 97;
276 }
277
278 // a4 --> {x:3,y:0}
6808d7a1 279 static SquareToCoords(sq) {
1c9f093d
BA
280 return {
281 // NOTE: column is always one char => max 26 columns
282 // row is counted from black side => subtraction
e50a8025 283 x: V.size.x - parseInt(sq.substr(1), 10),
1c9f093d
BA
284 y: sq[0].charCodeAt() - 97
285 };
286 }
287
288 // {x:0,y:4} --> e8
6808d7a1 289 static CoordsToSquare(coords) {
1c9f093d
BA
290 return V.CoordToColumn(coords.y) + (V.size.x - coords.x);
291 }
292
305ede7e 293 // Path to pieces (standard ones in pieces/ folder)
241bf8f2 294 getPpath(b) {
305ede7e 295 return b;
241bf8f2
BA
296 }
297
3a2a7b5f 298 // Path to promotion pieces (usually the same)
c7550017
BA
299 getPPpath(m) {
300 return this.getPpath(m.appear[0].c + m.appear[0].p);
3a2a7b5f
BA
301 }
302
1c9f093d 303 // Aggregates flags into one object
6808d7a1 304 aggregateFlags() {
1c9f093d
BA
305 return this.castleFlags;
306 }
307
308 // Reverse operation
6808d7a1 309 disaggregateFlags(flags) {
1c9f093d
BA
310 this.castleFlags = flags;
311 }
312
313 // En-passant square, if any
6808d7a1 314 getEpSquare(moveOrSquare) {
4a209313 315 if (!moveOrSquare) return undefined; //TODO: necessary line?!
6808d7a1 316 if (typeof moveOrSquare === "string") {
1c9f093d 317 const square = moveOrSquare;
6808d7a1 318 if (square == "-") return undefined;
1c9f093d
BA
319 return V.SquareToCoords(square);
320 }
321 // Argument is a move:
322 const move = moveOrSquare;
1c5bfdf2
BA
323 const s = move.start,
324 e = move.end;
6808d7a1 325 if (
1c5bfdf2 326 s.y == e.y &&
0d5335de
BA
327 Math.abs(s.x - e.x) == 2 &&
328 // Next conditions for variants like Atomic or Rifle, Recycle...
329 (move.appear.length > 0 && move.appear[0].p == V.PAWN) &&
330 (move.vanish.length > 0 && move.vanish[0].p == V.PAWN)
6808d7a1 331 ) {
1c9f093d 332 return {
1c5bfdf2
BA
333 x: (s.x + e.x) / 2,
334 y: s.y
1c9f093d
BA
335 };
336 }
337 return undefined; //default
338 }
339
340 // Can thing on square1 take thing on square2
6808d7a1
BA
341 canTake([x1, y1], [x2, y2]) {
342 return this.getColor(x1, y1) !== this.getColor(x2, y2);
1c9f093d
BA
343 }
344
345 // Is (x,y) on the chessboard?
6808d7a1
BA
346 static OnBoard(x, y) {
347 return x >= 0 && x < V.size.x && y >= 0 && y < V.size.y;
1c9f093d
BA
348 }
349
350 // Used in interface: 'side' arg == player color
6808d7a1
BA
351 canIplay(side, [x, y]) {
352 return this.turn == side && this.getColor(x, y) == side;
1c9f093d
BA
353 }
354
355 // On which squares is color under check ? (for interface)
af34341d
BA
356 getCheckSquares() {
357 const color = this.turn;
b0a0468a
BA
358 return (
359 this.underCheck(color)
2c5d7b20
BA
360 // kingPos must be duplicated, because it may change:
361 ? [JSON.parse(JSON.stringify(this.kingPos[color]))]
b0a0468a
BA
362 : []
363 );
1c9f093d
BA
364 }
365
366 /////////////
367 // FEN UTILS
368
7ba4a5bc 369 // Setup the initial random (asymmetric) position
eb2d61de
BA
370 static GenRandInitFen(options) {
371 if (!options.randomness || options.randomness == 0)
7ba4a5bc 372 // Deterministic:
3a2a7b5f 373 return "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 0 ahah -";
7ba4a5bc 374
6808d7a1 375 let pieces = { w: new Array(8), b: new Array(8) };
3a2a7b5f 376 let flags = "";
7ba4a5bc 377 // Shuffle pieces on first (and last rank if randomness == 2)
6808d7a1 378 for (let c of ["w", "b"]) {
eb2d61de 379 if (c == 'b' && options.randomness == 1) {
7ba4a5bc 380 pieces['b'] = pieces['w'];
3a2a7b5f 381 flags += flags;
7ba4a5bc
BA
382 break;
383 }
384
1c9f093d
BA
385 let positions = ArrayFun.range(8);
386
387 // Get random squares for bishops
656b1878 388 let randIndex = 2 * randInt(4);
1c9f093d
BA
389 const bishop1Pos = positions[randIndex];
390 // The second bishop must be on a square of different color
656b1878 391 let randIndex_tmp = 2 * randInt(4) + 1;
1c9f093d
BA
392 const bishop2Pos = positions[randIndex_tmp];
393 // Remove chosen squares
6808d7a1
BA
394 positions.splice(Math.max(randIndex, randIndex_tmp), 1);
395 positions.splice(Math.min(randIndex, randIndex_tmp), 1);
1c9f093d
BA
396
397 // Get random squares for knights
656b1878 398 randIndex = randInt(6);
1c9f093d
BA
399 const knight1Pos = positions[randIndex];
400 positions.splice(randIndex, 1);
656b1878 401 randIndex = randInt(5);
1c9f093d
BA
402 const knight2Pos = positions[randIndex];
403 positions.splice(randIndex, 1);
404
405 // Get random square for queen
656b1878 406 randIndex = randInt(4);
1c9f093d
BA
407 const queenPos = positions[randIndex];
408 positions.splice(randIndex, 1);
409
410 // Rooks and king positions are now fixed,
411 // because of the ordering rook-king-rook
412 const rook1Pos = positions[0];
413 const kingPos = positions[1];
414 const rook2Pos = positions[2];
415
416 // Finally put the shuffled pieces in the board array
6808d7a1
BA
417 pieces[c][rook1Pos] = "r";
418 pieces[c][knight1Pos] = "n";
419 pieces[c][bishop1Pos] = "b";
420 pieces[c][queenPos] = "q";
421 pieces[c][kingPos] = "k";
422 pieces[c][bishop2Pos] = "b";
423 pieces[c][knight2Pos] = "n";
424 pieces[c][rook2Pos] = "r";
3a2a7b5f 425 flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos);
1c9f093d 426 }
e3e2cc44 427 // Add turn + flags + enpassant
6808d7a1
BA
428 return (
429 pieces["b"].join("") +
1c9f093d
BA
430 "/pppppppp/8/8/8/8/PPPPPPPP/" +
431 pieces["w"].join("").toUpperCase() +
3a2a7b5f 432 " w 0 " + flags + " -"
e3e2cc44 433 );
1c9f093d
BA
434 }
435
436 // "Parse" FEN: just return untransformed string data
6808d7a1 437 static ParseFen(fen) {
1c9f093d 438 const fenParts = fen.split(" ");
6808d7a1 439 let res = {
1c9f093d
BA
440 position: fenParts[0],
441 turn: fenParts[1],
6808d7a1 442 movesCount: fenParts[2]
1c9f093d
BA
443 };
444 let nextIdx = 3;
6808d7a1
BA
445 if (V.HasFlags) Object.assign(res, { flags: fenParts[nextIdx++] });
446 if (V.HasEnpassant) Object.assign(res, { enpassant: fenParts[nextIdx] });
1c9f093d
BA
447 return res;
448 }
449
450 // Return current fen (game state)
6808d7a1
BA
451 getFen() {
452 return (
f9c36b2d
BA
453 this.getBaseFen() + " " +
454 this.getTurnFen() + " " +
6808d7a1
BA
455 this.movesCount +
456 (V.HasFlags ? " " + this.getFlagsFen() : "") +
457 (V.HasEnpassant ? " " + this.getEnpassantFen() : "")
458 );
1c9f093d
BA
459 }
460
f9c36b2d
BA
461 getFenForRepeat() {
462 // Omit movesCount, only variable allowed to differ
463 return (
464 this.getBaseFen() + "_" +
465 this.getTurnFen() +
466 (V.HasFlags ? "_" + this.getFlagsFen() : "") +
467 (V.HasEnpassant ? "_" + this.getEnpassantFen() : "")
468 );
469 }
470
1c9f093d 471 // Position part of the FEN string
6808d7a1 472 getBaseFen() {
6f2f9437
BA
473 const format = (count) => {
474 // if more than 9 consecutive free spaces, break the integer,
475 // otherwise FEN parsing will fail.
476 if (count <= 9) return count;
7c05a5f2
BA
477 // Most boards of size < 18:
478 if (count <= 18) return "9" + (count - 9);
479 // Except Gomoku:
480 return "99" + (count - 18);
6f2f9437 481 };
1c9f093d 482 let position = "";
6808d7a1 483 for (let i = 0; i < V.size.x; i++) {
1c9f093d 484 let emptyCount = 0;
6808d7a1
BA
485 for (let j = 0; j < V.size.y; j++) {
486 if (this.board[i][j] == V.EMPTY) emptyCount++;
487 else {
488 if (emptyCount > 0) {
1c9f093d 489 // Add empty squares in-between
6f2f9437 490 position += format(emptyCount);
1c9f093d
BA
491 emptyCount = 0;
492 }
493 position += V.board2fen(this.board[i][j]);
494 }
495 }
6808d7a1 496 if (emptyCount > 0) {
1c9f093d 497 // "Flush remainder"
6f2f9437 498 position += format(emptyCount);
1c9f093d 499 }
6808d7a1 500 if (i < V.size.x - 1) position += "/"; //separate rows
1c9f093d
BA
501 }
502 return position;
503 }
504
6808d7a1 505 getTurnFen() {
1c9f093d
BA
506 return this.turn;
507 }
508
509 // Flags part of the FEN string
6808d7a1 510 getFlagsFen() {
1c9f093d 511 let flags = "";
3a2a7b5f
BA
512 // Castling flags
513 for (let c of ["w", "b"])
514 flags += this.castleFlags[c].map(V.CoordToColumn).join("");
1c9f093d
BA
515 return flags;
516 }
517
518 // Enpassant part of the FEN string
6808d7a1 519 getEnpassantFen() {
1c9f093d 520 const L = this.epSquares.length;
6808d7a1
BA
521 if (!this.epSquares[L - 1]) return "-"; //no en-passant
522 return V.CoordsToSquare(this.epSquares[L - 1]);
1c9f093d
BA
523 }
524
525 // Turn position fen into double array ["wb","wp","bk",...]
6808d7a1 526 static GetBoard(position) {
1c9f093d
BA
527 const rows = position.split("/");
528 let board = ArrayFun.init(V.size.x, V.size.y, "");
6808d7a1 529 for (let i = 0; i < rows.length; i++) {
1c9f093d 530 let j = 0;
6808d7a1 531 for (let indexInRow = 0; indexInRow < rows[i].length; indexInRow++) {
1c9f093d 532 const character = rows[i][indexInRow];
e50a8025 533 const num = parseInt(character, 10);
a13cbc0f 534 // If num is a number, just shift j:
6808d7a1 535 if (!isNaN(num)) j += num;
a13cbc0f 536 // Else: something at position i,j
6808d7a1 537 else board[i][j++] = V.fen2board(character);
1c9f093d
BA
538 }
539 }
540 return board;
541 }
542
543 // Extract (relevant) flags from fen
6808d7a1 544 setFlags(fenflags) {
1c9f093d 545 // white a-castle, h-castle, black a-castle, h-castle
bb688df5 546 this.castleFlags = { w: [-1, -1], b: [-1, -1] };
3a2a7b5f
BA
547 for (let i = 0; i < 4; i++) {
548 this.castleFlags[i < 2 ? "w" : "b"][i % 2] =
549 V.ColumnToCoord(fenflags.charAt(i));
550 }
1c9f093d
BA
551 }
552
553 //////////////////
554 // INITIALIZATION
555
37cdcbf3 556 // Fen string fully describes the game state
b627d118
BA
557 constructor(fen) {
558 if (!fen)
559 // In printDiagram() fen isn't supply because only getPpath() is used
560 // TODO: find a better solution!
561 return;
1c9f093d
BA
562 const fenParsed = V.ParseFen(fen);
563 this.board = V.GetBoard(fenParsed.position);
af34341d 564 this.turn = fenParsed.turn;
e50a8025 565 this.movesCount = parseInt(fenParsed.movesCount, 10);
1c9f093d
BA
566 this.setOtherVariables(fen);
567 }
568
3a2a7b5f 569 // Scan board for kings positions
9d15c433 570 // TODO: should be done from board, no need for the complete FEN
3a2a7b5f 571 scanKings(fen) {
2c5d7b20
BA
572 // Squares of white and black king:
573 this.kingPos = { w: [-1, -1], b: [-1, -1] };
1c9f093d 574 const fenRows = V.ParseFen(fen).position.split("/");
6808d7a1 575 for (let i = 0; i < fenRows.length; i++) {
1c9f093d 576 let k = 0; //column index on board
6808d7a1
BA
577 for (let j = 0; j < fenRows[i].length; j++) {
578 switch (fenRows[i].charAt(j)) {
579 case "k":
580 this.kingPos["b"] = [i, k];
1c9f093d 581 break;
6808d7a1
BA
582 case "K":
583 this.kingPos["w"] = [i, k];
1c9f093d 584 break;
6808d7a1 585 default: {
e50a8025 586 const num = parseInt(fenRows[i].charAt(j), 10);
6808d7a1
BA
587 if (!isNaN(num)) k += num - 1;
588 }
1c9f093d
BA
589 }
590 k++;
591 }
592 }
593 }
594
595 // Some additional variables from FEN (variant dependant)
6808d7a1 596 setOtherVariables(fen) {
1c9f093d
BA
597 // Set flags and enpassant:
598 const parsedFen = V.ParseFen(fen);
6808d7a1
BA
599 if (V.HasFlags) this.setFlags(parsedFen.flags);
600 if (V.HasEnpassant) {
601 const epSq =
602 parsedFen.enpassant != "-"
9bd6786b 603 ? this.getEpSquare(parsedFen.enpassant)
6808d7a1
BA
604 : undefined;
605 this.epSquares = [epSq];
1c9f093d 606 }
3a2a7b5f
BA
607 // Search for kings positions:
608 this.scanKings(fen);
1c9f093d
BA
609 }
610
611 /////////////////////
612 // GETTERS & SETTERS
613
6808d7a1
BA
614 static get size() {
615 return { x: 8, y: 8 };
1c9f093d
BA
616 }
617
0ba6420d 618 // Color of thing on square (i,j). 'undefined' if square is empty
6808d7a1 619 getColor(i, j) {
1c9f093d
BA
620 return this.board[i][j].charAt(0);
621 }
622
623 // Piece type on square (i,j). 'undefined' if square is empty
6808d7a1 624 getPiece(i, j) {
1c9f093d
BA
625 return this.board[i][j].charAt(1);
626 }
627
628 // Get opponent color
6808d7a1
BA
629 static GetOppCol(color) {
630 return color == "w" ? "b" : "w";
1c9f093d
BA
631 }
632
1c9f093d 633 // Pieces codes (for a clearer code)
6808d7a1
BA
634 static get PAWN() {
635 return "p";
636 }
637 static get ROOK() {
638 return "r";
639 }
640 static get KNIGHT() {
641 return "n";
642 }
643 static get BISHOP() {
644 return "b";
645 }
646 static get QUEEN() {
647 return "q";
648 }
649 static get KING() {
650 return "k";
651 }
1c9f093d
BA
652
653 // For FEN checking:
6808d7a1
BA
654 static get PIECES() {
655 return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN, V.KING];
1c9f093d
BA
656 }
657
658 // Empty square
6808d7a1
BA
659 static get EMPTY() {
660 return "";
661 }
1c9f093d
BA
662
663 // Some pieces movements
6808d7a1 664 static get steps() {
1c9f093d 665 return {
6808d7a1
BA
666 r: [
667 [-1, 0],
668 [1, 0],
669 [0, -1],
670 [0, 1]
671 ],
672 n: [
673 [-1, -2],
674 [-1, 2],
675 [1, -2],
676 [1, 2],
677 [-2, -1],
678 [-2, 1],
679 [2, -1],
680 [2, 1]
681 ],
682 b: [
683 [-1, -1],
684 [-1, 1],
685 [1, -1],
686 [1, 1]
687 ]
1c9f093d
BA
688 };
689 }
690
691 ////////////////////
692 // MOVES GENERATION
693
0ba6420d 694 // All possible moves from selected square
173f11dc
BA
695 getPotentialMovesFrom(sq) {
696 switch (this.getPiece(sq[0], sq[1])) {
697 case V.PAWN: return this.getPotentialPawnMoves(sq);
698 case V.ROOK: return this.getPotentialRookMoves(sq);
699 case V.KNIGHT: return this.getPotentialKnightMoves(sq);
700 case V.BISHOP: return this.getPotentialBishopMoves(sq);
701 case V.QUEEN: return this.getPotentialQueenMoves(sq);
702 case V.KING: return this.getPotentialKingMoves(sq);
1c9f093d 703 }
2a0672a9 704 return []; //never reached (but some variants may use it: Bario...)
1c9f093d
BA
705 }
706
707 // Build a regular move from its initial and destination squares.
708 // tr: transformation
6808d7a1 709 getBasicMove([sx, sy], [ex, ey], tr) {
1c58eb76 710 const initColor = this.getColor(sx, sy);
7e8a7ea1 711 const initPiece = this.board[sx][sy].charAt(1);
1c9f093d
BA
712 let mv = new Move({
713 appear: [
714 new PiPo({
715 x: ex,
716 y: ey,
173f11dc
BA
717 c: !!tr ? tr.c : initColor,
718 p: !!tr ? tr.p : initPiece
1c9f093d
BA
719 })
720 ],
721 vanish: [
722 new PiPo({
723 x: sx,
724 y: sy,
1c58eb76
BA
725 c: initColor,
726 p: initPiece
1c9f093d
BA
727 })
728 ]
729 });
730
731 // The opponent piece disappears if we take it
6808d7a1 732 if (this.board[ex][ey] != V.EMPTY) {
1c9f093d
BA
733 mv.vanish.push(
734 new PiPo({
735 x: ex,
736 y: ey,
6808d7a1 737 c: this.getColor(ex, ey),
7e8a7ea1 738 p: this.board[ex][ey].charAt(1)
1c9f093d
BA
739 })
740 );
741 }
1c5bfdf2 742
1c9f093d
BA
743 return mv;
744 }
745
746 // Generic method to find possible moves of non-pawn pieces:
747 // "sliding or jumping"
6808d7a1 748 getSlideNJumpMoves([x, y], steps, oneStep) {
1c9f093d 749 let moves = [];
6808d7a1 750 outerLoop: for (let step of steps) {
1c9f093d
BA
751 let i = x + step[0];
752 let j = y + step[1];
6808d7a1
BA
753 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
754 moves.push(this.getBasicMove([x, y], [i, j]));
3208c667 755 if (!!oneStep) continue outerLoop;
1c9f093d
BA
756 i += step[0];
757 j += step[1];
758 }
6808d7a1
BA
759 if (V.OnBoard(i, j) && this.canTake([x, y], [i, j]))
760 moves.push(this.getBasicMove([x, y], [i, j]));
1c9f093d
BA
761 }
762 return moves;
763 }
764
32f6285e
BA
765 // Special case of en-passant captures: treated separately
766 getEnpassantCaptures([x, y], shiftX) {
767 const Lep = this.epSquares.length;
768 const epSquare = this.epSquares[Lep - 1]; //always at least one element
769 let enpassantMove = null;
770 if (
771 !!epSquare &&
772 epSquare.x == x + shiftX &&
773 Math.abs(epSquare.y - y) == 1
774 ) {
775 enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
776 enpassantMove.vanish.push({
777 x: x,
778 y: epSquare.y,
7e8a7ea1 779 p: this.board[x][epSquare.y].charAt(1),
32f6285e
BA
780 c: this.getColor(x, epSquare.y)
781 });
782 }
783 return !!enpassantMove ? [enpassantMove] : [];
784 }
785
1c58eb76
BA
786 // Consider all potential promotions:
787 addPawnMoves([x1, y1], [x2, y2], moves, promotions) {
788 let finalPieces = [V.PAWN];
af34341d 789 const color = this.turn; //this.getColor(x1, y1);
1c58eb76
BA
790 const lastRank = (color == "w" ? 0 : V.size.x - 1);
791 if (x2 == lastRank) {
792 // promotions arg: special override for Hiddenqueen variant
793 if (!!promotions) finalPieces = promotions;
15d69043 794 else if (!!V.PawnSpecs.promotions) finalPieces = V.PawnSpecs.promotions;
1c58eb76 795 }
1c58eb76 796 for (let piece of finalPieces) {
3b98a861 797 const tr = (piece != V.PAWN ? { c: color, p: piece } : null);
1c58eb76
BA
798 moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
799 }
800 }
801
1c9f093d 802 // What are the pawn moves from square x,y ?
32f6285e 803 getPotentialPawnMoves([x, y], promotions) {
af34341d 804 const color = this.turn; //this.getColor(x, y);
6808d7a1 805 const [sizeX, sizeY] = [V.size.x, V.size.y];
32f6285e 806 const pawnShiftX = V.PawnSpecs.directions[color];
1c58eb76 807 const firstRank = (color == "w" ? sizeX - 1 : 0);
0b8bd121 808 const forward = (color == 'w' ? -1 : 1);
32f6285e
BA
809
810 // Pawn movements in shiftX direction:
811 const getPawnMoves = (shiftX) => {
812 let moves = [];
813 // NOTE: next condition is generally true (no pawn on last rank)
814 if (x + shiftX >= 0 && x + shiftX < sizeX) {
815 if (this.board[x + shiftX][y] == V.EMPTY) {
0b8bd121 816 // One square forward (or backward)
1c58eb76 817 this.addPawnMoves([x, y], [x + shiftX, y], moves, promotions);
32f6285e
BA
818 // Next condition because pawns on 1st rank can generally jump
819 if (
820 V.PawnSpecs.twoSquares &&
472c0c4f
BA
821 (
822 (color == 'w' && x >= V.size.x - 1 - V.PawnSpecs.initShift['w'])
823 ||
824 (color == 'b' && x <= V.PawnSpecs.initShift['b'])
825 )
32f6285e 826 ) {
0b8bd121
BA
827 if (
828 shiftX == forward &&
829 this.board[x + 2 * shiftX][y] == V.EMPTY
830 ) {
472c0c4f
BA
831 // Two squares jump
832 moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
833 if (
834 V.PawnSpecs.threeSquares &&
835 this.board[x + 3 * shiftX][y] == V.EMPTY
836 ) {
837 // Three squares jump
838 moves.push(this.getBasicMove([x, y], [x + 3 * shiftX, y]));
839 }
840 }
32f6285e
BA
841 }
842 }
843 // Captures
844 if (V.PawnSpecs.canCapture) {
845 for (let shiftY of [-1, 1]) {
15d69043 846 if (y + shiftY >= 0 && y + shiftY < sizeY) {
32f6285e
BA
847 if (
848 this.board[x + shiftX][y + shiftY] != V.EMPTY &&
849 this.canTake([x, y], [x + shiftX, y + shiftY])
850 ) {
1c58eb76
BA
851 this.addPawnMoves(
852 [x, y], [x + shiftX, y + shiftY],
853 moves, promotions
854 );
32f6285e
BA
855 }
856 if (
0b8bd121 857 V.PawnSpecs.captureBackward && shiftX == forward &&
32f6285e
BA
858 x - shiftX >= 0 && x - shiftX < V.size.x &&
859 this.board[x - shiftX][y + shiftY] != V.EMPTY &&
860 this.canTake([x, y], [x - shiftX, y + shiftY])
861 ) {
1c58eb76 862 this.addPawnMoves(
0b8bd121 863 [x, y], [x - shiftX, y + shiftY],
1c58eb76
BA
864 moves, promotions
865 );
32f6285e
BA
866 }
867 }
1c9f093d
BA
868 }
869 }
870 }
32f6285e 871 return moves;
1c9f093d
BA
872 }
873
32f6285e
BA
874 let pMoves = getPawnMoves(pawnShiftX);
875 if (V.PawnSpecs.bidirectional)
876 pMoves = pMoves.concat(getPawnMoves(-pawnShiftX));
877
6808d7a1 878 if (V.HasEnpassant) {
32f6285e
BA
879 // NOTE: backward en-passant captures are not considered
880 // because no rules define them (for now).
881 Array.prototype.push.apply(
882 pMoves,
883 this.getEnpassantCaptures([x, y], pawnShiftX)
884 );
1c9f093d 885 }
294fe29f 886
32f6285e 887 return pMoves;
1c9f093d
BA
888 }
889
890 // What are the rook moves from square x,y ?
6808d7a1 891 getPotentialRookMoves(sq) {
1c9f093d
BA
892 return this.getSlideNJumpMoves(sq, V.steps[V.ROOK]);
893 }
894
895 // What are the knight moves from square x,y ?
6808d7a1 896 getPotentialKnightMoves(sq) {
1c9f093d
BA
897 return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep");
898 }
899
900 // What are the bishop moves from square x,y ?
6808d7a1 901 getPotentialBishopMoves(sq) {
1c9f093d
BA
902 return this.getSlideNJumpMoves(sq, V.steps[V.BISHOP]);
903 }
904
905 // What are the queen moves from square x,y ?
6808d7a1
BA
906 getPotentialQueenMoves(sq) {
907 return this.getSlideNJumpMoves(
908 sq,
909 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
910 );
1c9f093d
BA
911 }
912
913 // What are the king moves from square x,y ?
6808d7a1 914 getPotentialKingMoves(sq) {
1c9f093d 915 // Initialize with normal moves
c583ef1c 916 let moves = this.getSlideNJumpMoves(
6808d7a1
BA
917 sq,
918 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
919 "oneStep"
920 );
7e8a7ea1
BA
921 if (V.HasCastle && this.castleFlags[this.turn].some(v => v < V.size.y))
922 moves = moves.concat(this.getCastleMoves(sq));
c583ef1c 923 return moves;
1c9f093d
BA
924 }
925
a6836242 926 // "castleInCheck" arg to let some variants castle under check
7e8a7ea1 927 getCastleMoves([x, y], finalSquares, castleInCheck, castleWith) {
6808d7a1 928 const c = this.getColor(x, y);
1c9f093d
BA
929
930 // Castling ?
931 const oppCol = V.GetOppCol(c);
932 let moves = [];
9bd6786b 933 // King, then rook:
7e8a7ea1
BA
934 finalSquares = finalSquares || [ [2, 3], [V.size.y - 2, V.size.y - 3] ];
935 const castlingKing = this.board[x][y].charAt(1);
6808d7a1
BA
936 castlingCheck: for (
937 let castleSide = 0;
938 castleSide < 2;
939 castleSide++ //large, then small
940 ) {
3a2a7b5f 941 if (this.castleFlags[c][castleSide] >= V.size.y) continue;
3f22c2c3 942 // If this code is reached, rook and king are on initial position
1c9f093d 943
2c5d7b20 944 // NOTE: in some variants this is not a rook
32f6285e 945 const rookPos = this.castleFlags[c][castleSide];
7e8a7ea1 946 const castlingPiece = this.board[x][rookPos].charAt(1);
85a1dcba
BA
947 if (
948 this.board[x][rookPos] == V.EMPTY ||
949 this.getColor(x, rookPos) != c ||
7e8a7ea1 950 (!!castleWith && !castleWith.includes(castlingPiece))
85a1dcba 951 ) {
61656127 952 // Rook is not here, or changed color (see Benedict)
32f6285e 953 continue;
85a1dcba 954 }
32f6285e 955
2beba6db
BA
956 // Nothing on the path of the king ? (and no checks)
957 const finDist = finalSquares[castleSide][0] - y;
958 let step = finDist / Math.max(1, Math.abs(finDist));
059f0aa2 959 let i = y;
6808d7a1
BA
960 do {
961 if (
7e8a7ea1
BA
962 (!castleInCheck && this.isAttacked([x, i], oppCol)) ||
963 (
964 this.board[x][i] != V.EMPTY &&
6808d7a1 965 // NOTE: next check is enough, because of chessboard constraints
7e8a7ea1
BA
966 (this.getColor(x, i) != c || ![y, rookPos].includes(i))
967 )
6808d7a1 968 ) {
1c9f093d
BA
969 continue castlingCheck;
970 }
2beba6db 971 i += step;
6808d7a1 972 } while (i != finalSquares[castleSide][0]);
1c9f093d
BA
973
974 // Nothing on the path to the rook?
6808d7a1 975 step = castleSide == 0 ? -1 : 1;
3a2a7b5f 976 for (i = y + step; i != rookPos; i += step) {
6808d7a1 977 if (this.board[x][i] != V.EMPTY) continue castlingCheck;
1c9f093d 978 }
1c9f093d
BA
979
980 // Nothing on final squares, except maybe king and castling rook?
6808d7a1
BA
981 for (i = 0; i < 2; i++) {
982 if (
5e1bc651 983 finalSquares[castleSide][i] != rookPos &&
6808d7a1 984 this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
5e1bc651 985 (
7e8a7ea1 986 finalSquares[castleSide][i] != y ||
5e1bc651
BA
987 this.getColor(x, finalSquares[castleSide][i]) != c
988 )
6808d7a1 989 ) {
1c9f093d
BA
990 continue castlingCheck;
991 }
992 }
993
994 // If this code is reached, castle is valid
6808d7a1
BA
995 moves.push(
996 new Move({
997 appear: [
2c5d7b20
BA
998 new PiPo({
999 x: x,
1000 y: finalSquares[castleSide][0],
7e8a7ea1 1001 p: castlingKing,
2c5d7b20
BA
1002 c: c
1003 }),
1004 new PiPo({
1005 x: x,
1006 y: finalSquares[castleSide][1],
1007 p: castlingPiece,
1008 c: c
1009 })
6808d7a1
BA
1010 ],
1011 vanish: [
7e8a7ea1 1012 // King might be initially disguised (Titan...)
a19caec0 1013 new PiPo({ x: x, y: y, p: castlingKing, c: c }),
a6836242 1014 new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
6808d7a1
BA
1015 ],
1016 end:
1017 Math.abs(y - rookPos) <= 2
1018 ? { x: x, y: rookPos }
1019 : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
1020 })
1021 );
1c9f093d
BA
1022 }
1023
1024 return moves;
1025 }
1026
1027 ////////////////////
1028 // MOVES VALIDATION
1029
1030 // For the interface: possible moves for the current turn from square sq
6808d7a1
BA
1031 getPossibleMovesFrom(sq) {
1032 return this.filterValid(this.getPotentialMovesFrom(sq));
1c9f093d
BA
1033 }
1034
1035 // TODO: promotions (into R,B,N,Q) should be filtered only once
6808d7a1
BA
1036 filterValid(moves) {
1037 if (moves.length == 0) return [];
1c9f093d
BA
1038 const color = this.turn;
1039 return moves.filter(m => {
1040 this.play(m);
1041 const res = !this.underCheck(color);
1042 this.undo(m);
1043 return res;
1044 });
1045 }
1046
5e1bc651 1047 getAllPotentialMoves() {
1c9f093d 1048 const color = this.turn;
1c9f093d 1049 let potentialMoves = [];
6808d7a1
BA
1050 for (let i = 0; i < V.size.x; i++) {
1051 for (let j = 0; j < V.size.y; j++) {
156986e6 1052 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
6808d7a1
BA
1053 Array.prototype.push.apply(
1054 potentialMoves,
1055 this.getPotentialMovesFrom([i, j])
1056 );
1c9f093d
BA
1057 }
1058 }
1059 }
5e1bc651
BA
1060 return potentialMoves;
1061 }
1062
1063 // Search for all valid moves considering current turn
1064 // (for engine and game end)
1065 getAllValidMoves() {
1066 return this.filterValid(this.getAllPotentialMoves());
1c9f093d
BA
1067 }
1068
1069 // Stop at the first move found
2c5d7b20 1070 // TODO: not really, it explores all moves from a square (one is enough).
cdab5663
BA
1071 // Possible fix: add extra arg "oneMove" to getPotentialMovesFrom,
1072 // and then return only boolean true at first move found
1073 // (in all getPotentialXXXMoves() ... for all variants ...)
6808d7a1 1074 atLeastOneMove() {
1c9f093d 1075 const color = this.turn;
6808d7a1
BA
1076 for (let i = 0; i < V.size.x; i++) {
1077 for (let j = 0; j < V.size.y; j++) {
665eed90 1078 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
6808d7a1
BA
1079 const moves = this.getPotentialMovesFrom([i, j]);
1080 if (moves.length > 0) {
107dc1bd 1081 for (let k = 0; k < moves.length; k++)
6808d7a1 1082 if (this.filterValid([moves[k]]).length > 0) return true;
1c9f093d
BA
1083 }
1084 }
1085 }
1086 }
1087 return false;
1088 }
1089
68e19a44
BA
1090 // Check if pieces of given color are attacking (king) on square x,y
1091 isAttacked(sq, color) {
6808d7a1 1092 return (
68e19a44
BA
1093 this.isAttackedByPawn(sq, color) ||
1094 this.isAttackedByRook(sq, color) ||
1095 this.isAttackedByKnight(sq, color) ||
1096 this.isAttackedByBishop(sq, color) ||
1097 this.isAttackedByQueen(sq, color) ||
1098 this.isAttackedByKing(sq, color)
6808d7a1 1099 );
1c9f093d
BA
1100 }
1101
d1be8046 1102 // Generic method for non-pawn pieces ("sliding or jumping"):
68e19a44
BA
1103 // is x,y attacked by a piece of given color ?
1104 isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
d1be8046
BA
1105 for (let step of steps) {
1106 let rx = x + step[0],
1107 ry = y + step[1];
1108 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
1109 rx += step[0];
1110 ry += step[1];
1111 }
1112 if (
1113 V.OnBoard(rx, ry) &&
3cf54395 1114 this.board[rx][ry] != V.EMPTY &&
68e19a44 1115 this.getPiece(rx, ry) == piece &&
da9e846e 1116 this.getColor(rx, ry) == color
d1be8046
BA
1117 ) {
1118 return true;
1119 }
1120 }
1121 return false;
1122 }
1123
68e19a44 1124 // Is square x,y attacked by 'color' pawns ?
107dc1bd 1125 isAttackedByPawn(sq, color) {
68e19a44 1126 const pawnShift = (color == "w" ? 1 : -1);
107dc1bd
BA
1127 return this.isAttackedBySlideNJump(
1128 sq,
1129 color,
1130 V.PAWN,
1131 [[pawnShift, 1], [pawnShift, -1]],
1132 "oneStep"
1133 );
1c9f093d
BA
1134 }
1135
68e19a44
BA
1136 // Is square x,y attacked by 'color' rooks ?
1137 isAttackedByRook(sq, color) {
1138 return this.isAttackedBySlideNJump(sq, color, V.ROOK, V.steps[V.ROOK]);
1c9f093d
BA
1139 }
1140
68e19a44
BA
1141 // Is square x,y attacked by 'color' knights ?
1142 isAttackedByKnight(sq, color) {
6808d7a1
BA
1143 return this.isAttackedBySlideNJump(
1144 sq,
68e19a44 1145 color,
6808d7a1
BA
1146 V.KNIGHT,
1147 V.steps[V.KNIGHT],
1148 "oneStep"
1149 );
1c9f093d
BA
1150 }
1151
68e19a44
BA
1152 // Is square x,y attacked by 'color' bishops ?
1153 isAttackedByBishop(sq, color) {
1154 return this.isAttackedBySlideNJump(sq, color, V.BISHOP, V.steps[V.BISHOP]);
1c9f093d
BA
1155 }
1156
68e19a44
BA
1157 // Is square x,y attacked by 'color' queens ?
1158 isAttackedByQueen(sq, color) {
6808d7a1
BA
1159 return this.isAttackedBySlideNJump(
1160 sq,
68e19a44 1161 color,
6808d7a1
BA
1162 V.QUEEN,
1163 V.steps[V.ROOK].concat(V.steps[V.BISHOP])
1164 );
1c9f093d
BA
1165 }
1166
68e19a44
BA
1167 // Is square x,y attacked by 'color' king(s) ?
1168 isAttackedByKing(sq, color) {
6808d7a1
BA
1169 return this.isAttackedBySlideNJump(
1170 sq,
68e19a44 1171 color,
6808d7a1
BA
1172 V.KING,
1173 V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
1174 "oneStep"
1175 );
1c9f093d
BA
1176 }
1177
1c9f093d 1178 // Is color under check after his move ?
6808d7a1 1179 underCheck(color) {
1c58eb76 1180 return this.isAttacked(this.kingPos[color], V.GetOppCol(color));
1c9f093d
BA
1181 }
1182
1183 /////////////////
1184 // MOVES PLAYING
1185
1186 // Apply a move on board
6808d7a1
BA
1187 static PlayOnBoard(board, move) {
1188 for (let psq of move.vanish) board[psq.x][psq.y] = V.EMPTY;
1189 for (let psq of move.appear) board[psq.x][psq.y] = psq.c + psq.p;
1c9f093d
BA
1190 }
1191 // Un-apply the played move
6808d7a1
BA
1192 static UndoOnBoard(board, move) {
1193 for (let psq of move.appear) board[psq.x][psq.y] = V.EMPTY;
1194 for (let psq of move.vanish) board[psq.x][psq.y] = psq.c + psq.p;
1c9f093d
BA
1195 }
1196
3a2a7b5f
BA
1197 prePlay() {}
1198
1199 play(move) {
1200 // DEBUG:
1201// if (!this.states) this.states = [];
1c58eb76 1202// const stateFen = this.getFen() + JSON.stringify(this.kingPos);
3a2a7b5f
BA
1203// this.states.push(stateFen);
1204
1205 this.prePlay(move);
2c5d7b20
BA
1206 // Save flags (for undo)
1207 if (V.HasFlags) move.flags = JSON.stringify(this.aggregateFlags());
3a2a7b5f
BA
1208 if (V.HasEnpassant) this.epSquares.push(this.getEpSquare(move));
1209 V.PlayOnBoard(this.board, move);
1210 this.turn = V.GetOppCol(this.turn);
1211 this.movesCount++;
1212 this.postPlay(move);
1213 }
1214
a9e1202b 1215 updateCastleFlags(move, piece, color) {
4258b58c 1216 // TODO: check flags. If already off, no need to always re-evaluate
a9e1202b 1217 const c = color || V.GetOppCol(this.turn);
1c58eb76
BA
1218 const firstRank = (c == "w" ? V.size.x - 1 : 0);
1219 // Update castling flags if rooks are moved
c7550017 1220 const oppCol = this.turn;
1c58eb76 1221 const oppFirstRank = V.size.x - 1 - firstRank;
bb688df5
BA
1222 if (piece == V.KING && move.appear.length > 0)
1223 this.castleFlags[c] = [V.size.y, V.size.y];
1224 else if (
1c58eb76
BA
1225 move.start.x == firstRank && //our rook moves?
1226 this.castleFlags[c].includes(move.start.y)
1227 ) {
1228 const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
1229 this.castleFlags[c][flagIdx] = V.size.y;
305ede7e
BA
1230 }
1231 // NOTE: not "else if" because a rook could take an opposing rook
1232 if (
1c58eb76
BA
1233 move.end.x == oppFirstRank && //we took opponent rook?
1234 this.castleFlags[oppCol].includes(move.end.y)
1235 ) {
1236 const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
1237 this.castleFlags[oppCol][flagIdx] = V.size.y;
1238 }
1239 }
1240
1c9f093d 1241 // After move is played, update variables + flags
3a2a7b5f
BA
1242 postPlay(move) {
1243 const c = V.GetOppCol(this.turn);
1c9f093d 1244 let piece = undefined;
3a2a7b5f 1245 if (move.vanish.length >= 1)
1c9f093d
BA
1246 // Usual case, something is moved
1247 piece = move.vanish[0].p;
3a2a7b5f 1248 else
1c9f093d
BA
1249 // Crazyhouse-like variants
1250 piece = move.appear[0].p;
1c9f093d
BA
1251
1252 // Update king position + flags
964eda04
BA
1253 if (piece == V.KING && move.appear.length > 0)
1254 this.kingPos[c] = [move.appear[0].x, move.appear[0].y];
bb688df5 1255 if (V.HasCastle) this.updateCastleFlags(move, piece);
1c9f093d
BA
1256 }
1257
3a2a7b5f 1258 preUndo() {}
1c9f093d 1259
6808d7a1 1260 undo(move) {
3a2a7b5f 1261 this.preUndo(move);
6808d7a1
BA
1262 if (V.HasEnpassant) this.epSquares.pop();
1263 if (V.HasFlags) this.disaggregateFlags(JSON.parse(move.flags));
1c9f093d
BA
1264 V.UndoOnBoard(this.board, move);
1265 this.turn = V.GetOppCol(this.turn);
1266 this.movesCount--;
3a2a7b5f 1267 this.postUndo(move);
1c9f093d
BA
1268
1269 // DEBUG:
1c58eb76 1270// const stateFen = this.getFen() + JSON.stringify(this.kingPos);
9bd6786b
BA
1271// if (stateFen != this.states[this.states.length-1]) debugger;
1272// this.states.pop();
1c9f093d
BA
1273 }
1274
3a2a7b5f
BA
1275 // After move is undo-ed *and flags resetted*, un-update other variables
1276 // TODO: more symmetry, by storing flags increment in move (?!)
1277 postUndo(move) {
1278 // (Potentially) Reset king position
1279 const c = this.getColor(move.start.x, move.start.y);
1280 if (this.getPiece(move.start.x, move.start.y) == V.KING)
1281 this.kingPos[c] = [move.start.x, move.start.y];
1282 }
1283
1c9f093d
BA
1284 ///////////////
1285 // END OF GAME
1286
1287 // What is the score ? (Interesting if game is over)
6808d7a1 1288 getCurrentScore() {
bb688df5 1289 if (this.atLeastOneMove()) return "*";
1c9f093d
BA
1290 // Game over
1291 const color = this.turn;
1292 // No valid move: stalemate or checkmate?
bb688df5 1293 if (!this.underCheck(color)) return "1/2";
1c9f093d 1294 // OK, checkmate
68e19a44 1295 return (color == "w" ? "0-1" : "1-0");
1c9f093d
BA
1296 }
1297
1298 ///////////////
1299 // ENGINE PLAY
1300
1301 // Pieces values
6808d7a1 1302 static get VALUES() {
1c9f093d 1303 return {
6808d7a1
BA
1304 p: 1,
1305 r: 5,
1306 n: 3,
1307 b: 3,
1308 q: 9,
1309 k: 1000
1c9f093d
BA
1310 };
1311 }
1312
1313 // "Checkmate" (unreachable eval)
6808d7a1
BA
1314 static get INFINITY() {
1315 return 9999;
1316 }
1c9f093d
BA
1317
1318 // At this value or above, the game is over
6808d7a1
BA
1319 static get THRESHOLD_MATE() {
1320 return V.INFINITY;
1321 }
1c9f093d 1322
2c5d7b20 1323 // Search depth: 1,2 for e.g. higher branching factor, 4 for smaller
6808d7a1
BA
1324 static get SEARCH_DEPTH() {
1325 return 3;
1326 }
1c9f093d 1327
af34341d
BA
1328 // 'movesList' arg for some variants to provide a custom list
1329 getComputerMove(movesList) {
1c9f093d
BA
1330 const maxeval = V.INFINITY;
1331 const color = this.turn;
af34341d 1332 let moves1 = movesList || this.getAllValidMoves();
c322a844 1333
6808d7a1 1334 if (moves1.length == 0)
e71161fb 1335 // TODO: this situation should not happen
41cb9b94 1336 return null;
1c9f093d 1337
b83a675a 1338 // Rank moves using a min-max at depth 2 (if search_depth >= 2!)
6808d7a1 1339 for (let i = 0; i < moves1.length; i++) {
afbf3ca7
BA
1340 this.play(moves1[i]);
1341 const score1 = this.getCurrentScore();
1342 if (score1 != "*") {
1343 moves1[i].eval =
1344 score1 == "1/2"
1345 ? 0
1346 : (score1 == "1-0" ? 1 : -1) * maxeval;
1347 }
1348 if (V.SEARCH_DEPTH == 1 || score1 != "*") {
1349 if (!moves1[i].eval) moves1[i].eval = this.evalPosition();
1350 this.undo(moves1[i]);
b83a675a
BA
1351 continue;
1352 }
1c9f093d 1353 // Initial self evaluation is very low: "I'm checkmated"
6808d7a1 1354 moves1[i].eval = (color == "w" ? -1 : 1) * maxeval;
afbf3ca7
BA
1355 // Initial enemy evaluation is very low too, for him
1356 let eval2 = (color == "w" ? 1 : -1) * maxeval;
1357 // Second half-move:
1358 let moves2 = this.getAllValidMoves();
1359 for (let j = 0; j < moves2.length; j++) {
1360 this.play(moves2[j]);
1361 const score2 = this.getCurrentScore();
1362 let evalPos = 0; //1/2 value
1363 switch (score2) {
1364 case "*":
1365 evalPos = this.evalPosition();
1366 break;
1367 case "1-0":
1368 evalPos = maxeval;
1369 break;
1370 case "0-1":
1371 evalPos = -maxeval;
1372 break;
1c9f093d 1373 }
afbf3ca7
BA
1374 if (
1375 (color == "w" && evalPos < eval2) ||
1376 (color == "b" && evalPos > eval2)
1377 ) {
1378 eval2 = evalPos;
1379 }
1380 this.undo(moves2[j]);
1381 }
6808d7a1
BA
1382 if (
1383 (color == "w" && eval2 > moves1[i].eval) ||
1384 (color == "b" && eval2 < moves1[i].eval)
1385 ) {
1c9f093d
BA
1386 moves1[i].eval = eval2;
1387 }
1388 this.undo(moves1[i]);
1389 }
6808d7a1
BA
1390 moves1.sort((a, b) => {
1391 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
1392 });
a97bdbda 1393// console.log(moves1.map(m => { return [this.getNotation(m), m.eval]; }));
1c9f093d 1394
1c9f093d 1395 // Skip depth 3+ if we found a checkmate (or if we are checkmated in 1...)
6808d7a1 1396 if (V.SEARCH_DEPTH >= 3 && Math.abs(moves1[0].eval) < V.THRESHOLD_MATE) {
6808d7a1 1397 for (let i = 0; i < moves1.length; i++) {
1c9f093d
BA
1398 this.play(moves1[i]);
1399 // 0.1 * oldEval : heuristic to avoid some bad moves (not all...)
6808d7a1
BA
1400 moves1[i].eval =
1401 0.1 * moves1[i].eval +
1402 this.alphabeta(V.SEARCH_DEPTH - 1, -maxeval, maxeval);
1c9f093d
BA
1403 this.undo(moves1[i]);
1404 }
6808d7a1
BA
1405 moves1.sort((a, b) => {
1406 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
1407 });
b83a675a 1408 }
1c9f093d 1409
b83a675a 1410 let candidates = [0];
d54f6261
BA
1411 for (let i = 1; i < moves1.length && moves1[i].eval == moves1[0].eval; i++)
1412 candidates.push(i);
656b1878 1413 return moves1[candidates[randInt(candidates.length)]];
1c9f093d
BA
1414 }
1415
6808d7a1 1416 alphabeta(depth, alpha, beta) {
1c9f093d
BA
1417 const maxeval = V.INFINITY;
1418 const color = this.turn;
1419 const score = this.getCurrentScore();
1420 if (score != "*")
6808d7a1
BA
1421 return score == "1/2" ? 0 : (score == "1-0" ? 1 : -1) * maxeval;
1422 if (depth == 0) return this.evalPosition();
a97bdbda 1423 const moves = this.getAllValidMoves();
6808d7a1
BA
1424 let v = color == "w" ? -maxeval : maxeval;
1425 if (color == "w") {
1426 for (let i = 0; i < moves.length; i++) {
1c9f093d 1427 this.play(moves[i]);
6808d7a1 1428 v = Math.max(v, this.alphabeta(depth - 1, alpha, beta));
1c9f093d
BA
1429 this.undo(moves[i]);
1430 alpha = Math.max(alpha, v);
6808d7a1 1431 if (alpha >= beta) break; //beta cutoff
1c9f093d 1432 }
1c5bfdf2 1433 }
6808d7a1 1434 else {
1c5bfdf2 1435 // color=="b"
6808d7a1 1436 for (let i = 0; i < moves.length; i++) {
1c9f093d 1437 this.play(moves[i]);
6808d7a1 1438 v = Math.min(v, this.alphabeta(depth - 1, alpha, beta));
1c9f093d
BA
1439 this.undo(moves[i]);
1440 beta = Math.min(beta, v);
6808d7a1 1441 if (alpha >= beta) break; //alpha cutoff
1c9f093d
BA
1442 }
1443 }
1444 return v;
1445 }
1446
6808d7a1 1447 evalPosition() {
1c9f093d
BA
1448 let evaluation = 0;
1449 // Just count material for now
6808d7a1
BA
1450 for (let i = 0; i < V.size.x; i++) {
1451 for (let j = 0; j < V.size.y; j++) {
1452 if (this.board[i][j] != V.EMPTY) {
1453 const sign = this.getColor(i, j) == "w" ? 1 : -1;
1454 evaluation += sign * V.VALUES[this.getPiece(i, j)];
1c9f093d
BA
1455 }
1456 }
1457 }
1458 return evaluation;
1459 }
1460
1461 /////////////////////////
1462 // MOVES + GAME NOTATION
1463 /////////////////////////
1464
1465 // Context: just before move is played, turn hasn't changed
1466 // TODO: un-ambiguous notation (switch on piece type, check directions...)
6808d7a1
BA
1467 getNotation(move) {
1468 if (move.appear.length == 2 && move.appear[0].p == V.KING)
1cd3e362 1469 // Castle
6808d7a1 1470 return move.end.y < move.start.y ? "0-0-0" : "0-0";
1c9f093d
BA
1471
1472 // Translate final square
1473 const finalSquare = V.CoordsToSquare(move.end);
1474
1475 const piece = this.getPiece(move.start.x, move.start.y);
6808d7a1 1476 if (piece == V.PAWN) {
1c9f093d
BA
1477 // Pawn move
1478 let notation = "";
6808d7a1 1479 if (move.vanish.length > move.appear.length) {
1c9f093d
BA
1480 // Capture
1481 const startColumn = V.CoordToColumn(move.start.y);
1482 notation = startColumn + "x" + finalSquare;
78d64531 1483 }
6808d7a1
BA
1484 else notation = finalSquare;
1485 if (move.appear.length > 0 && move.appear[0].p != V.PAWN)
78d64531 1486 // Promotion
1c9f093d
BA
1487 notation += "=" + move.appear[0].p.toUpperCase();
1488 return notation;
1489 }
6808d7a1
BA
1490 // Piece movement
1491 return (
1492 piece.toUpperCase() +
1493 (move.vanish.length > move.appear.length ? "x" : "") +
1494 finalSquare
1495 );
1496 }
2c5d7b20
BA
1497
1498 static GetUnambiguousNotation(move) {
1499 // Machine-readable format with all the informations about the move
1500 return (
1501 (!!move.start && V.OnBoard(move.start.x, move.start.y)
1502 ? V.CoordsToSquare(move.start)
1503 : "-"
1504 ) + "." +
1505 (!!move.end && V.OnBoard(move.end.x, move.end.y)
1506 ? V.CoordsToSquare(move.end)
1507 : "-"
1508 ) + " " +
1509 (!!move.appear && move.appear.length > 0
1510 ? move.appear.map(a =>
1511 a.c + a.p + V.CoordsToSquare({ x: a.x, y: a.y })).join(".")
1512 : "-"
1513 ) + "/" +
1514 (!!move.vanish && move.vanish.length > 0
1515 ? move.vanish.map(a =>
1516 a.c + a.p + V.CoordsToSquare({ x: a.x, y: a.y })).join(".")
1517 : "-"
1518 )
1519 );
1520 }
7e8a7ea1 1521
6808d7a1 1522};