Commit | Line | Data |
---|---|---|
08909cf4 BA |
1 | import { ChessRules, Move, PiPo } from "@/base_rules"; |
2 | import { randInt } from "@/utils/alea"; | |
d2af3400 BA |
3 | |
4 | export class FanoronaRules extends ChessRules { | |
5 | ||
08909cf4 BA |
6 | static get HasFlags() { |
7 | return false; | |
8 | } | |
9 | ||
10 | static get HasEnpassant() { | |
11 | return false; | |
12 | } | |
13 | ||
14 | static get Monochrome() { | |
15 | return true; | |
16 | } | |
17 | ||
18 | static get Lines() { | |
19 | let lines = []; | |
20 | // Draw all inter-squares lines, shifted: | |
21 | for (let i = 0; i < V.size.x; i++) | |
22 | lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]); | |
23 | for (let j = 0; j < V.size.y; j++) | |
24 | lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]); | |
25 | const columnDiags = [ | |
26 | [[0.5, 0.5], [2.5, 2.5]], | |
27 | [[0.5, 2.5], [2.5, 0.5]], | |
28 | [[2.5, 0.5], [4.5, 2.5]], | |
29 | [[4.5, 0.5], [2.5, 2.5]] | |
30 | ]; | |
31 | for (let j of [0, 2, 4, 6]) { | |
32 | lines = lines.concat( | |
33 | columnDiags.map(L => [[L[0][0], L[0][1] + j], [L[1][0], L[1][1] + j]]) | |
34 | ); | |
35 | } | |
36 | return lines; | |
37 | } | |
38 | ||
39 | static get Notoodark() { | |
40 | return true; | |
41 | } | |
42 | ||
43 | static GenRandInitFen() { | |
44 | return "ppppppppp/ppppppppp/pPpP1pPpP/PPPPPPPPP/PPPPPPPPP w 0"; | |
45 | } | |
46 | ||
47 | setOtherVariables(fen) { | |
48 | // Local stack of captures during a turn (squares + directions) | |
cdab5663 | 49 | this.captures = [ [] ]; |
08909cf4 BA |
50 | } |
51 | ||
52 | static get size() { | |
53 | return { x: 5, y: 9 }; | |
54 | } | |
55 | ||
08909cf4 BA |
56 | getPiece() { |
57 | return V.PAWN; | |
58 | } | |
59 | ||
cdab5663 BA |
60 | static IsGoodPosition(position) { |
61 | if (position.length == 0) return false; | |
62 | const rows = position.split("/"); | |
63 | if (rows.length != V.size.x) return false; | |
64 | for (let row of rows) { | |
65 | let sumElts = 0; | |
66 | for (let i = 0; i < row.length; i++) { | |
67 | if (row[i].toLowerCase() == V.PAWN) sumElts++; | |
68 | else { | |
69 | const num = parseInt(row[i], 10); | |
70 | if (isNaN(num) || num <= 0) return false; | |
71 | sumElts += num; | |
72 | } | |
73 | } | |
74 | if (sumElts != V.size.y) return false; | |
75 | } | |
76 | return true; | |
77 | } | |
78 | ||
08909cf4 BA |
79 | getPpath(b) { |
80 | return "Fanorona/" + b; | |
81 | } | |
82 | ||
cdab5663 BA |
83 | getPPpath(m) { |
84 | // m.vanish.length >= 2, first capture gives direction | |
85 | const ref = (Math.abs(m.vanish[1].x - m.start.x) == 1 ? m.start : m.end); | |
86 | const step = [m.vanish[1].x - ref.x, m.vanish[1].y - ref.y]; | |
87 | const normalizedStep = [ | |
88 | step[0] / Math.abs(step[0]), | |
89 | step[1] / Math.abs(step[1]) | |
90 | ]; | |
91 | return ( | |
92 | "Fanorona/arrow_" + | |
93 | (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0) | |
94 | ); | |
95 | } | |
96 | ||
97 | // After moving, add stones captured in "step" direction from new location | |
98 | // [x, y] to mv.vanish (if any captured stone!) | |
99 | addCapture([x, y], step, move) { | |
100 | let [i, j] = [x + step[0], y + step[1]]; | |
101 | const oppCol = V.GetOppCol(move.vanish[0].c); | |
102 | while ( | |
103 | V.OnBoard(i, j) && | |
104 | this.board[i][j] != V.EMPTY && | |
105 | this.getColor(i, j) == oppCol | |
106 | ) { | |
107 | move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN })); | |
108 | i += step[0]; | |
109 | j += step[1]; | |
110 | } | |
111 | return (move.vanish.length >= 2); | |
112 | } | |
08909cf4 BA |
113 | |
114 | getPotentialMovesFrom([x, y]) { | |
cdab5663 BA |
115 | const L0 = this.captures.length; |
116 | const captures = this.captures[L0 - 1]; | |
117 | const L = captures.length; | |
118 | if (L > 0) { | |
119 | var c = captures[L-1]; | |
120 | if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1]) | |
121 | return []; | |
122 | } | |
123 | const oppCol = V.GetOppCol(this.turn); | |
124 | let steps = V.steps[V.ROOK]; | |
125 | if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); | |
126 | let moves = []; | |
127 | for (let s of steps) { | |
128 | if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) { | |
129 | // Add a move to say "I'm done capturing" | |
130 | moves.push( | |
131 | new Move({ | |
132 | appear: [], | |
133 | vanish: [], | |
134 | start: { x: x, y: y }, | |
135 | end: { x: x - s[0], y: y - s[1] } | |
136 | }) | |
137 | ); | |
138 | continue; | |
139 | } | |
140 | let [i, j] = [x + s[0], y + s[1]]; | |
141 | if (captures.some(c => c.square.x == i && c.square.y == j)) continue; | |
142 | if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { | |
143 | // The move is potentially allowed. Might lead to 2 different captures | |
144 | let mv = super.getBasicMove([x, y], [i, j]); | |
145 | const capt = this.addCapture([i, j], s, mv); | |
146 | if (capt) { | |
147 | moves.push(mv); | |
148 | mv = super.getBasicMove([x, y], [i, j]); | |
149 | } | |
150 | const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv); | |
151 | if (capt_bw) moves.push(mv); | |
152 | // Captures take priority (if available) | |
153 | if (!capt && !capt_bw && L == 0) moves.push(mv); | |
154 | } | |
155 | } | |
156 | return moves; | |
157 | } | |
158 | ||
159 | atLeastOneCapture() { | |
160 | const color = this.turn; | |
161 | const oppCol = V.GetOppCol(color); | |
162 | const L0 = this.captures.length; | |
163 | const captures = this.captures[L0 - 1]; | |
164 | const L = captures.length; | |
165 | if (L > 0) { | |
166 | // If some adjacent enemy stone, with free space to capture it, | |
167 | // toward a square not already visited, through a different step | |
168 | // from last one: then yes. | |
169 | const c = captures[L-1]; | |
170 | const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]]; | |
171 | let steps = V.steps[V.ROOK]; | |
172 | if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); | |
173 | // TODO: half of the steps explored are redundant | |
174 | for (let s of steps) { | |
175 | if (s[0] == c.step[0] && s[1] == c.step[1]) continue; | |
176 | const [i, j] = [x + s[0], y + s[1]]; | |
177 | if ( | |
178 | !V.OnBoard(i, j) || | |
179 | this.board[i][j] != V.EMPTY || | |
180 | captures.some(c => c.square.x == i && c.square.y == j) | |
181 | ) { | |
182 | continue; | |
183 | } | |
184 | if ( | |
185 | V.OnBoard(i + s[0], j + s[1]) && | |
186 | this.board[i + s[0]][j + s[1]] != V.EMPTY && | |
187 | this.getColor(i + s[0], j + s[1]) == oppCol | |
188 | ) { | |
189 | return true; | |
190 | } | |
191 | if ( | |
192 | V.OnBoard(x - s[0], y - s[1]) && | |
193 | this.board[x - s[0]][y - s[1]] != V.EMPTY && | |
194 | this.getColor(x - s[0], y - s[1]) == oppCol | |
195 | ) { | |
196 | return true; | |
197 | } | |
198 | } | |
199 | return false; | |
200 | } | |
201 | for (let i = 0; i < V.size.x; i++) { | |
202 | for (let j = 0; j < V.size.y; j++) { | |
203 | if ( | |
204 | this.board[i][j] != V.EMPTY && | |
205 | this.getColor(i, j) == color && | |
206 | // TODO: this could be more efficient | |
207 | this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2) | |
208 | ) { | |
209 | return true; | |
210 | } | |
211 | } | |
212 | } | |
213 | return false; | |
214 | } | |
215 | ||
216 | static KeepCaptures(moves) { | |
217 | return moves.filter(m => m.vanish.length >= 2); | |
218 | } | |
219 | ||
220 | getPossibleMovesFrom(sq) { | |
221 | let moves = this.getPotentialMovesFrom(sq); | |
222 | const L0 = this.captures.length; | |
223 | const captures = this.captures[L0 - 1]; | |
224 | if (captures.length > 0) return this.getPotentialMovesFrom(sq); | |
225 | const captureMoves = V.KeepCaptures(moves); | |
226 | if (captureMoves.length > 0) return captureMoves; | |
227 | if (this.atLeastOneCapture()) return []; | |
228 | return moves; | |
229 | } | |
230 | ||
231 | getAllValidMoves() { | |
232 | const moves = super.getAllValidMoves(); | |
233 | if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves); | |
234 | return moves; | |
08909cf4 BA |
235 | } |
236 | ||
237 | filterValid(moves) { | |
238 | return moves; | |
239 | } | |
240 | ||
241 | getCheckSquares() { | |
242 | return []; | |
243 | } | |
244 | ||
08909cf4 BA |
245 | play(move) { |
246 | const color = this.turn; | |
247 | move.turn = color; //for undo | |
cdab5663 BA |
248 | V.PlayOnBoard(this.board, move); |
249 | const L0 = this.captures.length; | |
250 | let captures = this.captures[L0 - 1]; | |
251 | if (move.vanish.length >= 2) { | |
252 | captures.push({ | |
253 | square: move.start, | |
254 | step: [move.end.x - move.start.x, move.end.y - move.start.y] | |
255 | }); | |
256 | if (this.atLeastOneCapture()) | |
257 | // There could be other captures (optional) | |
258 | // This field is mostly useful for computer play. | |
259 | move.notTheEnd = true; | |
260 | else captures.pop(); //useless now | |
261 | } | |
262 | if (!move.notTheEnd) { | |
263 | this.turn = V.GetOppCol(color); | |
08909cf4 | 264 | this.movesCount++; |
cdab5663 | 265 | this.captures.push([]); |
08909cf4 | 266 | } |
08909cf4 BA |
267 | } |
268 | ||
269 | undo(move) { | |
270 | V.UndoOnBoard(this.board, move); | |
08909cf4 BA |
271 | if (move.turn != this.turn) { |
272 | this.turn = move.turn; | |
273 | this.movesCount--; | |
cdab5663 BA |
274 | this.captures.pop(); |
275 | } | |
276 | else { | |
277 | const L0 = this.captures.length; | |
278 | let captures = this.captures[L0 - 1]; | |
279 | captures.pop(); | |
08909cf4 | 280 | } |
08909cf4 BA |
281 | } |
282 | ||
283 | getCurrentScore() { | |
284 | const color = this.turn; | |
285 | // If no stones on board, I lose | |
286 | if ( | |
287 | this.board.every(b => { | |
288 | return b.every(cell => { | |
289 | return (cell == "" || cell[0] != color); | |
290 | }); | |
291 | }) | |
292 | ) { | |
293 | return (color == 'w' ? "0-1" : "1-0"); | |
294 | } | |
295 | return "*"; | |
296 | } | |
297 | ||
298 | getComputerMove() { | |
cdab5663 | 299 | const moves = this.getAllValidMoves(); |
08909cf4 BA |
300 | if (moves.length == 0) return null; |
301 | const color = this.turn; | |
302 | // Capture available? If yes, play it | |
303 | let captures = moves.filter(m => m.vanish.length >= 2); | |
304 | let mvArray = []; | |
305 | while (captures.length >= 1) { | |
306 | // Then just pick random captures (trying to maximize) | |
307 | let candidates = captures.filter(c => !!c.notTheEnd); | |
308 | let mv = null; | |
309 | if (candidates.length >= 1) mv = candidates[randInt(candidates.length)]; | |
310 | else mv = captures[randInt(captures.length)]; | |
311 | this.play(mv); | |
cdab5663 BA |
312 | mvArray.push(mv); |
313 | captures = (this.turn == color ? this.getAllValidMoves() : []); | |
08909cf4 BA |
314 | } |
315 | if (mvArray.length >= 1) { | |
316 | for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); | |
317 | return mvArray; | |
318 | } | |
cdab5663 | 319 | // Just play a random move, which if possible does not let a capture |
08909cf4 BA |
320 | let candidates = []; |
321 | for (let m of moves) { | |
322 | this.play(m); | |
cdab5663 | 323 | if (!this.atLeastOneCapture()) candidates.push(m); |
08909cf4 BA |
324 | this.undo(m); |
325 | } | |
326 | if (candidates.length >= 1) return candidates[randInt(candidates.length)]; | |
327 | return moves[randInt(moves.length)]; | |
328 | } | |
329 | ||
330 | getNotation(move) { | |
cdab5663 | 331 | if (move.appear.length == 0) return "stop"; |
08909cf4 BA |
332 | return ( |
333 | V.CoordsToSquare(move.start) + | |
cdab5663 BA |
334 | V.CoordsToSquare(move.end) + |
335 | (move.vanish.length >= 2 ? "X" : "") | |
08909cf4 BA |
336 | ); |
337 | } | |
d2af3400 BA |
338 | |
339 | }; |