e1e653a32df0401d8b443fcd11df7e8bd4eae99a
[vchess.git] / client / src / variants / Fanorona.js
1 import { ChessRules, Move, PiPo } from "@/base_rules";
2 import { randInt } from "@/utils/alea";
3
4 export class FanoronaRules extends ChessRules {
5
6 static get Options() {
7 return null;
8 }
9
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)
53 this.captures = [ [] ];
54 }
55
56 static get size() {
57 return { x: 5, y: 9 };
58 }
59
60 getPiece() {
61 return V.PAWN;
62 }
63
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
83 getPpath(b) {
84 return "Fanorona/" + b;
85 }
86
87 getPPpath(m, orientation) {
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];
91 const multStep = (orientation == 'w' ? 1 : -1);
92 const normalizedStep = [
93 multStep * step[0] / Math.abs(step[0]),
94 multStep * step[1] / Math.abs(step[1])
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 }
118
119 getPotentialMovesFrom([x, y]) {
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;
240 }
241
242 filterValid(moves) {
243 return moves;
244 }
245
246 getCheckSquares() {
247 return [];
248 }
249
250 play(move) {
251 const color = this.turn;
252 move.turn = color; //for undo
253 V.PlayOnBoard(this.board, move);
254 if (move.vanish.length >= 2) {
255 const L0 = this.captures.length;
256 let captures = this.captures[L0 - 1];
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)
263 move.notTheEnd = true;
264 }
265 if (!move.notTheEnd) {
266 this.turn = V.GetOppCol(color);
267 this.movesCount++;
268 this.captures.push([]);
269 }
270 }
271
272 undo(move) {
273 V.UndoOnBoard(this.board, move);
274 if (!move.notTheEnd) {
275 this.turn = move.turn;
276 this.movesCount--;
277 this.captures.pop();
278 }
279 if (move.vanish.length >= 2) {
280 const L0 = this.captures.length;
281 let captures = this.captures[L0 - 1];
282 captures.pop();
283 }
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() {
302 const moves = this.getAllValidMoves();
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);
315 mvArray.push(mv);
316 captures = (this.turn == color ? this.getAllValidMoves() : []);
317 }
318 if (mvArray.length >= 1) {
319 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
320 return mvArray;
321 }
322 // Just play a random move, which if possible does not let a capture
323 let candidates = [];
324 for (let m of moves) {
325 this.play(m);
326 if (!this.atLeastOneCapture()) candidates.push(m);
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) {
334 if (move.appear.length == 0) return "stop";
335 return (
336 V.CoordsToSquare(move.start) +
337 V.CoordsToSquare(move.end) +
338 (move.vanish.length >= 2 ? "X" : "")
339 );
340 }
341
342 };