Add unambiguous section in the PGN + some fixes + code formatting and fix typos
[vchess.git] / client / src / variants / Synchrone.js
CommitLineData
7ebc0408 1import { ChessRules } from "@/base_rules";
d54f6261 2import { randInt } from "@/utils/alea";
7ebc0408
BA
3
4export class SynchroneRules extends ChessRules {
d54f6261 5 static get CanAnalyze() {
6b9378a6 6 return false;
d54f6261
BA
7 }
8
9 static get ShowMoves() {
10 return "byrow";
11 }
12
13 static IsGoodFen(fen) {
14 if (!ChessRules.IsGoodFen(fen)) return false;
15 const fenParsed = V.ParseFen(fen);
16 // 5) Check whiteMove
17 if (
18 (
9edfb714 19 fenParsed.turn == "b" &&
d54f6261
BA
20 // NOTE: do not check really JSON stringified move...
21 (!fenParsed.whiteMove || fenParsed.whiteMove == "-")
22 )
23 ||
9edfb714 24 (fenParsed.turn == "w" && fenParsed.whiteMove != "-")
d54f6261
BA
25 ) {
26 return false;
27 }
28 return true;
29 }
30
31 static IsGoodEnpassant(enpassant) {
32 const epArray = enpassant.split(",");
33 if (![2, 3].includes(epArray.length)) return false;
34 epArray.forEach(epsq => {
35 if (epsq != "-") {
36 const ep = V.SquareToCoords(epsq);
37 if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
38 }
39 });
40 return true;
41 }
42
43 static ParseFen(fen) {
44 const fenParts = fen.split(" ");
45 return Object.assign(
46 ChessRules.ParseFen(fen),
47 { whiteMove: fenParts[5] }
48 );
49 }
50
51 static GenRandInitFen(randomness) {
52 return ChessRules.GenRandInitFen(randomness).slice(0, -1) + "-,- -";
53 }
54
55 getFen() {
56 return super.getFen() + " " + this.getWhitemoveFen();
57 }
58
59 getFenForRepeat() {
60 return super.getFenForRepeat() + "_" + this.getWhitemoveFen();
61 }
62
63 setOtherVariables(fen) {
64 const parsedFen = V.ParseFen(fen);
65 this.setFlags(parsedFen.flags);
66 const epArray = parsedFen.enpassant.split(",");
67 this.epSquares = [];
68 epArray.forEach(epsq => this.epSquares.push(this.getEpSquare(epsq)));
69 super.scanKings(fen);
70 // Also init whiteMove
71 this.whiteMove =
72 parsedFen.whiteMove != "-"
73 ? JSON.parse(parsedFen.whiteMove)
74 : null;
75 }
76
77 // After undo(): no need to re-set INIT_COL_KING
78 scanKings() {
79 this.kingPos = { w: [-1, -1], b: [-1, -1] };
80 for (let i = 0; i < V.size.x; i++) {
81 for (let j = 0; j < V.size.y; j++) {
82 if (this.getPiece(i, j) == V.KING)
83 this.kingPos[this.getColor(i, j)] = [i, j];
84 }
85 }
86 }
87
88 getEnpassantFen() {
89 const L = this.epSquares.length;
90 let res = "";
91 const start = L - 2 - (this.turn == 'b' ? 1 : 0);
92 for (let i=start; i < L; i++) {
93 if (!this.epSquares[i]) res += "-,";
94 else res += V.CoordsToSquare(this.epSquares[i]) + ",";
95 }
96 return res.slice(0, -1);
97 }
98
99 getWhitemoveFen() {
100 if (!this.whiteMove) return "-";
101 return JSON.stringify({
102 start: this.whiteMove.start,
103 end: this.whiteMove.end,
104 appear: this.whiteMove.appear,
105 vanish: this.whiteMove.vanish
106 });
107 }
108
d54f6261 109 getPossibleMovesFrom([x, y]) {
1a3cfdc0
BA
110 return (
111 this.filterValid(super.getPotentialMovesFrom([x, y]))
112 // Augment with potential recaptures:
113 .concat(this.getRecaptures())
114 );
d54f6261
BA
115 }
116
6b9378a6
BA
117 // Aux function used to find opponent and self captures
118 getCaptures(x, y, color) {
d54f6261
BA
119 const sliderAttack = (xx, yy, allowedSteps) => {
120 const deltaX = xx - x,
121 absDeltaX = Math.abs(deltaX);
122 const deltaY = yy - y,
123 absDeltaY = Math.abs(deltaY);
124 const step = [ deltaX / absDeltaX || 0, deltaY / absDeltaY || 0 ];
125 if (
126 // Check that the step is a priori valid:
127 (absDeltaX != absDeltaY && deltaX != 0 && deltaY != 0) ||
128 allowedSteps.every(st => st[0] != step[0] || st[1] != step[1])
129 ) {
130 return null;
131 }
132 let sq = [ x + step[0], y + step[1] ];
133 while (sq[0] != xx || sq[1] != yy) {
134 // NOTE: no need to check OnBoard in this special case
135 if (this.board[sq[0]][sq[1]] != V.EMPTY) return null;
136 sq[0] += step[0];
137 sq[1] += step[1];
138 }
139 return this.getBasicMove([xx, yy], [x, y]);
140 };
141 // Can I take on the square [x, y] ?
142 // If yes, return the (list of) capturing move(s)
143 let moves = [];
144 for (let i=0; i<8; i++) {
145 for (let j=0; j<8; j++) {
146 if (this.getColor(i, j) == color) {
147 switch (this.getPiece(i, j)) {
148 case V.PAWN: {
149 // Pushed pawns move as enemy pawns
150 const shift = (color == 'w' ? 1 : -1);
151 if (x + shift == i && Math.abs(y - j) == 1)
152 moves.push(this.getBasicMove([i, j], [x, y]));
153 break;
154 }
155 case V.KNIGHT: {
156 const deltaX = Math.abs(i - x);
157 const deltaY = Math.abs(j - y);
158 if (
159 deltaX + deltaY == 3 &&
160 [1, 2].includes(deltaX) &&
161 [1, 2].includes(deltaY)
162 ) {
163 moves.push(this.getBasicMove([i, j], [x, y]));
164 }
165 break;
166 }
167 case V.KING:
168 if (Math.abs(i - x) <= 1 && Math.abs(j - y) <= 1)
169 moves.push(this.getBasicMove([i, j], [x, y]));
170 break;
171 case V.ROOK: {
172 const mv = sliderAttack(i, j, V.steps[V.ROOK]);
173 if (!!mv) moves.push(mv);
174 break;
175 }
176 case V.BISHOP: {
177 const mv = sliderAttack(i, j, V.steps[V.BISHOP]);
178 if (!!mv) moves.push(mv);
179 break;
180 }
181 case V.QUEEN: {
182 const mv = sliderAttack(
183 i, j, V.steps[V.ROOK].concat(V.steps[V.BISHOP]));
184 if (!!mv) moves.push(mv);
185 break;
186 }
187 }
188 }
189 }
190 }
191 return this.filterValid(moves);
192 }
193
1a3cfdc0 194 getRecaptures() {
6b9378a6
BA
195 // 1) Generate all opponent's capturing moves
196 let oppCaptureMoves = [];
1a3cfdc0 197 const color = this.turn;
6b9378a6
BA
198 const oppCol = V.GetOppCol(color);
199 for (let i=0; i<8; i++) {
200 for (let j=0; j<8; j++) {
201 if (
202 this.getColor(i, j) == color &&
203 // Do not consider king captures: self-captures of king are forbidden
204 this.getPiece(i, j) != V.KING
205 ) {
206 Array.prototype.push.apply(
207 oppCaptureMoves,
208 this.getCaptures(i, j, oppCol)
209 );
210 }
211 }
212 }
213 // 2) Play each opponent's capture, and see if back-captures are possible:
d54f6261
BA
214 // Lookup table to quickly decide if a move is already in list:
215 let moveSet = {};
1a3cfdc0 216 let moves = [];
6b9378a6 217 oppCaptureMoves.forEach(m => {
2c5d7b20 218 // If another opponent capture with same endpoint already processed, skip
6b9378a6
BA
219 const mHash = "m" + m.end.x + m.end.y;
220 if (!moveSet[mHash]) {
221 moveSet[mHash] = true;
222 // Just make enemy piece disappear, to clear potential path:
223 const justDisappear = {
224 appear: [],
225 vanish: [m.vanish[0]]
226 };
227 V.PlayOnBoard(this.board, justDisappear);
228 // Can I take on [m.end.x, m.end.y] ? If yes, add to list:
2c5d7b20
BA
229 this.getCaptures(m.end.x, m.end.y, color)
230 .forEach(cm => moves.push(cm));
6b9378a6
BA
231 V.UndoOnBoard(this.board, justDisappear);
232 }
d54f6261 233 });
1a3cfdc0
BA
234 return moves;
235 }
236
237 getAllValidMoves() {
238 // Return possible moves + potential recaptures
239 return super.getAllValidMoves().concat(this.getRecaptures());
d54f6261
BA
240 }
241
242 filterValid(moves) {
243 if (moves.length == 0) return [];
244 // filterValid can be called when it's "not our turn":
245 const color = moves[0].vanish[0].c;
246 return moves.filter(m => {
247 const piece = m.vanish[0].p;
248 if (piece == V.KING) {
249 this.kingPos[color][0] = m.appear[0].x;
250 this.kingPos[color][1] = m.appear[0].y;
251 }
252 V.PlayOnBoard(this.board, m);
253 let res = !this.underCheck(color);
254 V.UndoOnBoard(this.board, m);
255 if (piece == V.KING) this.kingPos[color] = [m.start.x, m.start.y];
256 return res;
257 });
258 }
259
260 atLeastOneMove(color) {
261 const curTurn = this.turn;
262 this.turn = color;
263 const res = super.atLeastOneMove();
264 this.turn = curTurn;
265 return res;
266 }
267
268 // White and black (partial) moves were played: merge
269 resolveSynchroneMove(move) {
270 const m1 = this.whiteMove;
271 const m2 = move;
272 // For PlayOnBoard (no need for start / end, irrelevant)
273 let smove = {
274 appear: [],
275 vanish: [
276 m1.vanish[0],
277 m2.vanish[0]
278 ]
279 };
280 if ((m1.end.x != m2.end.x) || (m1.end.y != m2.end.y)) {
281 // Easy case: two independant moves (which may (self-)capture)
282 smove.appear.push(m1.appear[0]);
283 smove.appear.push(m2.appear[0]);
284 // "Captured" pieces may have moved:
6b9378a6
BA
285 if (m1.appear.length == 2) {
286 // Castle
287 smove.appear.push(m1.appear[1]);
288 smove.vanish.push(m1.vanish[1]);
289 } else if (
d54f6261
BA
290 m1.vanish.length == 2 &&
291 (
6b9378a6
BA
292 m1.vanish[1].x != m2.start.x ||
293 m1.vanish[1].y != m2.start.y
d54f6261
BA
294 )
295 ) {
296 smove.vanish.push(m1.vanish[1]);
297 }
6b9378a6
BA
298 if (m2.appear.length == 2) {
299 // Castle
300 smove.appear.push(m2.appear[1]);
301 smove.vanish.push(m2.vanish[1]);
302 } else if (
d54f6261
BA
303 m2.vanish.length == 2 &&
304 (
6b9378a6
BA
305 m2.vanish[1].x != m1.start.x ||
306 m2.vanish[1].y != m1.start.y
d54f6261
BA
307 )
308 ) {
309 smove.vanish.push(m2.vanish[1]);
310 }
311 } else {
312 // Collision:
313 if (m1.vanish.length == 1 && m2.vanish.length == 1) {
314 // Easy case: both disappear except if one is a king
315 const p1 = m1.vanish[0].p;
316 const p2 = m2.vanish[0].p;
317 if ([p1, p2].includes(V.KING)) {
318 smove.appear.push({
319 x: m1.end.x,
320 y: m1.end.y,
321 p: V.KING,
322 c: (p1 == V.KING ? 'w' : 'b')
323 });
324 }
325 } else {
326 // One move is a self-capture and the other a normal capture:
327 // only the self-capture appears
d54f6261
BA
328 const selfCaptureMove =
329 m1.vanish[1].c == m1.vanish[0].c
330 ? m1
331 : m2;
332 smove.appear.push({
333 x: m1.end.x,
334 y: m1.end.y,
335 p: selfCaptureMove.appear[0].p,
336 c: selfCaptureMove.vanish[0].c
337 });
6b9378a6
BA
338 smove.vanish.push({
339 x: m1.end.x,
340 y: m1.end.y,
341 p: selfCaptureMove.vanish[1].p,
342 c: selfCaptureMove.vanish[0].c
343 });
d54f6261
BA
344 }
345 }
346 return smove;
347 }
348
349 play(move) {
350 move.flags = JSON.stringify(this.aggregateFlags()); //save flags (for undo)
351 this.epSquares.push(this.getEpSquare(move));
352 // Do not play on board (would reveal the move...)
353 this.turn = V.GetOppCol(this.turn);
354 this.movesCount++;
355 this.postPlay(move);
356 }
357
358 updateCastleFlags(move) {
359 const firstRank = { 'w': V.size.x - 1, 'b': 0 };
360 move.appear.concat(move.vanish).forEach(av => {
361 for (let c of ['w', 'b']) {
362 if (av.x == firstRank[c] && this.castleFlags[c].includes(av.y)) {
363 const flagIdx = (av.y == this.castleFlags[c][0] ? 0 : 1);
364 this.castleFlags[c][flagIdx] = 8;
365 }
366 }
367 });
368 }
369
370 postPlay(move) {
371 if (this.turn == 'b') {
372 // NOTE: whiteMove is used read-only, so no need to copy
373 this.whiteMove = move;
374 return;
375 }
376
377 // A full turn just ended:
378 const smove = this.resolveSynchroneMove(move);
379 V.PlayOnBoard(this.board, smove);
6b9378a6 380 move.whiteMove = this.whiteMove; //for undo
d54f6261
BA
381 this.whiteMove = null;
382
383 // Update king position + flags
384 let kingAppear = { 'w': false, 'b': false };
385 for (let i=0; i<smove.appear.length; i++) {
386 if (smove.appear[i].p == V.KING) {
387 const c = smove.appear[i].c;
388 kingAppear[c] = true;
389 this.kingPos[c][0] = smove.appear[i].x;
390 this.kingPos[c][1] = smove.appear[i].y;
391 }
392 }
393 for (let i=0; i<smove.vanish.length; i++) {
394 if (smove.vanish[i].p == V.KING) {
395 const c = smove.vanish[i].c;
396 if (!kingAppear[c]) {
397 this.kingPos[c][0] = -1;
398 this.kingPos[c][1] = -1;
399 }
400 break;
401 }
402 }
403 this.updateCastleFlags(smove);
404 move.smove = smove;
405 }
406
407 undo(move) {
408 this.epSquares.pop();
409 this.disaggregateFlags(JSON.parse(move.flags));
410 if (this.turn == 'w')
411 // Back to the middle of the move
412 V.UndoOnBoard(this.board, move.smove);
413 this.turn = V.GetOppCol(this.turn);
414 this.movesCount--;
415 this.postUndo(move);
416 }
417
418 postUndo(move) {
6b9378a6 419 if (this.turn == 'w') {
d54f6261
BA
420 // Reset king positions: scan board
421 this.scanKings();
6b9378a6
BA
422 // Also reset whiteMove
423 this.whiteMove = null;
424 } else this.whiteMove = move.whiteMove;
d54f6261
BA
425 }
426
427 getCheckSquares(color) {
6b9378a6
BA
428 if (color == 'b') {
429 // kingPos must be reset for appropriate highlighting:
430 var lastMove = JSON.parse(JSON.stringify(this.whiteMove));
431 this.undo(lastMove); //will erase whiteMove, thus saved above
432 }
d54f6261 433 let res = [];
1a3cfdc0 434 if (this.kingPos['w'][0] >= 0 && this.underCheck('w'))
d54f6261 435 res.push(JSON.parse(JSON.stringify(this.kingPos['w'])));
1a3cfdc0 436 if (this.kingPos['b'][0] >= 0 && this.underCheck('b'))
d54f6261 437 res.push(JSON.parse(JSON.stringify(this.kingPos['b'])));
6b9378a6 438 if (color == 'b') this.play(lastMove);
d54f6261
BA
439 return res;
440 }
441
442 getCurrentScore() {
443 if (this.turn == 'b')
444 // Turn (white + black) not over yet
445 return "*";
446 // Was a king captured?
447 if (this.kingPos['w'][0] < 0) return "0-1";
448 if (this.kingPos['b'][0] < 0) return "1-0";
449 const whiteCanMove = this.atLeastOneMove('w');
450 const blackCanMove = this.atLeastOneMove('b');
451 if (whiteCanMove && blackCanMove) return "*";
452 // Game over
453 const whiteInCheck = this.underCheck('w');
454 const blackInCheck = this.underCheck('b');
455 if (
456 (whiteCanMove && !this.underCheck('b')) ||
457 (blackCanMove && !this.underCheck('w'))
458 ) {
459 return "1/2";
460 }
461 // Checkmate: could be mutual
462 if (!whiteCanMove && !blackCanMove) return "1/2";
463 return (whiteCanMove ? "1-0" : "0-1");
464 }
465
466 getComputerMove() {
467 const maxeval = V.INFINITY;
468 const color = this.turn;
469 let moves = this.getAllValidMoves();
470 if (moves.length == 0)
471 // TODO: this situation should not happen
472 return null;
473
474 if (Math.random() < 0.5)
475 // Return a random move
476 return moves[randInt(moves.length)];
477
478 // Rank moves at depth 1:
479 // try to capture something (not re-capturing)
480 moves.forEach(m => {
481 V.PlayOnBoard(this.board, m);
482 m.eval = this.evalPosition();
483 V.UndoOnBoard(this.board, m);
484 });
485 moves.sort((a, b) => {
486 return (color == "w" ? 1 : -1) * (b.eval - a.eval);
487 });
488 let candidates = [0];
489 for (let i = 1; i < moves.length && moves[i].eval == moves[0].eval; i++)
490 candidates.push(i);
491 return moves[candidates[randInt(candidates.length)]];
492 }
6b9378a6
BA
493
494 getNotation(move) {
495 if (move.appear.length == 2 && move.appear[0].p == V.KING)
496 // Castle
497 return move.end.y < move.start.y ? "0-0-0" : "0-0";
498 // Basic system: piece + init + dest square
499 return (
500 move.vanish[0].p.toUpperCase() +
501 V.CoordsToSquare(move.start) +
502 V.CoordsToSquare(move.end)
503 );
504 }
7ebc0408 505};