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