Fixes. Dynamo draft almost working
[vchess.git] / client / src / variants / Dynamo.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2
3 export class DynamoRules extends ChessRules {
4 // TODO: later, allow to push out pawns on a and h files
5 static get HasEnpassant() {
6 return false;
7 }
8
9 canIplay(side, [x, y]) {
10 // Sometimes opponent's pieces can be moved directly
11 return true;
12 }
13
14 setOtherVariables(fen) {
15 super.setOtherVariables(fen);
16 this.subTurn = 1;
17 // Local stack of "action moves"
18 this.amoves = [];
19 const amove = V.ParseFen(fen).amove;
20 if (amove != "-") {
21 const amoveParts = amove.split("/");
22 let amove = {
23 // No need for start & end
24 appear: [],
25 vanish: []
26 };
27 [0, 1].map(i => {
28 amoveParts[i].split(".").forEach(av => {
29 // Format is "bpe3"
30 const xy = V.SquareToCoords(av.substr(2));
31 move[i == 0 ? "appear" : "vanish"].push(
32 new PiPo({
33 x: xy.x,
34 y: xy.y,
35 c: av[0],
36 p: av[1]
37 })
38 );
39 });
40 });
41 this.amoves.push(move);
42 }
43 this.subTurn = 1;
44 // Stack "first moves" (on subTurn 1) to merge and check opposite moves
45 this.firstMove = [];
46 }
47
48 static ParseFen(fen) {
49 return Object.assign(
50 ChessRules.ParseFen(fen),
51 { amove: fen.split(" ")[4] }
52 );
53 }
54
55 static IsGoodFen(fen) {
56 if (!ChessRules.IsGoodFen(fen)) return false;
57 const fenParts = fen.split(" ");
58 if (fenParts.length != 6) return false;
59 if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/))
60 return false;
61 return true;
62 }
63
64 getFen() {
65 return super.getFen() + " " + this.getAmoveFen();
66 }
67
68 getFenForRepeat() {
69 return super.getFenForRepeat() + "_" + this.getAmoveFen();
70 }
71
72 getAmoveFen() {
73 const L = this.amoves.length;
74 if (L == 0) return "-";
75 return (
76 ["appear","vanish"].map(
77 mpart => {
78 return (
79 this.amoves[L-1][mpart].map(
80 av => {
81 const square = V.CoordsToSquare({ x: av.x, y: av.y });
82 return av.c + av.p + square;
83 }
84 ).join(".")
85 );
86 }
87 ).join("/")
88 );
89 }
90
91 canTake() {
92 // Captures don't occur (only pulls & pushes)
93 return false;
94 }
95
96 // Step is right, just add (push/pull) moves in this direction
97 // Direction is assumed normalized.
98 getMovesInDirection([x, y], [dx, dy], nbSteps) {
99 nbSteps = nbSteps || 8; //max 8 steps anyway
100 let [i, j] = [x + dx, y + dy];
101 let moves = [];
102 const color = this.getColor(x, y);
103 const piece = this.getPiece(x, y);
104 const lastRank = (color == 'w' ? 0 : 7);
105 let counter = 1;
106 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
107 if (i == lastRank && piece == V.PAWN) {
108 // Promotion by push or pull
109 V.PawnSpecs.promotions.forEach(p => {
110 let move = super.getBasicMove([x, y], [i, j], { c: color, p: p });
111 moves.push(move);
112 });
113 }
114 else moves.push(super.getBasicMove([x, y], [i, j]));
115 if (++counter > nbSteps) break;
116 i += dx;
117 j += dy;
118 }
119 if (!V.OnBoard(i, j) && piece != V.KING) {
120 // Add special "exit" move, by "taking king"
121 moves.push(
122 new Move({
123 start: { x: x, y: y },
124 end: { x: this.kingPos[color][0], y: this.kingPos[color][1] },
125 appear: [],
126 vanish: [{ x: x, y: y, c: color, p: piece }]
127 })
128 );
129 }
130 return moves;
131 }
132
133 // Normalize direction to know the step
134 getNormalizedDirection([dx, dy]) {
135 const absDir = [Math.abs(dx), Math.abs(dy)];
136 let divisor = 0;
137 if (absDir[0] != 0 && absDir[1] != 0 && absDir[0] != absDir[1])
138 // Knight
139 divisor = Math.min(absDir[0], absDir[1]);
140 else
141 // Standard slider (or maybe a pawn or king: same)
142 divisor = Math.max(absDir[0], absDir[1]);
143 return [dx / divisor, dy / divisor];
144 }
145
146 // There is something on x2,y2, maybe our color, pushed/pulled
147 static IsAprioriValidMove([x1, y1], [x2, y2]) {
148 const color1 = this.getColor(x1, y1);
149 const color2 = this.getColor(x2, y2);
150 const pawnShift = (color1 == 'w' ? -1 : 1);
151 const pawnStartRank = (color1 == 'w' ? 6 : 1);
152 const deltaX = Math.abs(x1 - x2);
153 const deltaY = Math.abs(y1 - y2);
154 switch (this.getPiece(x1, y1)) {
155 case V.PAWN:
156 return (
157 (
158 color1 == color2 &&
159 y1 == y2 &&
160 (
161 x1 + pawnShift == x2 ||
162 x1 == pawnStartRank && x1 + 2 * pawnShift == x2
163 )
164 )
165 ||
166 (
167 color1 != color2 &&
168 deltaY == 1 &&
169 x1 + pawnShift == x2
170 )
171 );
172 case V.ROOK:
173 return (x1 == x2 || y1 == y2);
174 case V.KNIGHT: {
175 return (deltaX + deltaY == 3 && (deltaX == 1 || deltaY == 1));
176 }
177 case V.BISHOP:
178 return (deltaX == deltaY);
179 case V.QUEEN:
180 return (
181 (deltaX == 0 || deltaY == 0 || deltaX == deltaY)
182 );
183 case V.KING:
184 return (deltaX <= 1 && deltaY <= 1);
185 }
186 return false;
187 }
188
189 // NOTE: for pushes, play the pushed piece first.
190 // for pulls: play the piece doing the action first
191 // NOTE: to push a piece out of the board, make it slide until its king
192 getPotentialMovesFrom([x, y]) {
193 const color = this.turn;
194 if (this.subTurn == 1) {
195 const getMoveHash = (m) => {
196 return V.CoordsToSquare(m.start) + V.CoordsToSquare(m.end);
197 };
198 const addMoves = (dir, nbSteps) => {
199 const newMoves =
200 this.getMovesInDirection([x, y], [-dir[0], -dir[1]], nbSteps)
201 .filter(m => !movesHash[getMoveHash(m)]);
202 newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; });
203 Array.prototype.push.apply(moves, newMoves);
204 };
205 // Free to play any move:
206 const moves = super.getPotentialMovesFrom([x, y])
207 const pawnShift = (color == 'w' ? -1 : 1);
208 const pawnStartRank = (color == 'w' ? 6 : 1);
209 // Structure to avoid adding moves twice (can be action & move)
210 let movesHash = {};
211 moves.forEach(m => { movesHash[getMoveHash(m)] = true; });
212 // [x, y] is pushed by 'color'
213 for (let step of V.steps[V.KNIGHT]) {
214 const [i, j] = [x + step[0], y + step[1]];
215 if (
216 V.OnBoard(i, j) &&
217 this.board[i][j] != V.EMPTY &&
218 this.getColor(i, j) == color &&
219 this.getPiece(i, j) == V.KNIGHT
220 ) {
221 addMoves(step, 1);
222 }
223 }
224 for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) {
225 let [i, j] = [x + step[0], y + step[1]];
226 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
227 i += step[0];
228 j += step[1];
229 }
230 if (
231 V.OnBoard(i, j) &&
232 this.board[i][j] != V.EMPTY &&
233 this.getColor(i, j) == color
234 ) {
235 const deltaX = Math.abs(i - x);
236 const deltaY = Math.abs(j - y);
237 // Can a priori go both ways, except with pawns
238 switch (this.getPiece(i, j)) {
239 case V.PAWN:
240 if (deltaX <= 2 && deltaY <= 1) {
241 const pColor = this.getColor(x, y);
242 if (pColor == color && deltaY == 0) {
243 // Pushed forward
244 const maxSteps = (i == pawnStartRank && deltaX == 1 ? 2 : 1);
245 addMoves(step, maxSteps);
246 }
247 else if (pColor != color && deltaY == 1 && deltaX == 1)
248 // Pushed diagonally
249 addMoves(step, 1);
250 }
251 break;
252 case V.ROOK:
253 if (deltaX == 0 || deltaY == 0) addMoves(step);
254 break;
255 case V.BISHOP:
256 if (deltaX == deltaY) addMoves(step);
257 break;
258 case V.QUEEN:
259 if (deltaX == 0 || deltaY == 0 || deltaX == deltaY)
260 addMoves(step);
261 break;
262 case V.KING:
263 if (deltaX <= 1 && deltaY <= 1) addMoves(step, 1);
264 break;
265 }
266 }
267 }
268 return moves;
269 }
270 // If subTurn == 2 then we should have a first move,
271 // which restrict what we can play now: only in the first move direction
272 // NOTE: no need for knight or pawn checks, because the move will be
273 // naturally limited in those cases.
274 const L = this.firstMove.length;
275 const fm = this.firstMove[L-1];
276 if (fm.appear.length == 2 && fm.vanish.length == 2)
277 // Castle: no real move playable then.
278 return [];
279 if (fm.appear.length == 0) {
280 // Piece at subTurn 1 just exited the board.
281 // Can I be a piece which caused the exit?
282 this.undo(fm);
283 const moveOk = V.IsAprioriValidMove([x, y], [fm.start.x, fm.start.y]);
284 this.play(fm);
285 if (moveOk) {
286 // Seems so:
287 const dir = this.getNormalizedDirection(
288 [fm.start.x - x, fm.start.y - y]);
289 return this.getMovesInDirection([x, y], dir);
290 }
291 }
292 else {
293 const dirM = this.getNormalizedDirection(
294 [fm.end.x - fm.start.x, fm.end.y - fm.start.y]);
295 const dir = this.getNormalizedDirection(
296 [fm.start.x - x, fm.start.y - y]);
297 // Normalized directions should match:
298 if (dir[0] == dirM[0] && dir[1] == dirM[1])
299 return this.getMovesInDirection([x, y], dir);
300 }
301 return [];
302 }
303
304 // Does m2 un-do m1 ? (to disallow undoing actions)
305 oppositeMoves(m1, m2) {
306 const isEqual = (av1, av2) => {
307 // Precondition: av1 and av2 length = 2
308 for (let av of av1) {
309 const avInAv2 = av2.find(elt => {
310 return (
311 elt.x == av.x &&
312 elt.y == av.y &&
313 elt.c == av.c &&
314 elt.p == av.p
315 );
316 });
317 if (!avInAv2) return false;
318 }
319 return true;
320 };
321 return (
322 m1.appear.length == 2 &&
323 m2.appear.length == 2 &&
324 m1.vanish.length == 2 &&
325 m2.vanish.length == 2 &&
326 isEqual(m1.appear, m2.vanish) &&
327 isEqual(m1.vanish, m2.appear)
328 );
329 }
330
331 getAmove(move1, move2) {
332 // Just merge (one is action one is move, one may be empty)
333 return {
334 appear: move1.appear.concat(move2.appear),
335 vanish: move1.vanish.concat(move2.vanish)
336 }
337 }
338
339 filterValid(moves) {
340 const color = this.turn;
341 if (this.subTurn == 1) {
342 return moves.filter(m => {
343 // A move is valid either if it doesn't result in a check,
344 // or if a second move is possible to counter the check
345 // (not undoing a potential move + action of the opponent)
346 this.play(m);
347 let res = this.underCheck(color);
348 if (res) {
349 const moves2 = this.getAllPotentialMoves();
350 for (m2 of moves2) {
351 this.play(m2);
352 const res2 = this.underCheck(color);
353 this.undo(m2);
354 if (!res2) {
355 res = false;
356 break;
357 }
358 }
359 }
360 this.undo(m);
361 return !res;
362 });
363 }
364 const Lf = this.firstMove.length;
365 const La = this.amoves.length;
366 if (La == 0) return super.filterValid(moves);
367 return (
368 super.filterValid(
369 moves.filter(m => {
370 // Move shouldn't undo another:
371 const amove = this.getAmove(this.firstMove[Lf-1], m);
372 return !this.oppositeMoves(this.amoves[La-1], amove);
373 })
374 )
375 );
376 }
377
378 isAttackedBySlideNJump([x, y], color, piece, steps, oneStep) {
379 for (let step of steps) {
380 let rx = x + step[0],
381 ry = y + step[1];
382 while (V.OnBoard(rx, ry) && this.board[rx][ry] == V.EMPTY && !oneStep) {
383 rx += step[0];
384 ry += step[1];
385 }
386 if (
387 V.OnBoard(rx, ry) &&
388 this.getPiece(rx, ry) == piece &&
389 this.getColor(rx, ry) == color
390 ) {
391 // Now step in the other direction: if end of the world, then attacked
392 rx = x - step[0];
393 ry = y - step[1];
394 while (
395 V.OnBoard(rx, ry) &&
396 this.board[rx][ry] == V.EMPTY &&
397 !oneStep
398 ) {
399 rx -= step[0];
400 ry -= step[1];
401 }
402 if (!V.OnBoard(rx, ry)) return true;
403 }
404 }
405 return false;
406 }
407
408 isAttackedByPawn([x, y], color) {
409 const lastRank = (color == 'w' ? 0 : 7);
410 if (y != lastRank)
411 // The king can be pushed out by a pawn only on last rank
412 return false;
413 const pawnShift = (color == "w" ? 1 : -1);
414 for (let i of [-1, 1]) {
415 if (
416 y + i >= 0 &&
417 y + i < V.size.y &&
418 this.getPiece(x + pawnShift, y + i) == V.PAWN &&
419 this.getColor(x + pawnShift, y + i) == color
420 ) {
421 return true;
422 }
423 }
424 return false;
425 }
426
427 getCurrentScore() {
428 if (this.subTurn == 2)
429 // Move not over
430 return "*";
431 return super.getCurrentScore();
432 }
433
434 doClick(square) {
435 // If subTurn == 2 && square is empty && !underCheck,
436 // then return an empty move, allowing to "pass" subTurn2
437 if (
438 this.subTurn == 2 &&
439 this.board[square[0]][square[1]] == V.EMPTY &&
440 !this.underCheck(this.turn)
441 ) {
442 return {
443 start: { x: -1, y: -1 },
444 end: { x: -1, y: -1 },
445 appear: [],
446 vanish: []
447 };
448 }
449 return null;
450 }
451
452 play(move) {
453 move.flags = JSON.stringify(this.aggregateFlags());
454 V.PlayOnBoard(this.board, move);
455 if (this.subTurn == 2) {
456 const L = this.firstMove.length;
457 this.amoves.push(this.getAmove(this.firstMove[L-1], move));
458 this.turn = V.GetOppCol(this.turn);
459 this.movesCount++;
460 }
461 else this.firstMove.push(move);
462 this.subTurn = 3 - this.subTurn;
463 this.postPlay(move);
464 }
465
466 postPlay(move) {
467 if (move.start.x < 0) return;
468 for (let a of move.appear)
469 if (a.p == V.KING) this.kingPos[a.c] = [a.x, a.y];
470 this.updateCastleFlags(move);
471 }
472
473 updateCastleFlags(move) {
474 const firstRank = { 'w': V.size.x - 1, 'b': 0 };
475 for (let v of move.vanish) {
476 if (v.p == V.KING) this.castleFlags[v.c] = [V.size.y, V.size.y];
477 else if (v.x == firstRank[v.c] && this.castleFlags[v.c].includes(v.y)) {
478 const flagIdx = (v.y == this.castleFlags[v.c][0] ? 0 : 1);
479 this.castleFlags[v.c][flagIdx] = V.size.y;
480 }
481 }
482 }
483
484 undo(move) {
485 this.disaggregateFlags(JSON.parse(move.flags));
486 V.UndoOnBoard(this.board, move);
487 if (this.subTurn == 1) {
488 this.turn = V.GetOppCol(this.turn);
489 this.movesCount--;
490 }
491 else this.firstMove.pop();
492 this.subTurn = 3 - this.subTurn;
493 this.postUndo(move);
494 }
495
496 postUndo(move) {
497 // (Potentially) Reset king position
498 for (let v of move.vanish)
499 if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
500 }
501
502 getNotation(move) {
503 if (move.start.x < 0)
504 // A second move is always required, but may be empty
505 return "-";
506 const initialSquare = V.CoordsToSquare(move.start);
507 const finalSquare = V.CoordsToSquare(move.end);
508 if (move.appear.length == 0)
509 // Pushed or pulled out of the board
510 return initialSquare + "R";
511 return move.appear[0].p.toUpperCase() + initialSquare + finalSquare;
512 }
513 };