| 1 | import { ChessRules } from "@/base_rules"; |
| 2 | import { randInt, shuffle } from "@/utils/alea"; |
| 3 | import { ArrayFun } from "@/utils/array"; |
| 4 | |
| 5 | export class FootballRules extends ChessRules { |
| 6 | |
| 7 | static get HasEnpassant() { |
| 8 | return false; |
| 9 | } |
| 10 | |
| 11 | static get HasFlags() { |
| 12 | return false; |
| 13 | } |
| 14 | |
| 15 | static get size() { |
| 16 | return { x: 9, y: 9 }; |
| 17 | } |
| 18 | |
| 19 | static get Lines() { |
| 20 | return [ |
| 21 | // White goal: |
| 22 | [[0, 4], [0, 5]], |
| 23 | [[0, 5], [1, 5]], |
| 24 | [[1, 4], [0, 4]], |
| 25 | // Black goal: |
| 26 | [[9, 4], [9, 5]], |
| 27 | [[9, 5], [8, 5]], |
| 28 | [[8, 4], [9, 4]] |
| 29 | ]; |
| 30 | } |
| 31 | |
| 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. |
| 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; |
| 43 | let pieces = { "w": 0, "b": 0 }; |
| 44 | let ballCount = 0; |
| 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(); |
| 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; |
| 54 | sumElts++; |
| 55 | } |
| 56 | else { |
| 57 | const num = parseInt(row[i], 10); |
| 58 | if (isNaN(num)) return false; |
| 59 | sumElts += num; |
| 60 | } |
| 61 | } |
| 62 | if (sumElts != V.size.y) return false; |
| 63 | } |
| 64 | if (ballCount != 1 || Object.values(pieces).some(v => v == 0)) |
| 65 | return false; |
| 66 | return true; |
| 67 | } |
| 68 | |
| 69 | static board2fen(b) { |
| 70 | if (b == V.BALL) return 'a'; |
| 71 | return ChessRules.board2fen(b); |
| 72 | } |
| 73 | |
| 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(options) { |
| 108 | if (options.randomness == 0) |
| 109 | return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0"; |
| 110 | |
| 111 | let pieces = { w: new Array(8), b: new Array(8) }; |
| 112 | for (let c of ["w", "b"]) { |
| 113 | if (c == 'b' && options.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 | // 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])) { |
| 126 | for (let i=2; i<8; i++) { |
| 127 | if (realOddity(positions[i]) != rem2) { |
| 128 | [positions[1], positions[i]] = [positions[i], positions[1]]; |
| 129 | break; |
| 130 | } |
| 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 | ( |
| 162 | // In a corner? Then, allow all ball moves |
| 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)); |
| 194 | if ( |
| 195 | emptySquare(i, j) && |
| 196 | (this.movesCount >= 2 || j != 4 || ![0, 8].includes(i)) && |
| 197 | (!horizontalStepOnGoalRow || j != 4) |
| 198 | ) { |
| 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; |
| 205 | if ( |
| 206 | (this.movesCount >= 2 || j != 4 || ![0, 8].includes(i)) && |
| 207 | (!horizontalStepOnGoalRow || j != 4) |
| 208 | ) { |
| 209 | moves.push(super.getBasicMove(bp, [i, j])); |
| 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 | } |
| 233 | } while (true); |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | const kickedFrom = x + "-" + y; |
| 238 | moves.forEach(m => m.start.by = kickedFrom) |
| 239 | return moves; |
| 240 | } |
| 241 | |
| 242 | getPotentialMovesFrom([x, y], computer) { |
| 243 | const piece = this.getPiece(x, y); |
| 244 | if (V.PIECES.includes(piece)) { |
| 245 | if (this.subTurn > 1) return []; |
| 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 |
| 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]; |
| 255 | if ( |
| 256 | this.board[indX[0]][indY[0]] != V.EMPTY && |
| 257 | this.board[indX[1]][indY[1]] == V.EMPTY |
| 258 | ) { |
| 259 | return [super.getBasicMove([x, y], [indX[1], indY[1]])]; |
| 260 | } |
| 261 | } |
| 262 | return moves; |
| 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 = []; |
| 268 | let kicks = {}; |
| 269 | let adjacentPieces = false; |
| 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 | ) { |
| 277 | const kmoves = this.tryKickFrom([i, j]); |
| 278 | kmoves.forEach(km => { |
| 279 | const key = V.CoordsToSquare(km.start) + V.CoordsToSquare(km.end); |
| 280 | if (!kicks[key]) { |
| 281 | moves.push(km); |
| 282 | kicks[key] = true; |
| 283 | } |
| 284 | }); |
| 285 | if (!adjacentPieces) adjacentPieces = true; |
| 286 | } |
| 287 | } |
| 288 | if (adjacentPieces) { |
| 289 | // Add the "end" move (even if no valid kicks) |
| 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) { |
| 293 | moves.push({ |
| 294 | appear: [], vanish: [], |
| 295 | start: { x: x, y: y }, end: { x: i, y: j } |
| 296 | }); |
| 297 | if (computer) break outerLoop; //no choice for computer |
| 298 | } |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | return moves; |
| 303 | } |
| 304 | |
| 305 | canTake() { |
| 306 | return false; |
| 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]; |
| 333 | return moves.filter(m => !m.start.by || !kb[m.start.by]); |
| 334 | } |
| 335 | |
| 336 | getCheckSquares() { |
| 337 | return []; |
| 338 | } |
| 339 | |
| 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 == 0); |
| 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++; |
| 380 | if (!!move.start.by) { |
| 381 | const L = this.kickedBy.length; |
| 382 | this.kickedBy[L-1][move.start.by] = true; |
| 383 | } |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | undo(move) { |
| 388 | const passesOver = (move.vanish.length == 0); |
| 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--; |
| 396 | if (!!move.start.by) { |
| 397 | const L = this.kickedBy.length; |
| 398 | delete this.kickedBy[L-1][move.start.by]; |
| 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 | } |
| 411 | |
| 412 | getCurrentScore() { |
| 413 | if (this.board[0][4] == V.BALL) return "1-0"; |
| 414 | if (this.board[8][4] == V.BALL) return "0-1"; |
| 415 | return "*"; |
| 416 | } |
| 417 | |
| 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 == 0) return "pass"; |
| 443 | if (move.vanish[0].p != 'a') return super.getNotation(move); |
| 444 | // Kick: simple notation (TODO?) |
| 445 | return V.CoordsToSquare(move.end); |
| 446 | } |
| 447 | |
| 448 | }; |