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