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 | ||
ded43c88 | 83 | getPPpath(m, orientation) { |
cdab5663 BA |
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]; | |
ded43c88 | 87 | const multStep = (orientation == 'w' ? 1 : -1); |
cdab5663 | 88 | const normalizedStep = [ |
ded43c88 BA |
89 | multStep * step[0] / Math.abs(step[0]), |
90 | multStep * step[1] / Math.abs(step[1]) | |
cdab5663 BA |
91 | ]; |
92 | return ( | |
93 | "Fanorona/arrow_" + | |
94 | (normalizedStep[0] || 0) + "_" + (normalizedStep[1] || 0) | |
95 | ); | |
96 | } | |
97 | ||
98 | // After moving, add stones captured in "step" direction from new location | |
99 | // [x, y] to mv.vanish (if any captured stone!) | |
100 | addCapture([x, y], step, move) { | |
101 | let [i, j] = [x + step[0], y + step[1]]; | |
102 | const oppCol = V.GetOppCol(move.vanish[0].c); | |
103 | while ( | |
104 | V.OnBoard(i, j) && | |
105 | this.board[i][j] != V.EMPTY && | |
106 | this.getColor(i, j) == oppCol | |
107 | ) { | |
108 | move.vanish.push(new PiPo({ x: i, y: j, c: oppCol, p: V.PAWN })); | |
109 | i += step[0]; | |
110 | j += step[1]; | |
111 | } | |
112 | return (move.vanish.length >= 2); | |
113 | } | |
08909cf4 BA |
114 | |
115 | getPotentialMovesFrom([x, y]) { | |
cdab5663 BA |
116 | const L0 = this.captures.length; |
117 | const captures = this.captures[L0 - 1]; | |
118 | const L = captures.length; | |
119 | if (L > 0) { | |
120 | var c = captures[L-1]; | |
121 | if (x != c.square.x + c.step[0] || y != c.square.y + c.step[1]) | |
122 | return []; | |
123 | } | |
124 | const oppCol = V.GetOppCol(this.turn); | |
125 | let steps = V.steps[V.ROOK]; | |
126 | if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); | |
127 | let moves = []; | |
128 | for (let s of steps) { | |
129 | if (L > 0 && c.step[0] == s[0] && c.step[1] == s[1]) { | |
130 | // Add a move to say "I'm done capturing" | |
131 | moves.push( | |
132 | new Move({ | |
133 | appear: [], | |
134 | vanish: [], | |
135 | start: { x: x, y: y }, | |
136 | end: { x: x - s[0], y: y - s[1] } | |
137 | }) | |
138 | ); | |
139 | continue; | |
140 | } | |
141 | let [i, j] = [x + s[0], y + s[1]]; | |
142 | if (captures.some(c => c.square.x == i && c.square.y == j)) continue; | |
143 | if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { | |
144 | // The move is potentially allowed. Might lead to 2 different captures | |
145 | let mv = super.getBasicMove([x, y], [i, j]); | |
146 | const capt = this.addCapture([i, j], s, mv); | |
147 | if (capt) { | |
148 | moves.push(mv); | |
149 | mv = super.getBasicMove([x, y], [i, j]); | |
150 | } | |
151 | const capt_bw = this.addCapture([x, y], [-s[0], -s[1]], mv); | |
152 | if (capt_bw) moves.push(mv); | |
153 | // Captures take priority (if available) | |
154 | if (!capt && !capt_bw && L == 0) moves.push(mv); | |
155 | } | |
156 | } | |
157 | return moves; | |
158 | } | |
159 | ||
160 | atLeastOneCapture() { | |
161 | const color = this.turn; | |
162 | const oppCol = V.GetOppCol(color); | |
163 | const L0 = this.captures.length; | |
164 | const captures = this.captures[L0 - 1]; | |
165 | const L = captures.length; | |
166 | if (L > 0) { | |
167 | // If some adjacent enemy stone, with free space to capture it, | |
168 | // toward a square not already visited, through a different step | |
169 | // from last one: then yes. | |
170 | const c = captures[L-1]; | |
171 | const [x, y] = [c.square.x + c.step[0], c.square.y + c.step[1]]; | |
172 | let steps = V.steps[V.ROOK]; | |
173 | if ((x + y) % 2 == 0) steps = steps.concat(V.steps[V.BISHOP]); | |
174 | // TODO: half of the steps explored are redundant | |
175 | for (let s of steps) { | |
176 | if (s[0] == c.step[0] && s[1] == c.step[1]) continue; | |
177 | const [i, j] = [x + s[0], y + s[1]]; | |
178 | if ( | |
179 | !V.OnBoard(i, j) || | |
180 | this.board[i][j] != V.EMPTY || | |
181 | captures.some(c => c.square.x == i && c.square.y == j) | |
182 | ) { | |
183 | continue; | |
184 | } | |
185 | if ( | |
186 | V.OnBoard(i + s[0], j + s[1]) && | |
187 | this.board[i + s[0]][j + s[1]] != V.EMPTY && | |
188 | this.getColor(i + s[0], j + s[1]) == oppCol | |
189 | ) { | |
190 | return true; | |
191 | } | |
192 | if ( | |
193 | V.OnBoard(x - s[0], y - s[1]) && | |
194 | this.board[x - s[0]][y - s[1]] != V.EMPTY && | |
195 | this.getColor(x - s[0], y - s[1]) == oppCol | |
196 | ) { | |
197 | return true; | |
198 | } | |
199 | } | |
200 | return false; | |
201 | } | |
202 | for (let i = 0; i < V.size.x; i++) { | |
203 | for (let j = 0; j < V.size.y; j++) { | |
204 | if ( | |
205 | this.board[i][j] != V.EMPTY && | |
206 | this.getColor(i, j) == color && | |
207 | // TODO: this could be more efficient | |
208 | this.getPotentialMovesFrom([i, j]).some(m => m.vanish.length >= 2) | |
209 | ) { | |
210 | return true; | |
211 | } | |
212 | } | |
213 | } | |
214 | return false; | |
215 | } | |
216 | ||
217 | static KeepCaptures(moves) { | |
218 | return moves.filter(m => m.vanish.length >= 2); | |
219 | } | |
220 | ||
221 | getPossibleMovesFrom(sq) { | |
222 | let moves = this.getPotentialMovesFrom(sq); | |
223 | const L0 = this.captures.length; | |
224 | const captures = this.captures[L0 - 1]; | |
225 | if (captures.length > 0) return this.getPotentialMovesFrom(sq); | |
226 | const captureMoves = V.KeepCaptures(moves); | |
227 | if (captureMoves.length > 0) return captureMoves; | |
228 | if (this.atLeastOneCapture()) return []; | |
229 | return moves; | |
230 | } | |
231 | ||
232 | getAllValidMoves() { | |
233 | const moves = super.getAllValidMoves(); | |
234 | if (moves.some(m => m.vanish.length >= 2)) return V.KeepCaptures(moves); | |
235 | return moves; | |
08909cf4 BA |
236 | } |
237 | ||
238 | filterValid(moves) { | |
239 | return moves; | |
240 | } | |
241 | ||
242 | getCheckSquares() { | |
243 | return []; | |
244 | } | |
245 | ||
08909cf4 BA |
246 | play(move) { |
247 | const color = this.turn; | |
248 | move.turn = color; //for undo | |
cdab5663 | 249 | V.PlayOnBoard(this.board, move); |
cdab5663 | 250 | if (move.vanish.length >= 2) { |
d982fffc BA |
251 | const L0 = this.captures.length; |
252 | let captures = this.captures[L0 - 1]; | |
cdab5663 BA |
253 | captures.push({ |
254 | square: move.start, | |
255 | step: [move.end.x - move.start.x, move.end.y - move.start.y] | |
256 | }); | |
257 | if (this.atLeastOneCapture()) | |
258 | // There could be other captures (optional) | |
cdab5663 | 259 | move.notTheEnd = true; |
cdab5663 BA |
260 | } |
261 | if (!move.notTheEnd) { | |
262 | this.turn = V.GetOppCol(color); | |
08909cf4 | 263 | this.movesCount++; |
cdab5663 | 264 | this.captures.push([]); |
08909cf4 | 265 | } |
08909cf4 BA |
266 | } |
267 | ||
268 | undo(move) { | |
269 | V.UndoOnBoard(this.board, move); | |
d982fffc | 270 | if (!move.notTheEnd) { |
08909cf4 BA |
271 | this.turn = move.turn; |
272 | this.movesCount--; | |
cdab5663 BA |
273 | this.captures.pop(); |
274 | } | |
d982fffc | 275 | if (move.vanish.length >= 2) { |
cdab5663 BA |
276 | const L0 = this.captures.length; |
277 | let captures = this.captures[L0 - 1]; | |
278 | captures.pop(); | |
08909cf4 | 279 | } |
08909cf4 BA |
280 | } |
281 | ||
282 | getCurrentScore() { | |
283 | const color = this.turn; | |
284 | // If no stones on board, I lose | |
285 | if ( | |
286 | this.board.every(b => { | |
287 | return b.every(cell => { | |
288 | return (cell == "" || cell[0] != color); | |
289 | }); | |
290 | }) | |
291 | ) { | |
292 | return (color == 'w' ? "0-1" : "1-0"); | |
293 | } | |
294 | return "*"; | |
295 | } | |
296 | ||
297 | getComputerMove() { | |
cdab5663 | 298 | const moves = this.getAllValidMoves(); |
08909cf4 BA |
299 | if (moves.length == 0) return null; |
300 | const color = this.turn; | |
301 | // Capture available? If yes, play it | |
302 | let captures = moves.filter(m => m.vanish.length >= 2); | |
303 | let mvArray = []; | |
304 | while (captures.length >= 1) { | |
305 | // Then just pick random captures (trying to maximize) | |
306 | let candidates = captures.filter(c => !!c.notTheEnd); | |
307 | let mv = null; | |
308 | if (candidates.length >= 1) mv = candidates[randInt(candidates.length)]; | |
309 | else mv = captures[randInt(captures.length)]; | |
310 | this.play(mv); | |
cdab5663 BA |
311 | mvArray.push(mv); |
312 | captures = (this.turn == color ? this.getAllValidMoves() : []); | |
08909cf4 BA |
313 | } |
314 | if (mvArray.length >= 1) { | |
315 | for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); | |
316 | return mvArray; | |
317 | } | |
cdab5663 | 318 | // Just play a random move, which if possible does not let a capture |
08909cf4 BA |
319 | let candidates = []; |
320 | for (let m of moves) { | |
321 | this.play(m); | |
cdab5663 | 322 | if (!this.atLeastOneCapture()) candidates.push(m); |
08909cf4 BA |
323 | this.undo(m); |
324 | } | |
325 | if (candidates.length >= 1) return candidates[randInt(candidates.length)]; | |
326 | return moves[randInt(moves.length)]; | |
327 | } | |
328 | ||
329 | getNotation(move) { | |
cdab5663 | 330 | if (move.appear.length == 0) return "stop"; |
08909cf4 BA |
331 | return ( |
332 | V.CoordsToSquare(move.start) + | |
cdab5663 BA |
333 | V.CoordsToSquare(move.end) + |
334 | (move.vanish.length >= 2 ? "X" : "") | |
08909cf4 BA |
335 | ); |
336 | } | |
d2af3400 BA |
337 | |
338 | }; |