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