Commit | Line | Data |
---|---|---|
b866a62a BA |
1 | import { ChessRules, Move, PiPo } from "@/base_rules"; |
2 | ||
0d5335de | 3 | export 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 | } |
7b53b5a7 | 43 | this.subTurn = 1; |
a443d256 BA |
44 | // Stack "first moves" (on subTurn 1) to merge and check opposite moves |
45 | this.firstMove = []; | |
c7550017 BA |
46 | } |
47 | ||
61656127 BA |
48 | static ParseFen(fen) { |
49 | return Object.assign( | |
50 | ChessRules.ParseFen(fen), | |
93ce6119 | 51 | { amove: fen.split(" ")[4] } |
61656127 BA |
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 | ||
93ce6119 BA |
64 | getFen() { |
65 | return super.getFen() + " " + this.getAmoveFen(); | |
61656127 BA |
66 | } |
67 | ||
93ce6119 BA |
68 | getFenForRepeat() { |
69 | return super.getFenForRepeat() + "_" + this.getAmoveFen(); | |
70 | } | |
71 | ||
72 | getAmoveFen() { | |
73 | const L = this.amoves.length; | |
7b53b5a7 | 74 | if (L == 0) return "-"; |
93ce6119 BA |
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 | ); | |
61656127 BA |
89 | } |
90 | ||
91 | canTake() { | |
92 | // Captures don't occur (only pulls & pushes) | |
93 | return false; | |
94 | } | |
95 | ||
b2655276 BA |
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]; | |
61656127 | 101 | let moves = []; |
b2655276 BA |
102 | const color = this.getColor(x, y); |
103 | const piece = this.getPiece(x, y); | |
104 | const lastRank = (color == 'w' ? 0 : 7); | |
7b53b5a7 | 105 | let counter = 1; |
b2655276 BA |
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])); | |
7b53b5a7 BA |
115 | if (++counter > nbSteps) break; |
116 | i += dx; | |
117 | j += dy; | |
b2655276 BA |
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 }, | |
7b53b5a7 | 124 | end: { x: this.kingPos[color][0], y: this.kingPos[color][1] }, |
b2655276 BA |
125 | appear: [], |
126 | vanish: [{ x: x, y: y, c: color, p: piece }] | |
127 | }) | |
128 | ); | |
129 | } | |
61656127 BA |
130 | return moves; |
131 | } | |
132 | ||
b2655276 BA |
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)); | |
b866a62a | 176 | } |
b2655276 BA |
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); | |
93ce6119 | 185 | } |
b2655276 | 186 | return false; |
b866a62a BA |
187 | } |
188 | ||
93ce6119 | 189 | // NOTE: for pushes, play the pushed piece first. |
61656127 | 190 | // for pulls: play the piece doing the action first |
b2655276 | 191 | // NOTE: to push a piece out of the board, make it slide until its king |
b866a62a BA |
192 | getPotentialMovesFrom([x, y]) { |
193 | const color = this.turn; | |
93ce6119 | 194 | if (this.subTurn == 1) { |
b2655276 BA |
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)]); | |
7b53b5a7 | 202 | newMoves.forEach(m => { movesHash[getMoveHash(m)] = true; }); |
b2655276 BA |
203 | Array.prototype.push.apply(moves, newMoves); |
204 | }; | |
7b53b5a7 BA |
205 | // Free to play any move: |
206 | const moves = super.getPotentialMovesFrom([x, y]) | |
b2655276 BA |
207 | const pawnShift = (color == 'w' ? -1 : 1); |
208 | const pawnStartRank = (color == 'w' ? 6 : 1); | |
7b53b5a7 BA |
209 | // Structure to avoid adding moves twice (can be action & move) |
210 | let movesHash = {}; | |
211 | moves.forEach(m => { movesHash[getMoveHash(m)] = true; }); | |
b2655276 BA |
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]]; | |
7b53b5a7 BA |
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); | |
b2655276 BA |
222 | } |
223 | } | |
7b53b5a7 | 224 | for (let step of V.steps[V.ROOK].concat(V.steps[V.BISHOP])) { |
b2655276 BA |
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; | |
b2655276 BA |
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; | |
b866a62a | 269 | } |
93ce6119 | 270 | // If subTurn == 2 then we should have a first move, |
b2655276 BA |
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. | |
93ce6119 BA |
274 | const L = this.firstMove.length; |
275 | const fm = this.firstMove[L-1]; | |
b2655276 BA |
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 | } | |
93ce6119 BA |
291 | } |
292 | else { | |
b2655276 BA |
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); | |
93ce6119 | 300 | } |
b2655276 | 301 | return []; |
61656127 BA |
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 ( | |
61656127 BA |
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 | ||
93ce6119 BA |
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 | ||
61656127 | 339 | filterValid(moves) { |
93ce6119 BA |
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); | |
a443d256 | 367 | return ( |
93ce6119 | 368 | super.filterValid( |
a443d256 BA |
369 | moves.filter(m => { |
370 | // Move shouldn't undo another: | |
93ce6119 BA |
371 | const amove = this.getAmove(this.firstMove[Lf-1], m); |
372 | return !this.oppositeMoves(this.amoves[La-1], amove); | |
a443d256 BA |
373 | }) |
374 | ) | |
375 | ); | |
b866a62a BA |
376 | } |
377 | ||
c7550017 BA |
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]; | |
2c5d7b20 BA |
394 | while ( |
395 | V.OnBoard(rx, ry) && | |
396 | this.board[rx][ry] == V.EMPTY && | |
397 | !oneStep | |
398 | ) { | |
c7550017 BA |
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 | } | |
61656127 BA |
426 | |
427 | getCurrentScore() { | |
428 | if (this.subTurn == 2) | |
429 | // Move not over | |
430 | return "*"; | |
431 | return super.getCurrentScore(); | |
432 | } | |
433 | ||
93ce6119 | 434 | doClick(square) { |
b2655276 BA |
435 | // If subTurn == 2 && square is empty && !underCheck, |
436 | // then return an empty move, allowing to "pass" subTurn2 | |
93ce6119 BA |
437 | if ( |
438 | this.subTurn == 2 && | |
7b53b5a7 | 439 | this.board[square[0]][square[1]] == V.EMPTY && |
b2655276 | 440 | !this.underCheck(this.turn) |
93ce6119 BA |
441 | ) { |
442 | return { | |
7b53b5a7 BA |
443 | start: { x: -1, y: -1 }, |
444 | end: { x: -1, y: -1 }, | |
93ce6119 BA |
445 | appear: [], |
446 | vanish: [] | |
447 | }; | |
448 | } | |
449 | return null; | |
450 | } | |
451 | ||
61656127 BA |
452 | play(move) { |
453 | move.flags = JSON.stringify(this.aggregateFlags()); | |
454 | V.PlayOnBoard(this.board, move); | |
7ddfec38 | 455 | if (this.subTurn == 2) { |
7b53b5a7 BA |
456 | const L = this.firstMove.length; |
457 | this.amoves.push(this.getAmove(this.firstMove[L-1], move)); | |
61656127 | 458 | this.turn = V.GetOppCol(this.turn); |
7ddfec38 | 459 | this.movesCount++; |
61656127 | 460 | } |
a443d256 | 461 | else this.firstMove.push(move); |
7ddfec38 | 462 | this.subTurn = 3 - this.subTurn; |
61656127 BA |
463 | this.postPlay(move); |
464 | } | |
465 | ||
7b53b5a7 BA |
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 }; | |
7ddfec38 | 475 | for (let v of move.vanish) { |
7b53b5a7 BA |
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; | |
7ddfec38 | 480 | } |
61656127 BA |
481 | } |
482 | } | |
483 | ||
484 | undo(move) { | |
485 | this.disaggregateFlags(JSON.parse(move.flags)); | |
486 | V.UndoOnBoard(this.board, move); | |
7ddfec38 | 487 | if (this.subTurn == 1) { |
61656127 | 488 | this.turn = V.GetOppCol(this.turn); |
7ddfec38 | 489 | this.movesCount--; |
61656127 | 490 | } |
a443d256 | 491 | else this.firstMove.pop(); |
7ddfec38 | 492 | this.subTurn = 3 - this.subTurn; |
61656127 BA |
493 | this.postUndo(move); |
494 | } | |
7b53b5a7 BA |
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 | } | |
0d5335de | 513 | }; |