X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FFootball.js;h=b1630eac24bbedd3e734a0489df19794b3615476;hp=430e31ac3d0e74e43c092856156ac3e433033264;hb=4f3a08234f754abcb74f369067f960a8269557a3;hpb=8ec71052706a106ea3d04316d7f76a48aff98bec diff --git a/client/src/variants/Football.js b/client/src/variants/Football.js index 430e31ac..b1630eac 100644 --- a/client/src/variants/Football.js +++ b/client/src/variants/Football.js @@ -1,47 +1,55 @@ import { ChessRules } from "@/base_rules"; -import { SuicideRules } from "@/variants/Suicide"; +import { randInt } from "@/utils/alea"; export class FootballRules extends ChessRules { + static get HasEnpassant() { + return false; + } + static get HasFlags() { return false; } - static get PawnSpecs() { - return Object.assign( - {}, - ChessRules.PawnSpecs, - { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) } - ); + static get size() { + return { x: 9, y: 9 }; } static get Lines() { return [ // White goal: - [[0, 3], [0, 5]], + [[0, 4], [0, 5]], [[0, 5], [1, 5]], - [[1, 5], [1, 3]], - [[1, 3], [0, 3]], + [[1, 4], [0, 4]], // Black goal: - [[8, 3], [8, 5]], - [[8, 5], [7, 5]], - [[7, 5], [7, 3]], - [[7, 3], [8, 3]] + [[9, 4], [9, 5]], + [[9, 5], [8, 5]], + [[8, 4], [9, 4]] ]; } + static get BALL() { + // 'b' is already taken: + return "aa"; + } + + // Check that exactly one ball is on the board + // + at least one piece per color. static IsGoodPosition(position) { if (position.length == 0) return false; const rows = position.split("/"); if (rows.length != V.size.x) return false; - // Just check that at least one piece of each color is there: let pieces = { "w": 0, "b": 0 }; + let ballCount = 0; for (let row of rows) { let sumElts = 0; for (let i = 0; i < row.length; i++) { const lowerRi = row[i].toLowerCase(); - if (V.PIECES.includes(lowerRi)) { - pieces[row[i] == lowerRi ? "b" : "w"]++; + if (!!lowerRi.match(/^[a-z]$/)) { + if (V.PIECES.includes(lowerRi)) + pieces[row[i] == lowerRi ? "b" : "w"]++; + else if (lowerRi == 'a') ballCount++; + else return false; sumElts++; } else { @@ -52,37 +60,340 @@ export class FootballRules extends ChessRules { } if (sumElts != V.size.y) return false; } - if (Object.values(pieces).some(v => v == 0)) return false; + if (ballCount != 1 || Object.values(pieces).some(v => v == 0)) + return false; return true; } - scanKings() {} + static board2fen(b) { + if (b == V.BALL) return 'a'; + return ChessRules.board2fen(b); + } - filterValid(moves) { + static fen2board(f) { + if (f == 'a') return V.BALL; + return ChessRules.fen2board(f); + } + + getPpath(b) { + if (b == V.BALL) return "Football/ball"; + return b; + } + + canIplay(side, [x, y]) { + return ( + side == this.turn && + (this.board[x][y] == V.BALL || this.getColor(x, y) == side) + ); + } + + // No checks or king tracking etc. But, track ball + setOtherVariables() { + // Stack of "kicked by" coordinates, to avoid infinite loops + this.kickedBy = [ {} ]; + this.subTurn = 1; + this.ballPos = [-1, -1]; + for (let i=0; i < V.size.x; i++) { + for (let j=0; j< V.size.y; j++) { + if (this.board[i][j] == V.BALL) { + this.ballPos = [i, j]; + return; + } + } + } + } + + static GenRandInitFen(randomness) { + if (randomness == 0) + return "rnbq1knbr/9/9/9/4a4/9/9/9/RNBQ1KNBR w 0"; + + // TODO: following is mostly copy-paste from Suicide variant + let pieces = { w: new Array(8), b: new Array(8) }; + for (let c of ["w", "b"]) { + if (c == 'b' && randomness == 1) { + pieces['b'] = pieces['w']; + break; + } + + // Get random squares for every piece, totally freely + let positions = shuffle(ArrayFun.range(8)); + const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q']; + const rem2 = positions[0] % 2; + if (rem2 == positions[1] % 2) { + // Fix bishops (on different colors) + for (let i=2; i<8; i++) { + if (positions[i] % 2 != rem2) + [positions[1], positions[i]] = [positions[i], positions[1]]; + } + } + for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i]; + } + const piecesB = pieces["b"].join("") ; + const piecesW = pieces["w"].join("").toUpperCase(); + return ( + piecesB.substr(0, 4) + "1" + piecesB.substr(4) + + "/9/9/9/4a4/9/9/9/" + + piecesW.substr(0, 4) + "1" + piecesW.substr(4) + + " w 0" + ); + } + + tryKickFrom([x, y]) { + const bp = this.ballPos; + const emptySquare = (i, j) => { + return V.OnBoard(i, j) && this.board[i][j] == V.EMPTY; + }; + // Kick the (adjacent) ball from x, y with current turn: + const step = [bp[0] - x, bp[1] - y]; + const piece = this.getPiece(x, y); + let moves = []; + if (piece == V.KNIGHT) { + // The knight case is particular + V.steps[V.KNIGHT].forEach(s => { + const [i, j] = [bp[0] + s[0], bp[1] + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] == V.EMPTY && + ( + // In a corner? The, allow all ball moves + ([0, 8].includes(bp[0]) && [0, 8].includes(bp[1])) || + // Do not end near the knight + (Math.abs(i - x) >= 2 || Math.abs(j - y) >= 2) + ) + ) { + moves.push(super.getBasicMove(bp, [i, j])); + } + }); + } + else { + let compatible = false, + oneStep = false; + switch (piece) { + case V.ROOK: + compatible = (step[0] == 0 || step[1] == 0); + break; + case V.BISHOP: + compatible = (step[0] != 0 && step[1] != 0); + break; + case V.QUEEN: + compatible = true; + break; + case V.KING: + compatible = true; + oneStep = true; + break; + } + if (!compatible) return []; + let [i, j] = [bp[0] + step[0], bp[1] + step[1]]; + const horizontalStepOnGoalRow = + ([0, 8].includes(bp[0]) && step.some(s => s == 0)); + if (emptySquare(i, j) && (!horizontalStepOnGoalRow || j != 4)) { + moves.push(super.getBasicMove(bp, [i, j])); + if (!oneStep) { + do { + i += step[0]; + j += step[1]; + if (!emptySquare(i, j)) break; + if (!horizontalStepOnGoalRow || j != 4) + moves.push(super.getBasicMove(bp, [i, j])); + } while (true); + } + } + } + const kickedFrom = x + "-" + y; + moves.forEach(m => m.by = kickedFrom) return moves; } + getPotentialMovesFrom([x, y], computer) { + if (V.PIECES.includes(this.getPiece(x, y))) { + if (this.subTurn > 1) return []; + return ( + super.getPotentialMovesFrom([x, y]) + .filter(m => m.end.y != 4 || ![0, 8].includes(m.end.x)) + ); + } + // Kicking the ball: look for adjacent pieces. + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + const c = this.turn; + let moves = []; + for (let s of steps) { + const [i, j] = [x + s[0], y + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == c + ) { + Array.prototype.push.apply(moves, this.tryKickFrom([i, j])); + } + } + // And, always add the "end" move. For computer, keep only one + outerLoop: for (let i=0; i < V.size.x; i++) { + for (let j=0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) { + moves.push(super.getBasicMove([x, y], [i, j])); + if (!!computer) break outerLoop; + } + } + } + return moves; + } + + // No captures: + getSlideNJumpMoves([x, y], steps, oneStep) { + let moves = []; + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = y + step[1]; + let stepCount = 1; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + moves.push(this.getBasicMove([x, y], [i, j])); + if (!!oneStep) continue outerLoop; + i += step[0]; + j += step[1]; + stepCount++; + } + } + return moves; + } + + // Extra arg "computer" to avoid trimming all redundant pass moves: + getAllPotentialMoves(computer) { + const color = this.turn; + let potentialMoves = []; + for (let i = 0; i < V.size.x; i++) { + for (let j = 0; j < V.size.y; j++) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) { + Array.prototype.push.apply( + potentialMoves, + this.getPotentialMovesFrom([i, j], computer) + ); + } + } + } + return potentialMoves; + } + + getAllValidMoves() { + return this.filterValid(this.getAllPotentialMoves("computer")); + } + + filterValid(moves) { + const L = this.kickedBy.length; + const kb = this.kickedBy[L-1]; + return moves.filter(m => !m.by || !kb[m.by]); + } + getCheckSquares() { return []; } - // No variables update because no royal king + no castling - prePlay() {} - postPlay() {} - preUndo() {} - postUndo() {} + allowAnotherPass(color) { + // Two cases: a piece moved, or the ball moved. + // In both cases, check our pieces and ball proximity, + // so the move played doesn't matter (if ball position updated) + const bp = this.ballPos; + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let s of steps) { + const [i, j] = [this.ballPos[0] + s[0], this.ballPos[1] + s[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color + ) { + return true; //potentially... + } + } + return false; + } + + prePlay(move) { + if (move.appear[0].p == 'a') + this.ballPos = [move.appear[0].x, move.appear[0].y]; + } + + play(move) { + // Special message saying "passes are over" + const passesOver = (move.vanish.length == 2); + if (!passesOver) { + this.prePlay(move); + V.PlayOnBoard(this.board, move); + } + move.turn = [this.turn, this.subTurn]; //easier undo + if (passesOver || !this.allowAnotherPass(this.turn)) { + this.turn = V.GetOppCol(this.turn); + this.subTurn = 1; + this.movesCount++; + this.kickedBy.push( {} ); + } + else { + this.subTurn++; + if (!!move.by) { + const L = this.kickedBy.length; + this.kickedBy[L-1][move.by] = true; + } + } + } + + undo(move) { + const passesOver = (move.vanish.length == 2); + if (move.turn[0] != this.turn) { + [this.turn, this.subTurn] = move.turn; + this.movesCount--; + this.kickedBy.pop(); + } + else { + this.subTurn--; + if (!!move.by) { + const L = this.kickedBy.length; + delete this.kickedBy[L-1][move.by]; + } + } + if (!passesOver) { + V.UndoOnBoard(this.board, move); + this.postUndo(move); + } + } + + postUndo(move) { + if (move.vanish[0].p == 'a') + this.ballPos = [move.vanish[0].x, move.vanish[0].y]; + } getCurrentScore() { - const oppCol = V.GetOppCol(this.turn); - const goal = (oppCol == 'w' ? 0 : 7); - if (this.board[goal].slice(3, 5).some(b => b[0] == oppCol)) - return oppCol == 'w' ? "1-0" : "0-1"; - if (this.atLeastOneMove()) return "*"; - return "1/2"; + if (this.board[0][4] == V.BALL) return "1-0"; + if (this.board[8][4] == V.BALL) return "0-1"; + return "*"; } - static GenRandInitFen(randomness) { - return SuicideRules.GenRandInitFen(randomness); + getComputerMove() { + let initMoves = this.getAllValidMoves(); + if (initMoves.length == 0) return null; + let moves = JSON.parse(JSON.stringify(initMoves)); + let mvArray = []; + let mv = null; + // Just play random moves (for now at least. TODO?) + const c = this.turn; + while (moves.length > 0) { + mv = moves[randInt(moves.length)]; + mvArray.push(mv); + this.play(mv); + if (mv.vanish.length == 1 && this.allowAnotherPass(c)) + // Potential kick + moves = this.getPotentialMovesFrom(this.ballPos); + else break; + } + for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]); + return (mvArray.length > 1 ? mvArray : mvArray[0]); + } + + // NOTE: evalPosition() is wrong, but unused since bot plays at random + + getNotation(move) { + if (move.vanish.length == 2) return "pass"; + if (move.vanish[0].p != 'a') return super.getNotation(move); + // Kick: simple notation (TODO?) + return V.CoordsToSquare(move.end); } };