Fix Football
[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
4313762d
BA
107 static GenRandInitFen(options) {
108 if (options.randomness == 0)
4f3a0823
BA
109 return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0";
110
4f3a0823
BA
111 let pieces = { w: new Array(8), b: new Array(8) };
112 for (let c of ["w", "b"]) {
4313762d 113 if (c == 'b' && options.randomness == 1) {
4f3a0823
BA
114 pieces['b'] = pieces['w'];
115 break;
116 }
117
118 // Get random squares for every piece, totally freely
119 let positions = shuffle(ArrayFun.range(8));
120 const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
5d74fcea
BA
121 // Fix bishops (on different colors)
122 const realOddity =
123 (pos) => { return (pos <= 3 ? pos % 2 : (pos + 1) % 2); };
124 const rem2 = realOddity(positions[0]);
125 if (rem2 == realOddity(positions[1])) {
4f3a0823 126 for (let i=2; i<8; i++) {
5d74fcea 127 if (realOddity(positions[i]) != rem2) {
4f3a0823 128 [positions[1], positions[i]] = [positions[i], positions[1]];
5d74fcea
BA
129 break;
130 }
4f3a0823
BA
131 }
132 }
133 for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
134 }
135 const piecesB = pieces["b"].join("") ;
136 const piecesW = pieces["w"].join("").toUpperCase();
137 return (
138 piecesB.substr(0, 4) + "1" + piecesB.substr(4) +
139 "/9/9/9/4a4/9/9/9/" +
140 piecesW.substr(0, 4) + "1" + piecesW.substr(4) +
141 " w 0"
142 );
143 }
144
145 tryKickFrom([x, y]) {
146 const bp = this.ballPos;
147 const emptySquare = (i, j) => {
148 return V.OnBoard(i, j) && this.board[i][j] == V.EMPTY;
149 };
150 // Kick the (adjacent) ball from x, y with current turn:
151 const step = [bp[0] - x, bp[1] - y];
152 const piece = this.getPiece(x, y);
153 let moves = [];
154 if (piece == V.KNIGHT) {
155 // The knight case is particular
156 V.steps[V.KNIGHT].forEach(s => {
157 const [i, j] = [bp[0] + s[0], bp[1] + s[1]];
158 if (
159 V.OnBoard(i, j) &&
160 this.board[i][j] == V.EMPTY &&
161 (
5c27a4b5 162 // In a corner? Then, allow all ball moves
4f3a0823
BA
163 ([0, 8].includes(bp[0]) && [0, 8].includes(bp[1])) ||
164 // Do not end near the knight
165 (Math.abs(i - x) >= 2 || Math.abs(j - y) >= 2)
166 )
167 ) {
168 moves.push(super.getBasicMove(bp, [i, j]));
169 }
170 });
171 }
172 else {
173 let compatible = false,
174 oneStep = false;
175 switch (piece) {
176 case V.ROOK:
177 compatible = (step[0] == 0 || step[1] == 0);
178 break;
179 case V.BISHOP:
180 compatible = (step[0] != 0 && step[1] != 0);
181 break;
182 case V.QUEEN:
183 compatible = true;
184 break;
185 case V.KING:
186 compatible = true;
187 oneStep = true;
188 break;
189 }
190 if (!compatible) return [];
191 let [i, j] = [bp[0] + step[0], bp[1] + step[1]];
192 const horizontalStepOnGoalRow =
193 ([0, 8].includes(bp[0]) && step.some(s => s == 0));
5c6c52ac
BA
194 if (
195 emptySquare(i, j) &&
196 (this.movesCount >= 2 || j != 4 || ![0, 8].includes(i)) &&
197 (!horizontalStepOnGoalRow || j != 4)
198 ) {
4f3a0823
BA
199 moves.push(super.getBasicMove(bp, [i, j]));
200 if (!oneStep) {
201 do {
202 i += step[0];
203 j += step[1];
204 if (!emptySquare(i, j)) break;
5c6c52ac
BA
205 if (
206 (this.movesCount >= 2 || j != 4 || ![0, 8].includes(i)) &&
207 (!horizontalStepOnGoalRow || j != 4)
208 ) {
4f3a0823 209 moves.push(super.getBasicMove(bp, [i, j]));
5c6c52ac
BA
210 }
211 } while (true);
212 }
213 }
214 // Try the other direction (TODO: experimental)
215 [i, j] = [bp[0] - 2*step[0], bp[1] - 2*step[1]];
216 if (
217 emptySquare(i, j) &&
218 (this.movesCount >= 2 || j != 4 || ![0, 8].includes(i)) &&
219 (!horizontalStepOnGoalRow || j != 4)
220 ) {
221 moves.push(super.getBasicMove(bp, [i, j]));
222 if (!oneStep) {
223 do {
224 i -= step[0];
225 j -= step[1];
226 if (!emptySquare(i, j)) break;
227 if (
228 (this.movesCount >= 2 || j != 4 || ![0, 8].includes(i)) &&
229 (!horizontalStepOnGoalRow || j != 4)
230 ) {
231 moves.push(super.getBasicMove(bp, [i, j]));
232 }
4f3a0823
BA
233 } while (true);
234 }
235 }
236 }
237 const kickedFrom = x + "-" + y;
de9e4580 238 moves.forEach(m => m.start.by = kickedFrom)
107dc1bd
BA
239 return moves;
240 }
241
4f3a0823 242 getPotentialMovesFrom([x, y], computer) {
5c6c52ac
BA
243 const piece = this.getPiece(x, y);
244 if (V.PIECES.includes(piece)) {
4f3a0823 245 if (this.subTurn > 1) return [];
5c6c52ac
BA
246 const moves = super.getPotentialMovesFrom([x, y])
247 .filter(m => m.end.y != 4 || ![0, 8].includes(m.end.x));
248 // If bishop stuck in a corner: allow to jump over the next obstacle
5c27a4b5
BA
249 if (
250 moves.length == 0 && piece == V.BISHOP &&
251 [0, 8].includes(x) && [0, 8].includes(y)
252 ) {
253 const indX = x == 0 ? [1, 2] : [7, 6];
254 const indY = y == 0 ? [1, 2] : [7, 6];
5c6c52ac 255 if (
5c27a4b5
BA
256 this.board[indX[0]][indY[0]] != V.EMPTY &&
257 this.board[indX[1]][indY[1]] == V.EMPTY
5c6c52ac 258 ) {
5c27a4b5 259 return [super.getBasicMove([x, y], [indX[1], indY[1]])];
5c6c52ac
BA
260 }
261 }
262 return moves;
4f3a0823
BA
263 }
264 // Kicking the ball: look for adjacent pieces.
265 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
266 const c = this.turn;
267 let moves = [];
2cfc3b91 268 let kicks = {};
f897a4ef 269 let adjacentPieces = false;
4f3a0823
BA
270 for (let s of steps) {
271 const [i, j] = [x + s[0], y + s[1]];
272 if (
273 V.OnBoard(i, j) &&
274 this.board[i][j] != V.EMPTY &&
275 this.getColor(i, j) == c
276 ) {
2cfc3b91
BA
277 const kmoves = this.tryKickFrom([i, j]);
278 kmoves.forEach(km => {
279 const key = V.CoordsToSquare(km.start) + V.CoordsToSquare(km.end);
5c27a4b5 280 if (!kicks[key]) {
2cfc3b91 281 moves.push(km);
5c27a4b5 282 kicks[key] = true;
2cfc3b91
BA
283 }
284 });
f897a4ef 285 if (!adjacentPieces) adjacentPieces = true;
4f3a0823
BA
286 }
287 }
f897a4ef
BA
288 if (adjacentPieces) {
289 // Add the "end" move (even if no valid kicks)
4a3cbf1d
BA
290 outerLoop: for (let i=0; i < V.size.x; i++) {
291 for (let j=0; j < V.size.y; j++) {
292 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) {
45f79d0f
BA
293 moves.push({
294 appear: [], vanish: [],
295 start: { x: x, y: y }, end: { x: i, y: j }
296 });
f897a4ef 297 if (computer) break outerLoop; //no choice for computer
4a3cbf1d 298 }
4f3a0823
BA
299 }
300 }
301 }
302 return moves;
303 }
304
4313762d
BA
305 canTake() {
306 return false;
4f3a0823
BA
307 }
308
309 // Extra arg "computer" to avoid trimming all redundant pass moves:
310 getAllPotentialMoves(computer) {
311 const color = this.turn;
312 let potentialMoves = [];
313 for (let i = 0; i < V.size.x; i++) {
314 for (let j = 0; j < V.size.y; j++) {
315 if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
316 Array.prototype.push.apply(
317 potentialMoves,
318 this.getPotentialMovesFrom([i, j], computer)
319 );
320 }
321 }
322 }
323 return potentialMoves;
324 }
325
326 getAllValidMoves() {
327 return this.filterValid(this.getAllPotentialMoves("computer"));
328 }
329
330 filterValid(moves) {
331 const L = this.kickedBy.length;
332 const kb = this.kickedBy[L-1];
de9e4580 333 return moves.filter(m => !m.start.by || !kb[m.start.by]);
4f3a0823
BA
334 }
335
107dc1bd
BA
336 getCheckSquares() {
337 return [];
338 }
339
4f3a0823
BA
340 allowAnotherPass(color) {
341 // Two cases: a piece moved, or the ball moved.
342 // In both cases, check our pieces and ball proximity,
343 // so the move played doesn't matter (if ball position updated)
344 const bp = this.ballPos;
345 const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
346 for (let s of steps) {
347 const [i, j] = [this.ballPos[0] + s[0], this.ballPos[1] + s[1]];
348 if (
349 V.OnBoard(i, j) &&
350 this.board[i][j] != V.EMPTY &&
351 this.getColor(i, j) == color
352 ) {
353 return true; //potentially...
354 }
355 }
356 return false;
357 }
358
359 prePlay(move) {
360 if (move.appear[0].p == 'a')
361 this.ballPos = [move.appear[0].x, move.appear[0].y];
362 }
363
364 play(move) {
365 // Special message saying "passes are over"
366 const passesOver = (move.vanish.length == 2);
367 if (!passesOver) {
368 this.prePlay(move);
369 V.PlayOnBoard(this.board, move);
370 }
371 move.turn = [this.turn, this.subTurn]; //easier undo
372 if (passesOver || !this.allowAnotherPass(this.turn)) {
373 this.turn = V.GetOppCol(this.turn);
374 this.subTurn = 1;
375 this.movesCount++;
376 this.kickedBy.push( {} );
377 }
378 else {
379 this.subTurn++;
de9e4580 380 if (!!move.start.by) {
4f3a0823 381 const L = this.kickedBy.length;
de9e4580 382 this.kickedBy[L-1][move.start.by] = true;
4f3a0823
BA
383 }
384 }
385 }
386
387 undo(move) {
388 const passesOver = (move.vanish.length == 2);
389 if (move.turn[0] != this.turn) {
390 [this.turn, this.subTurn] = move.turn;
391 this.movesCount--;
392 this.kickedBy.pop();
393 }
394 else {
395 this.subTurn--;
de9e4580 396 if (!!move.start.by) {
4f3a0823 397 const L = this.kickedBy.length;
de9e4580 398 delete this.kickedBy[L-1][move.start.by];
4f3a0823
BA
399 }
400 }
401 if (!passesOver) {
402 V.UndoOnBoard(this.board, move);
403 this.postUndo(move);
404 }
405 }
406
407 postUndo(move) {
408 if (move.vanish[0].p == 'a')
409 this.ballPos = [move.vanish[0].x, move.vanish[0].y];
410 }
107dc1bd
BA
411
412 getCurrentScore() {
4f3a0823
BA
413 if (this.board[0][4] == V.BALL) return "1-0";
414 if (this.board[8][4] == V.BALL) return "0-1";
415 return "*";
107dc1bd 416 }
b9ce3d0f 417
4f3a0823
BA
418 getComputerMove() {
419 let initMoves = this.getAllValidMoves();
420 if (initMoves.length == 0) return null;
421 let moves = JSON.parse(JSON.stringify(initMoves));
422 let mvArray = [];
423 let mv = null;
424 // Just play random moves (for now at least. TODO?)
425 const c = this.turn;
426 while (moves.length > 0) {
427 mv = moves[randInt(moves.length)];
428 mvArray.push(mv);
429 this.play(mv);
430 if (mv.vanish.length == 1 && this.allowAnotherPass(c))
431 // Potential kick
432 moves = this.getPotentialMovesFrom(this.ballPos);
433 else break;
434 }
435 for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
436 return (mvArray.length > 1 ? mvArray : mvArray[0]);
437 }
438
439 // NOTE: evalPosition() is wrong, but unused since bot plays at random
440
441 getNotation(move) {
442 if (move.vanish.length == 2) return "pass";
443 if (move.vanish[0].p != 'a') return super.getNotation(move);
444 // Kick: simple notation (TODO?)
445 return V.CoordsToSquare(move.end);
b9ce3d0f 446 }
7e8a7ea1 447
107dc1bd 448};