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