Fix Football variant
[vchess.git] / client / src / variants / Football.js
CommitLineData
107dc1bd 1import { ChessRules } from "@/base_rules";
5c50b18e
BA
2import { randInt, shuffle } from "@/utils/alea";
3import { ArrayFun } from "@/utils/array";
107dc1bd
BA
4
5export class FootballRules extends ChessRules {
7e8a7ea1 6
4f3a0823
BA
7 static get HasEnpassant() {
8 return false;
9 }
10
107dc1bd
BA
11 static get HasFlags() {
12 return false;
13 }
14
4f3a0823
BA
15 static get size() {
16 return { x: 9, y: 9 };
107dc1bd
BA
17 }
18
19 static get Lines() {
20 return [
21 // White goal:
4f3a0823 22 [[0, 4], [0, 5]],
107dc1bd 23 [[0, 5], [1, 5]],
4f3a0823 24 [[1, 4], [0, 4]],
107dc1bd 25 // Black goal:
4f3a0823
BA
26 [[9, 4], [9, 5]],
27 [[9, 5], [8, 5]],
28 [[8, 4], [9, 4]]
107dc1bd
BA
29 ];
30 }
31
4f3a0823
BA
32 static get BALL() {
33 // 'b' is already taken:
34 return "aa";
35 }
36
37 // Check that exactly one ball is on the board
38 // + at least one piece per color.
107dc1bd
BA
39 static IsGoodPosition(position) {
40 if (position.length == 0) return false;
41 const rows = position.split("/");
42 if (rows.length != V.size.x) return false;
107dc1bd 43 let pieces = { "w": 0, "b": 0 };
4f3a0823 44 let ballCount = 0;
107dc1bd
BA
45 for (let row of rows) {
46 let sumElts = 0;
47 for (let i = 0; i < row.length; i++) {
48 const lowerRi = row[i].toLowerCase();
4f3a0823
BA
49 if (!!lowerRi.match(/^[a-z]$/)) {
50 if (V.PIECES.includes(lowerRi))
51 pieces[row[i] == lowerRi ? "b" : "w"]++;
52 else if (lowerRi == 'a') ballCount++;
53 else return false;
107dc1bd 54 sumElts++;
0f7762c1
BA
55 }
56 else {
e50a8025 57 const num = parseInt(row[i], 10);
107dc1bd
BA
58 if (isNaN(num)) return false;
59 sumElts += num;
60 }
61 }
62 if (sumElts != V.size.y) return false;
63 }
4f3a0823
BA
64 if (ballCount != 1 || Object.values(pieces).some(v => v == 0))
65 return false;
107dc1bd
BA
66 return true;
67 }
68
4f3a0823
BA
69 static board2fen(b) {
70 if (b == V.BALL) return 'a';
71 return ChessRules.board2fen(b);
72 }
107dc1bd 73
4f3a0823
BA
74 static fen2board(f) {
75 if (f == 'a') return V.BALL;
76 return ChessRules.fen2board(f);
77 }
78
79 getPpath(b) {
80 if (b == V.BALL) return "Football/ball";
81 return b;
82 }
83
84 canIplay(side, [x, y]) {
85 return (
86 side == this.turn &&
87 (this.board[x][y] == V.BALL || this.getColor(x, y) == side)
88 );
89 }
90
91 // No checks or king tracking etc. But, track ball
92 setOtherVariables() {
93 // Stack of "kicked by" coordinates, to avoid infinite loops
94 this.kickedBy = [ {} ];
95 this.subTurn = 1;
96 this.ballPos = [-1, -1];
97 for (let i=0; i < V.size.x; i++) {
98 for (let j=0; j< V.size.y; j++) {
99 if (this.board[i][j] == V.BALL) {
100 this.ballPos = [i, j];
101 return;
102 }
103 }
104 }
105 }
106
107 static GenRandInitFen(randomness) {
108 if (randomness == 0)
109 return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0";
110
111 // TODO: following is mostly copy-paste from Suicide variant
112 let pieces = { w: new Array(8), b: new Array(8) };
113 for (let c of ["w", "b"]) {
114 if (c == 'b' && randomness == 1) {
115 pieces['b'] = pieces['w'];
116 break;
117 }
118
119 // Get random squares for every piece, totally freely
120 let positions = shuffle(ArrayFun.range(8));
121 const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
122 const rem2 = positions[0] % 2;
123 if (rem2 == positions[1] % 2) {
124 // Fix bishops (on different colors)
125 for (let i=2; i<8; i++) {
126 if (positions[i] % 2 != rem2)
127 [positions[1], positions[i]] = [positions[i], positions[1]];
128 }
129 }
130 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
131 }
132 const piecesB = pieces["b"].join("") ;
133 const piecesW = pieces["w"].join("").toUpperCase();
134 return (
135 piecesB.substr(0, 4) + "1" + piecesB.substr(4) +
136 "/9/9/9/4a4/9/9/9/" +
137 piecesW.substr(0, 4) + "1" + piecesW.substr(4) +
138 " w 0"
139 );
140 }
141
142 tryKickFrom([x, y]) {
143 const bp = this.ballPos;
144 const emptySquare = (i, j) => {
145 return V.OnBoard(i, j) && this.board[i][j] == V.EMPTY;
146 };
147 // Kick the (adjacent) ball from x, y with current turn:
148 const step = [bp[0] - x, bp[1] - y];
149 const piece = this.getPiece(x, y);
150 let moves = [];
151 if (piece == V.KNIGHT) {
152 // The knight case is particular
153 V.steps[V.KNIGHT].forEach(s => {
154 const [i, j] = [bp[0] + s[0], bp[1] + s[1]];
155 if (
156 V.OnBoard(i, j) &&
157 this.board[i][j] == V.EMPTY &&
158 (
159 // In a corner? The, allow all ball moves
160 ([0, 8].includes(bp[0]) && [0, 8].includes(bp[1])) ||
161 // Do not end near the knight
162 (Math.abs(i - x) >= 2 || Math.abs(j - y) >= 2)
163 )
164 ) {
165 moves.push(super.getBasicMove(bp, [i, j]));
166 }
167 });
168 }
169 else {
170 let compatible = false,
171 oneStep = false;
172 switch (piece) {
173 case V.ROOK:
174 compatible = (step[0] == 0 || step[1] == 0);
175 break;
176 case V.BISHOP:
177 compatible = (step[0] != 0 && step[1] != 0);
178 break;
179 case V.QUEEN:
180 compatible = true;
181 break;
182 case V.KING:
183 compatible = true;
184 oneStep = true;
185 break;
186 }
187 if (!compatible) return [];
188 let [i, j] = [bp[0] + step[0], bp[1] + step[1]];
189 const horizontalStepOnGoalRow =
190 ([0, 8].includes(bp[0]) && step.some(s => s == 0));
191 if (emptySquare(i, j) && (!horizontalStepOnGoalRow || j != 4)) {
192 moves.push(super.getBasicMove(bp, [i, j]));
193 if (!oneStep) {
194 do {
195 i += step[0];
196 j += step[1];
197 if (!emptySquare(i, j)) break;
198 if (!horizontalStepOnGoalRow || j != 4)
199 moves.push(super.getBasicMove(bp, [i, j]));
200 } while (true);
201 }
202 }
203 }
204 const kickedFrom = x + "-" + y;
205 moves.forEach(m => m.by = kickedFrom)
107dc1bd
BA
206 return moves;
207 }
208
4f3a0823
BA
209 getPotentialMovesFrom([x, y], computer) {
210 if (V.PIECES.includes(this.getPiece(x, y))) {
211 if (this.subTurn > 1) return [];
212 return (
213 super.getPotentialMovesFrom([x, y])
214 .filter(m => m.end.y != 4 || ![0, 8].includes(m.end.x))
215 );
216 }
217 // Kicking the ball: look for adjacent pieces.
218 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
219 const c = this.turn;
220 let moves = [];
221 for (let s of steps) {
222 const [i, j] = [x + s[0], y + s[1]];
223 if (
224 V.OnBoard(i, j) &&
225 this.board[i][j] != V.EMPTY &&
226 this.getColor(i, j) == c
227 ) {
228 Array.prototype.push.apply(moves, this.tryKickFrom([i, j]));
229 }
230 }
231 // And, always add the "end" move. For computer, keep only one
232 outerLoop: for (let i=0; i < V.size.x; i++) {
233 for (let j=0; j < V.size.y; j++) {
234 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) {
235 moves.push(super.getBasicMove([x, y], [i, j]));
236 if (!!computer) break outerLoop;
237 }
238 }
239 }
240 return moves;
241 }
242
243 // No captures:
244 getSlideNJumpMoves([x, y], steps, oneStep) {
245 let moves = [];
246 outerLoop: for (let step of steps) {
247 let i = x + step[0];
248 let j = y + step[1];
249 let stepCount = 1;
250 while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
251 moves.push(this.getBasicMove([x, y], [i, j]));
252 if (!!oneStep) continue outerLoop;
253 i += step[0];
254 j += step[1];
255 stepCount++;
256 }
257 }
258 return moves;
259 }
260
261 // Extra arg "computer" to avoid trimming all redundant pass moves:
262 getAllPotentialMoves(computer) {
263 const color = this.turn;
264 let potentialMoves = [];
265 for (let i = 0; i < V.size.x; i++) {
266 for (let j = 0; j < V.size.y; j++) {
267 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
268 Array.prototype.push.apply(
269 potentialMoves,
270 this.getPotentialMovesFrom([i, j], computer)
271 );
272 }
273 }
274 }
275 return potentialMoves;
276 }
277
278 getAllValidMoves() {
279 return this.filterValid(this.getAllPotentialMoves("computer"));
280 }
281
282 filterValid(moves) {
283 const L = this.kickedBy.length;
284 const kb = this.kickedBy[L-1];
285 return moves.filter(m => !m.by || !kb[m.by]);
286 }
287
107dc1bd
BA
288 getCheckSquares() {
289 return [];
290 }
291
4f3a0823
BA
292 allowAnotherPass(color) {
293 // Two cases: a piece moved, or the ball moved.
294 // In both cases, check our pieces and ball proximity,
295 // so the move played doesn't matter (if ball position updated)
296 const bp = this.ballPos;
297 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
298 for (let s of steps) {
299 const [i, j] = [this.ballPos[0] + s[0], this.ballPos[1] + s[1]];
300 if (
301 V.OnBoard(i, j) &&
302 this.board[i][j] != V.EMPTY &&
303 this.getColor(i, j) == color
304 ) {
305 return true; //potentially...
306 }
307 }
308 return false;
309 }
310
311 prePlay(move) {
312 if (move.appear[0].p == 'a')
313 this.ballPos = [move.appear[0].x, move.appear[0].y];
314 }
315
316 play(move) {
317 // Special message saying "passes are over"
318 const passesOver = (move.vanish.length == 2);
319 if (!passesOver) {
320 this.prePlay(move);
321 V.PlayOnBoard(this.board, move);
322 }
323 move.turn = [this.turn, this.subTurn]; //easier undo
324 if (passesOver || !this.allowAnotherPass(this.turn)) {
325 this.turn = V.GetOppCol(this.turn);
326 this.subTurn = 1;
327 this.movesCount++;
328 this.kickedBy.push( {} );
329 }
330 else {
331 this.subTurn++;
332 if (!!move.by) {
333 const L = this.kickedBy.length;
334 this.kickedBy[L-1][move.by] = true;
335 }
336 }
337 }
338
339 undo(move) {
340 const passesOver = (move.vanish.length == 2);
341 if (move.turn[0] != this.turn) {
342 [this.turn, this.subTurn] = move.turn;
343 this.movesCount--;
344 this.kickedBy.pop();
345 }
346 else {
347 this.subTurn--;
348 if (!!move.by) {
349 const L = this.kickedBy.length;
350 delete this.kickedBy[L-1][move.by];
351 }
352 }
353 if (!passesOver) {
354 V.UndoOnBoard(this.board, move);
355 this.postUndo(move);
356 }
357 }
358
359 postUndo(move) {
360 if (move.vanish[0].p == 'a')
361 this.ballPos = [move.vanish[0].x, move.vanish[0].y];
362 }
107dc1bd
BA
363
364 getCurrentScore() {
4f3a0823
BA
365 if (this.board[0][4] == V.BALL) return "1-0";
366 if (this.board[8][4] == V.BALL) return "0-1";
367 return "*";
107dc1bd 368 }
b9ce3d0f 369
4f3a0823
BA
370 getComputerMove() {
371 let initMoves = this.getAllValidMoves();
372 if (initMoves.length == 0) return null;
373 let moves = JSON.parse(JSON.stringify(initMoves));
374 let mvArray = [];
375 let mv = null;
376 // Just play random moves (for now at least. TODO?)
377 const c = this.turn;
378 while (moves.length > 0) {
379 mv = moves[randInt(moves.length)];
380 mvArray.push(mv);
381 this.play(mv);
382 if (mv.vanish.length == 1 && this.allowAnotherPass(c))
383 // Potential kick
384 moves = this.getPotentialMovesFrom(this.ballPos);
385 else break;
386 }
387 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
388 return (mvArray.length > 1 ? mvArray : mvArray[0]);
389 }
390
391 // NOTE: evalPosition() is wrong, but unused since bot plays at random
392
393 getNotation(move) {
394 if (move.vanish.length == 2) return "pass";
395 if (move.vanish[0].p != 'a') return super.getNotation(move);
396 // Kick: simple notation (TODO?)
397 return V.CoordsToSquare(move.end);
b9ce3d0f 398 }
7e8a7ea1 399
107dc1bd 400};