Commit | Line | Data |
---|---|---|
4258b58c BA |
1 | import { ChessRules, Move, PiPo } from "@/base_rules"; |
2 | import { randInt } from "@/utils/alea"; | |
3 | ||
4 | export class AvalancheRules extends ChessRules { | |
5 | ||
6 | static get PawnSpecs() { | |
7 | return ( | |
8 | Object.assign( | |
9 | { promotions: [V.PAWN] }, | |
10 | ChessRules.PawnSpecs | |
11 | ) | |
12 | ); | |
13 | } | |
14 | ||
15 | static get HasEnpassant() { | |
16 | return false; | |
17 | } | |
18 | ||
19 | static IsGoodFen(fen) { | |
20 | if (!ChessRules.IsGoodFen(fen)) return false; | |
21 | const fenParts = fen.split(" "); | |
22 | if (fenParts.length != 5) return false; | |
23 | if (!fenParts[4].match(/^[0-8]$/)) return false; | |
24 | return true; | |
25 | } | |
26 | ||
27 | canIplay(side, [x, y]) { | |
28 | if (this.subTurn == 0) return (x >= V.size.x); | |
29 | const c = this.getColor(x, y); | |
30 | return ( | |
31 | (this.subTurn == 1 && c == side) || | |
32 | (this.subTurn == 2 && c != side && this.getPiece(x, y) == V.PAWN) | |
33 | ); | |
34 | } | |
35 | ||
36 | static ParseFen(fen) { | |
37 | const fenParts = fen.split(" "); | |
38 | return Object.assign( | |
39 | ChessRules.ParseFen(fen), | |
40 | { promoteFile: fenParts[4] } | |
41 | ); | |
42 | } | |
43 | ||
44 | getPromoteFen() { | |
45 | const L = this.promoteFile.length; | |
46 | return (this.promoteFile[L-1] + 1); | |
47 | } | |
48 | ||
49 | getFen() { | |
50 | return super.getFen() + " " + this.getPromoteFen(); | |
51 | } | |
52 | ||
53 | getFenForRepeat() { | |
54 | return super.getFenForRepeat() + "_" + this.getPromoteFen(); | |
55 | } | |
56 | ||
57 | static GenRandInitFen(randomness) { | |
58 | return ChessRules.GenRandInitFen(randomness).slice(0, -1) + "0"; | |
59 | } | |
60 | ||
61 | getPiece(i, j) { | |
62 | if (i >= V.size.x) return V.RESERVE_PIECES[j]; | |
63 | return this.board[i][j].charAt(1); | |
64 | } | |
65 | ||
66 | static get RESERVE_PIECES() { | |
67 | // Promotion pieces | |
68 | return [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]; | |
69 | } | |
70 | ||
71 | setOtherVariables(fen) { | |
72 | super.setOtherVariables(fen); | |
73 | const fenPromoteFile = V.ParseFen(fen).promoteFile; | |
74 | this.promoteFile = [parseInt(fenPromoteFile, 10) - 1]; | |
75 | this.reserve = { 'w': null, 'b': null }; | |
76 | if (this.promoteFile[0] >= 0) { | |
77 | this.reserve = { | |
78 | [this.turn]: { | |
79 | [V.ROOK]: 1, | |
80 | [V.KNIGHT]: 1, | |
81 | [V.BISHOP]: 1, | |
82 | [V.QUEEN]: 1 | |
83 | } | |
84 | }; | |
85 | this.subTurn = 0; | |
86 | } | |
87 | else this.subTurn = 1; | |
88 | } | |
89 | ||
90 | getReservePpath(index, color) { | |
91 | return color + V.RESERVE_PIECES[index]; | |
92 | } | |
93 | ||
94 | getReserveMove(y) { | |
95 | // Send a new piece piece to our first rank | |
96 | const color = this.turn; | |
97 | const L = this.promoteFile.length; | |
98 | const [rank, file] = [color == 'w' ? 0 : 7, this.promoteFile[L-1]]; | |
99 | return new Move({ | |
100 | appear: [ | |
101 | new PiPo({ x: rank, y: file, c: color, p: V.RESERVE_PIECES[y] }) | |
102 | ], | |
103 | vanish: [ | |
104 | new PiPo({ x: rank, y: file, c: color, p: V.PAWN }) | |
105 | ], | |
106 | start: { x: 8, y: y }, | |
107 | end: { x: rank, y: file } | |
108 | }); | |
109 | } | |
110 | ||
111 | getPotentialMovesFrom([x, y]) { | |
112 | if (this.subTurn == 0) | |
113 | // Reserves, outside of board: x == sizeX(+1) | |
114 | return (x >= 8 ? [this.getReserveMove(y)] : []); | |
115 | if (this.subTurn == 1) | |
116 | // Usual case: | |
117 | return super.getPotentialMovesFrom([x, y]); | |
118 | // subTurn == 2: only allowed to push an opponent's pawn (if possible) | |
119 | const oppPawnShift = (this.turn == 'w' ? 1 : -1); | |
120 | if ( | |
121 | V.OnBoard(x + oppPawnShift, y) && | |
122 | this.board[x + oppPawnShift][y] == V.EMPTY | |
123 | ) { | |
124 | return [this.getBasicMove([x, y], [x + oppPawnShift, y])]; | |
125 | } | |
126 | return []; | |
127 | } | |
128 | ||
129 | getAllValidMoves() { | |
130 | if (this.subTurn == 0) { | |
131 | let moves = []; | |
132 | for (let y = 0; y < V.RESERVE_PIECES.length; y++) | |
133 | moves.push(this.getReserveMove(y)); | |
134 | return moves; | |
135 | } | |
136 | if (this.subTurn == 1) | |
137 | return this.filterValid(super.getAllPotentialMoves()); | |
138 | // subTurn == 2: move opponent's pawn only | |
139 | let moves = []; | |
140 | const oppCol = V.GetOppCol(this.turn); | |
141 | for (let i = 0; i < 8; i++) { | |
142 | for (let j = 0; j < 8; j++) { | |
143 | if ( | |
144 | this.board[i][j] != V.EMPTY && | |
145 | this.getColor(i, j) == oppCol && | |
146 | this.getPiece(i, j) == V.PAWN | |
147 | ) { | |
148 | Array.prototype.push.apply( | |
149 | moves, this.getPotentialMovesFrom([i, j])); | |
150 | } | |
151 | } | |
152 | } | |
153 | return moves; | |
154 | } | |
155 | ||
156 | filterValid(moves) { | |
157 | if (this.subTurn != 1) return moves; //self-checks by pawns are allowed | |
158 | return super.filterValid(moves); | |
159 | } | |
160 | ||
161 | atLeastOneMove() { | |
162 | if (this.subTurn == 0) return true; //TODO: never called in this situation | |
163 | if (this.subTurn == 1) { | |
164 | // Cannot use super method: infinite recursive calls | |
165 | const color = this.turn; | |
166 | for (let i = 0; i < 8; i++) { | |
167 | for (let j = 0; j < 8; j++) { | |
168 | if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { | |
169 | const moves = this.getPotentialMovesFrom([i, j]); | |
170 | if (moves.length > 0) { | |
171 | for (let k = 0; k < moves.length; k++) { | |
172 | const piece = moves[k].vanish[0].p; | |
173 | if (piece == V.KING) { | |
174 | this.kingPos[color] = | |
175 | [moves[k].appear[0].x, moves[k].appear[0].y]; | |
176 | } | |
177 | V.PlayOnBoard(this.board, moves[k]); | |
178 | const res = !this.underCheck(color); | |
179 | V.UndoOnBoard(this.board, moves[k]); | |
180 | if (piece == V.KING) { | |
181 | this.kingPos[color] = | |
182 | [moves[k].vanish[0].x, moves[k].vanish[0].y]; | |
183 | } | |
184 | if (res) return true; | |
185 | } | |
186 | } | |
187 | } | |
188 | } | |
189 | } | |
190 | return false; | |
191 | } | |
192 | // subTurn == 2: need to find an enemy pawn which can advance | |
193 | const oppCol = V.GetOppCol(this.turn); | |
194 | const oppPawnShift = (oppCol == 'w' ? -1 : 1); | |
195 | for (let i = 0; i < 8; i++) { | |
196 | for (let j = 0; j < 8; j++) { | |
197 | if ( | |
198 | this.board[i][j] != V.EMPTY && | |
199 | this.getColor(i, j) == oppCol && | |
200 | this.getPiece(i, j) == V.PAWN && | |
201 | V.OnBoard(i + oppPawnShift, j) && | |
202 | this.board[i + oppPawnShift][j] == V.EMPTY | |
203 | ) { | |
204 | return true; | |
205 | } | |
206 | } | |
207 | } | |
208 | return false; | |
209 | } | |
210 | ||
211 | getCheckSquares() { | |
212 | if (this.kingPos[this.turn][0] < 0) return []; | |
213 | return super.getCheckSquares(); | |
214 | } | |
215 | ||
216 | getCurrentScore() { | |
217 | // If my king disappeared: I lost! | |
218 | const c = this.turn; | |
219 | if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0"); | |
220 | return super.getCurrentScore(); | |
221 | } | |
222 | ||
223 | prePlay(move) { | |
224 | if (this.subTurn != 1) return; | |
225 | const c = move.vanish[0].c; | |
226 | const piece = move.vanish[0].p; | |
227 | const firstRank = c == "w" ? V.size.x - 1 : 0; | |
228 | if (piece == V.KING) { | |
229 | this.kingPos[c] = [move.appear[0].x, move.appear[0].y]; | |
230 | this.castleFlags[c] = [V.size.y, V.size.y]; | |
231 | return; | |
232 | } | |
233 | const oppCol = V.GetOppCol(c); | |
234 | if (move.vanish.length == 2 && move.vanish[1].p == V.KING) { | |
235 | // Opponent's king is captured, game over | |
236 | this.kingPos[oppCol] = [-1, -1]; | |
237 | move.captureKing = true; | |
238 | } | |
239 | const oppFirstRank = V.size.x - 1 - firstRank; | |
240 | if ( | |
241 | move.start.x == firstRank && //our rook moves? | |
242 | this.castleFlags[c].includes(move.start.y) | |
243 | ) { | |
244 | const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1); | |
245 | this.castleFlags[c][flagIdx] = V.size.y; | |
246 | } | |
247 | if ( | |
248 | move.end.x == oppFirstRank && //we took opponent rook? | |
249 | this.castleFlags[oppCol].includes(move.end.y) | |
250 | ) { | |
251 | const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1); | |
252 | this.castleFlags[oppCol][flagIdx] = V.size.y; | |
253 | } | |
254 | } | |
255 | ||
256 | play(move) { | |
257 | move.flags = JSON.stringify(this.aggregateFlags()); | |
258 | this.prePlay(move); | |
259 | V.PlayOnBoard(this.board, move); | |
260 | const c = this.turn; | |
261 | move.turn = [c, this.subTurn]; | |
262 | const oppCol = V.GetOppCol(c); | |
263 | const oppLastRank = (c == 'w' ? 7 : 0); | |
264 | if (this.subTurn <= 1) this.reserve[oppCol] = null; | |
265 | if (this.subTurn == 0) { | |
266 | this.subTurn++; | |
267 | this.reserve[c] = null; | |
268 | } | |
269 | else if (this.subTurn == 1) { | |
270 | this.subTurn++; | |
271 | if ( | |
272 | this.movesCount == 0 || | |
273 | !!move.captureKing || | |
274 | !this.atLeastOneMove() | |
275 | ) { | |
276 | this.turn = oppCol; | |
277 | this.movesCount++; | |
278 | this.subTurn = 1; | |
279 | this.promoteFile.push(-1); | |
280 | move.pushPromote = true; | |
281 | } | |
282 | } | |
283 | else { | |
284 | // subTurn == 2 | |
285 | this.turn = oppCol; | |
286 | if (move.end.x == oppLastRank) { | |
287 | this.promoteFile.push(move.end.y); | |
288 | this.reserve[oppCol] = { | |
289 | [V.ROOK]: 1, | |
290 | [V.KNIGHT]: 1, | |
291 | [V.BISHOP]: 1, | |
292 | [V.QUEEN]: 1 | |
293 | }; | |
294 | this.subTurn = 0; | |
295 | } | |
296 | else { | |
297 | this.subTurn = 1; | |
298 | this.promoteFile.push(-1); | |
299 | } | |
300 | move.pushPromote = true; | |
301 | this.movesCount++; | |
302 | } | |
303 | } | |
304 | ||
305 | undo(move) { | |
306 | this.disaggregateFlags(JSON.parse(move.flags)); | |
307 | V.UndoOnBoard(this.board, move); | |
308 | const changeTurn = (this.turn != move.turn[0]); | |
309 | this.turn = move.turn[0]; | |
310 | this.subTurn = move.turn[1]; | |
311 | if (!!move.pushPromote) { | |
312 | const promoteFile = this.promoteFile.pop(); | |
313 | if (promoteFile >= 0) this.reserve[V.GetOppCol(this.turn)] = null; | |
314 | } | |
315 | else if (this.subTurn == 0) { | |
316 | this.reserve[this.turn] = { | |
317 | [V.ROOK]: 1, | |
318 | [V.KNIGHT]: 1, | |
319 | [V.BISHOP]: 1, | |
320 | [V.QUEEN]: 1 | |
321 | }; | |
322 | } | |
323 | if (changeTurn) this.movesCount--; | |
324 | this.postUndo(move); | |
325 | } | |
326 | ||
327 | postUndo(move) { | |
328 | if (this.subTurn != 1) return; | |
329 | if (move.vanish.length == 2 && move.vanish[1].p == V.KING) | |
330 | // Opponent's king was captured | |
331 | this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y]; | |
332 | super.postUndo(move); | |
333 | } | |
334 | ||
335 | getComputerMove() { | |
336 | // Just try to capture as much material as possible (1-half move) | |
337 | const moves = this.getAllValidMoves(); | |
338 | if (this.subTurn == 0) { | |
339 | this.play(moves[3]); //HACK... 3 = queen index | |
340 | const res = this.getComputerMove(); | |
341 | this.undo(moves[3]); | |
342 | return [moves[3], res]; | |
343 | } | |
344 | // subTurn == 1 (necessarily) | |
345 | let candidates = []; | |
346 | let maxValue = -V.INFINITY; | |
347 | for (let m of moves) { | |
348 | let value = 0; | |
349 | if (m.vanish.length == 2) { | |
350 | // Compute delta value, to not give all material on pawns... (TODO) | |
351 | // 0.5 to favor captures (if same type of piece). | |
352 | value = 0.5 + | |
353 | ChessRules.VALUES[m.vanish[1].p] - ChessRules.VALUES[m.vanish[0].p]; | |
354 | } | |
355 | if (value > maxValue) { | |
356 | candidates = [m]; | |
357 | maxValue = value; | |
358 | } | |
359 | else if (value == maxValue) candidates.push(m); | |
360 | } | |
361 | const m1 = candidates[randInt(candidates.length)]; | |
362 | this.play(m1); | |
363 | let m2 = null; | |
364 | if (this.subTurn == 2) { | |
365 | // Just pick a pawn at random | |
366 | const moves2 = this.getAllValidMoves(); | |
367 | m2 = moves2[randInt(moves2.length)]; | |
368 | } | |
369 | this.undo(m1); | |
370 | if (!m2) return m1; | |
371 | return [m1, m2]; | |
372 | } | |
373 | ||
374 | getNotation(move) { | |
375 | if (this.subTurn == 0) | |
376 | return move.appear[0].p.toUpperCase() + "@" + V.CoordsToSquare(move.end); | |
377 | if (this.subTurn == 1) return super.getNotation(move); | |
378 | // subTurn == 2: just indicate final square | |
379 | return V.CoordsToSquare(move.end); | |
380 | } | |
381 | ||
382 | }; |