Some fixes. Screen variant computer play is still broken, seemingly
[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 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)
49 this.captures = [ [] ];
50 }
51
52 static get size() {
53 return { x: 5, y: 9 };
54 }
55
56 getPiece() {
57 return V.PAWN;
58 }
59
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
79 getPpath(b) {
80 return "Fanorona/" + b;
81 }
82
83 getPPpath(m, orientation) {
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 multStep = (orientation == 'w' ? 1 : -1);
88 const normalizedStep = [
89 multStep * step[0] / Math.abs(step[0]),
90 multStep * step[1] / Math.abs(step[1])
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 }
114
115 getPotentialMovesFrom([x, y]) {
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;
236 }
237
238 filterValid(moves) {
239 return moves;
240 }
241
242 getCheckSquares() {
243 return [];
244 }
245
246 play(move) {
247 const color = this.turn;
248 move.turn = color; //for undo
249 V.PlayOnBoard(this.board, move);
250 if (move.vanish.length >= 2) {
251 const L0 = this.captures.length;
252 let captures = this.captures[L0 - 1];
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)
259 move.notTheEnd = true;
260 }
261 if (!move.notTheEnd) {
262 this.turn = V.GetOppCol(color);
263 this.movesCount++;
264 this.captures.push([]);
265 }
266 }
267
268 undo(move) {
269 V.UndoOnBoard(this.board, move);
270 if (!move.notTheEnd) {
271 this.turn = move.turn;
272 this.movesCount--;
273 this.captures.pop();
274 }
275 if (move.vanish.length >= 2) {
276 const L0 = this.captures.length;
277 let captures = this.captures[L0 - 1];
278 captures.pop();
279 }
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() {
298 const moves = this.getAllValidMoves();
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);
311 mvArray.push(mv);
312 captures = (this.turn == color ? this.getAllValidMoves() : []);
313 }
314 if (mvArray.length >= 1) {
315 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
316 return mvArray;
317 }
318 // Just play a random move, which if possible does not let a capture
319 let candidates = [];
320 for (let m of moves) {
321 this.play(m);
322 if (!this.atLeastOneCapture()) candidates.push(m);
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) {
330 if (move.appear.length == 0) return "stop";
331 return (
332 V.CoordsToSquare(move.start) +
333 V.CoordsToSquare(move.end) +
334 (move.vanish.length >= 2 ? "X" : "")
335 );
336 }
337
338 };