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