X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FMaxima.js;fp=client%2Fsrc%2Fvariants%2FMaxima.js;h=ab97a2177308fa7f4cb9ede94267c38bd4803816;hb=e90bafa8fb5fb7641728231bf2398590d96c672a;hp=0000000000000000000000000000000000000000;hpb=2f8dce6a81063289d9d4cbca7971f80b1b194b84;p=vchess.git diff --git a/client/src/variants/Maxima.js b/client/src/variants/Maxima.js new file mode 100644 index 00000000..ab97a217 --- /dev/null +++ b/client/src/variants/Maxima.js @@ -0,0 +1,832 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; +import { shuffle } from "@/utils/alea"; + +export class MaximaRules extends ChessRules { + static get HasFlags() { + return false; + } + + static get HasEnpassant() { + return false; + } + + static get PIECES() { + return ChessRules.PIECES.concat([V.IMMOBILIZER, V.MAGE, V.GUARD]); + } + + getPpath(b) { + if (b[0] == 'x') return "Maxima/nothing"; + if (['m','d','g'].includes(b[1])) + return "Maxima/" + b; + return b; + } + + // For space next to the palaces: + static get NOTHING() { + return "xx"; + } + + static board2fen(b) { + if (b[0] == 'x') return 'x'; + return ChessRules.board2fen(b); + } + + static fen2board(f) { + if (f == 'x') return V.NOTHING; + return ChessRules.fen2board(f); + } + + // TODO: the wall position should be checked too + static IsGoodPosition(position) { + if (position.length == 0) return false; + const rows = position.split("/"); + if (rows.length != V.size.x) return false; + let kings = { "k": 0, "K": 0 }; + for (let row of rows) { + let sumElts = 0; + for (let i = 0; i < row.length; i++) { + if (['K','k'].includes(row[i])) kings[row[i]]++; + if (['x'].concat(V.PIECES).includes(row[i].toLowerCase())) sumElts++; + else { + const num = parseInt(row[i]); + if (isNaN(num)) return false; + sumElts += num; + } + } + if (sumElts != V.size.y) return false; + } + if (Object.values(kings).some(v => v != 1)) return false; + return true; + } + + // No castling, but checks, so keep track of kings + setOtherVariables(fen) { + this.kingPos = { w: [-1, -1], b: [-1, -1] }; + const fenParts = fen.split(" "); + const position = fenParts[0].split("/"); + for (let i = 0; i < position.length; i++) { + let k = 0; + for (let j = 0; j < position[i].length; j++) { + switch (position[i].charAt(j)) { + case "k": + this.kingPos["b"] = [i, k]; + break; + case "K": + this.kingPos["w"] = [i, k]; + break; + default: { + const num = parseInt(position[i].charAt(j)); + if (!isNaN(num)) k += num - 1; + } + } + k++; + } + } + } + + static get size() { + return { x: 11, y: 8 }; + } + + static OnBoard(x, y) { + return ( + (x >= 1 && x <= 9 && y >= 0 && y <= 7) || + ([3, 4].includes(y) && [0, 10].includes(x)) + ); + } + + static get IMMOBILIZER() { + return "m"; + } + static get MAGE() { + return 'g'; + } + static get GUARD() { + return 'd'; + } + // Although other pieces keep their names here for coding simplicity, + // keep in mind that: + // - a "rook" is a coordinator, capturing by coordinating with the king + // - a "knight" is a long-leaper, capturing as in draughts + // - a "bishop" is a chameleon, capturing as its prey + // - a "queen" is a withdrawer, capturing by moving away from pieces + + // Is piece on square (x,y) immobilized? + isImmobilized([x, y]) { + const piece = this.getPiece(x, y); + if (piece == V.MAGE) + // Mages are not immobilized: + return false; + const oppCol = V.GetOppCol(this.getColor(x, y)); + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let step of adjacentSteps) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + const oppPiece = this.getPiece(i, j); + if (oppPiece == V.IMMOBILIZER) return [i, j]; + // Only immobilizers are immobilized by chameleons: + if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) return [i, j]; + } + } + return null; + } + + getPotentialMovesFrom([x, y]) { + // Pre-check: is thing on this square immobilized? + const imSq = this.isImmobilized([x, y]); + const piece = this.getPiece(x, y); + if (!!imSq && piece != V.KING) { + // Only option is suicide, if I'm not a king: + return [ + new Move({ + start: { x: x, y: y }, + end: { x: imSq[0], y: imSq[1] }, + appear: [], + vanish: [ + new PiPo({ + x: x, + y: y, + c: this.getColor(x, y), + p: this.getPiece(x, y) + }) + ] + }) + ]; + } + let moves = undefined; + switch (piece) { + case V.IMMOBILIZER: + moves = this.getPotentialImmobilizerMoves([x, y]); + break; + case V.GUARD: + moves = this.getPotentialGuardMoves([x, y]); + break; + case V.MAGE: + moves = this.getPotentialMageMoves([x, y]); + break; + default: + moves = super.getPotentialMovesFrom([x, y]); + } + const pX = (this.turn == 'w' ? 10 : 0); + if (this.board[pX][3] == V.EMPTY && this.board[pX][4] == V.EMPTY) + return moves; + // Filter out moves resulting in self palace occupation: + // NOTE: cannot invade own palace but still check the king there. + const pY = (this.board[pX][3] == V.EMPTY ? 4 : 3); + return moves.filter(m => m.end.x != pX || m.end.y != pY); + } + + getSlideNJumpMoves([x, y], steps, oneStep, mageInitSquare, onlyTake) { + const piece = !mageInitSquare ? this.getPiece(x, y) : V.MAGE; + const initSquare = mageInitSquare || [x, y]; + let moves = []; + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = y + step[1]; + if (piece == V.KING) j = j % V.size.y; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (!onlyTake) moves.push(this.getBasicMove(initSquare, [i, j])); + if (!!oneStep) continue outerLoop; + i += step[0]; + j += step[1]; + } + // Only king, guard and mage + chameleon can take on occupied square: + if ( + V.OnBoard(i, j) + && + this.canTake(initSquare, [i, j]) + && + ( + [V.KING, V.GUARD, V.MAGE].includes(piece) || + (piece == V.BISHOP && this.getPiece(i, j) === onlyTake) + ) + ) { + moves.push(this.getBasicMove(initSquare, [i, j])); + } + } + return moves; + } + + // Modify capturing moves among listed pawn moves + addPawnCaptures(moves, byChameleon) { + const steps = V.steps[V.ROOK]; + const color = this.turn; + const oppCol = V.GetOppCol(color); + moves.forEach(m => { + if (!!byChameleon && m.start.x != m.end.x && m.start.y != m.end.y) + // Chameleon not moving as pawn + return; + // Try capturing in every direction + for (let step of steps) { + const sq2 = [m.end.x + 2 * step[0], m.end.y + 2 * step[1]]; + if ( + V.OnBoard(sq2[0], sq2[1]) && + this.board[sq2[0]][sq2[1]] != V.EMPTY && + this.getColor(sq2[0], sq2[1]) == color + ) { + // Potential capture + const sq1 = [m.end.x + step[0], m.end.y + step[1]]; + if ( + this.board[sq1[0]][sq1[1]] != V.EMPTY && + this.getColor(sq1[0], sq1[1]) == oppCol + ) { + const piece1 = this.getPiece(sq1[0], sq1[1]); + if (!byChameleon || piece1 == V.PAWN) { + m.vanish.push( + new PiPo({ + x: sq1[0], + y: sq1[1], + c: oppCol, + p: piece1 + }) + ); + } + } + } + } + }); + } + + // "Pincer" + getPotentialPawnMoves([x, y]) { + let moves = super.getPotentialRookMoves([x, y]); + this.addPawnCaptures(moves); + return moves; + } + + addRookCaptures(moves, byChameleon) { + const color = this.turn; + const oppCol = V.GetOppCol(color); + const kp = this.kingPos[color]; + moves.forEach(m => { + // Check piece-king rectangle (if any) corners for enemy pieces + if (m.end.x == kp[0] || m.end.y == kp[1]) return; //"flat rectangle" + const corner1 = [m.end.x, kp[1]]; + const corner2 = [kp[0], m.end.y]; + for (let [i, j] of [corner1, corner2]) { + if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == oppCol) { + const piece = this.getPiece(i, j); + if (!byChameleon || piece == V.ROOK) { + m.vanish.push( + new PiPo({ + x: i, + y: j, + p: piece, + c: oppCol + }) + ); + } + } + } + }); + } + + // Coordinator + getPotentialRookMoves(sq) { + let moves = super.getPotentialQueenMoves(sq); + this.addRookCaptures(moves); + return moves; + } + + getKnightCaptures(startSquare, byChameleon) { + // Look in every direction for captures + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + const color = this.turn; + const oppCol = V.GetOppCol(color); + let moves = []; + const [x, y] = [startSquare[0], startSquare[1]]; + const piece = this.getPiece(x, y); //might be a chameleon! + outerLoop: for (let step of steps) { + let [i, j] = [x + step[0], y + step[1]]; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += step[0]; + j += step[1]; + } + if ( + !V.OnBoard(i, j) || + this.getColor(i, j) == color || + (!!byChameleon && this.getPiece(i, j) != V.KNIGHT) + ) { + continue; + } + // last(thing), cur(thing) : stop if "cur" is our color, + // or beyond board limits, or if "last" isn't empty and cur neither. + // Otherwise, if cur is empty then add move until cur square; + // if cur is occupied then stop if !!byChameleon and the square not + // occupied by a leaper. + let last = [i, j]; + let cur = [i + step[0], j + step[1]]; + let vanished = [new PiPo({ x: x, y: y, c: color, p: piece })]; + while (V.OnBoard(cur[0], cur[1])) { + if (this.board[last[0]][last[1]] != V.EMPTY) { + const oppPiece = this.getPiece(last[0], last[1]); + if (!!byChameleon && oppPiece != V.KNIGHT) continue outerLoop; + // Something to eat: + vanished.push( + new PiPo({ x: last[0], y: last[1], c: oppCol, p: oppPiece }) + ); + } + if (this.board[cur[0]][cur[1]] != V.EMPTY) { + if ( + this.getColor(cur[0], cur[1]) == color || + this.board[last[0]][last[1]] != V.EMPTY + ) { + //TODO: redundant test + continue outerLoop; + } + } else { + moves.push( + new Move({ + appear: [new PiPo({ x: cur[0], y: cur[1], c: color, p: piece })], + vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required? + start: { x: x, y: y }, + end: { x: cur[0], y: cur[1] } + }) + ); + } + last = [last[0] + step[0], last[1] + step[1]]; + cur = [cur[0] + step[0], cur[1] + step[1]]; + } + } + return moves; + } + + // Long-leaper + getPotentialKnightMoves(sq) { + return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq)); + } + + // Chameleon + getPotentialBishopMoves([x, y]) { + let moves = super + .getPotentialQueenMoves([x, y]) + .concat(this.getKnightCaptures([x, y], "asChameleon")) + .concat(this.getPotentialGuardMoves([x, y], "asChameleon")) + .concat(this.getPotentialMageMoves([x, y], "asChameleon")); + // No "king capture" because king cannot remain under check + this.addPawnCaptures(moves, "asChameleon"); + this.addRookCaptures(moves, "asChameleon"); + this.addQueenCaptures(moves, "asChameleon"); + // Post-processing: merge similar moves, concatenating vanish arrays + let mergedMoves = {}; + moves.forEach(m => { + const key = m.end.x + V.size.x * m.end.y; + if (!mergedMoves[key]) mergedMoves[key] = m; + else { + for (let i = 1; i < m.vanish.length; i++) + mergedMoves[key].vanish.push(m.vanish[i]); + } + }); + return Object.values(mergedMoves); + } + + addQueenCaptures(moves, byChameleon) { + if (moves.length == 0) return; + const [x, y] = [moves[0].start.x, moves[0].start.y]; + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + let capturingDirections = []; + const color = this.turn; + const oppCol = V.GetOppCol(color); + adjacentSteps.forEach(step => { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol && + (!byChameleon || this.getPiece(i, j) == V.QUEEN) + ) { + capturingDirections.push(step); + } + }); + moves.forEach(m => { + const step = [ + m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0, + m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0 + ]; + // TODO: this test should be done only once per direction + if ( + capturingDirections.some(dir => { + return dir[0] == -step[0] && dir[1] == -step[1]; + }) + ) { + const [i, j] = [x - step[0], y - step[1]]; + m.vanish.push( + new PiPo({ + x: i, + y: j, + p: this.getPiece(i, j), + c: oppCol + }) + ); + } + }); + } + + // Withdrawer + getPotentialQueenMoves(sq) { + let moves = super.getPotentialQueenMoves(sq); + this.addQueenCaptures(moves); + return moves; + } + + getPotentialImmobilizerMoves(sq) { + // Immobilizer doesn't capture + return super.getPotentialQueenMoves(sq); + } + + getPotentialKingMoves(sq) { + return this.getSlideNJumpMoves(sq, V.steps[V.KNIGHT], "oneStep"); + } + + getPotentialGuardMoves(sq, byChameleon) { + const onlyTake = !byChameleon ? null : V.GUARD; + return ( + this.getSlideNJumpMoves( + sq, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep", + null, + onlyTake + ) + ); + } + + getNextMageSteps(step) { + if (step[0] == -1) { + if (step[1] == -1) return [[-1, 0], [0, -1]]; + return [[-1, 0], [0, 1]]; + } + if (step[1] == -1) return [[1, 0], [0, -1]]; + return [[1, 0], [0, 1]]; + } + + getPotentialMageMoves([x, y], byChameleon) { + const oppCol = V.GetOppCol(this.turn); + const onlyTake = !byChameleon ? null : V.MAGE; + let moves = []; + for (let step of V.steps[V.BISHOP]) { + let [i, j] = [x + step[0], y + step[1]]; + if (!V.OnBoard(i, j)) continue; + if (this.board[i][j] != V.EMPTY) { + if ( + this.getColor(i, j) == oppCol && + (!onlyTake || this.getPiece(i, j) == V.MAGE) + ) { + // Capture + moves.push(this.getBasicMove([x, y], [i, j])); + } + } + else { + if (!onlyTake) moves.push(this.getBasicMove([x, y], [i, j])); + // Continue orthogonally: + const stepO = this.getNextMageSteps(step); + Array.prototype.push.apply( + moves, + this.getSlideNJumpMoves([i, j], stepO, null, [x, y], onlyTake) + ); + } + } + return moves; + } + + isAttacked(sq, color) { + return ( + super.isAttacked(sq, color) || + this.isAttackedByGuard(sq, color) || + this.isAttackedByMage(sq, color) + ); + } + + isAttackedByPawn([x, y], color) { + // Square (x,y) must be surroundable by two enemy pieces, + // and one of them at least should be a pawn (moving). + const dirs = [ + [1, 0], + [0, 1] + ]; + const steps = V.steps[V.ROOK]; + for (let dir of dirs) { + const [i1, j1] = [x - dir[0], y - dir[1]]; //"before" + const [i2, j2] = [x + dir[0], y + dir[1]]; //"after" + if (V.OnBoard(i1, j1) && V.OnBoard(i2, j2)) { + if ( + ( + this.board[i1][j1] != V.EMPTY && + this.getColor(i1, j1) == color && + this.board[i2][j2] == V.EMPTY + ) + || + ( + this.board[i2][j2] != V.EMPTY && + this.getColor(i2, j2) == color && + this.board[i1][j1] == V.EMPTY + ) + ) { + // Search a movable enemy pawn landing on the empty square + for (let step of steps) { + let [ii, jj] = this.board[i1][j1] == V.EMPTY ? [i1, j1] : [i2, j2]; + let [i3, j3] = [ii + step[0], jj + step[1]]; + while (V.OnBoard(i3, j3) && this.board[i3][j3] == V.EMPTY) { + i3 += step[0]; + j3 += step[1]; + } + if ( + V.OnBoard(i3, j3) && + this.getColor(i3, j3) == color && + this.getPiece(i3, j3) == V.PAWN && + !this.isImmobilized([i3, j3]) + ) { + return true; + } + } + } + } + } + return false; + } + + isAttackedByRook([x, y], color) { + // King must be on same column or row, + // and a rook should be able to reach a capturing square + const sameRow = x == this.kingPos[color][0]; + const sameColumn = y == this.kingPos[color][1]; + if (sameRow || sameColumn) { + // Look for the enemy rook (maximum 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.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.ROOK + ) { + if (this.isImmobilized([i, j])) + // Because only one rook: + return false; + // Can it reach a capturing square? Easy but quite suboptimal way + // (TODO: generate all moves (turn is OK)) + const moves = this.getPotentialMovesFrom([i, j]); + for (let move of moves) { + if ( + (sameRow && move.end.y == y) || + (sameColumn && move.end.x == x) + ) { + return true; + } + } + } + } + } + } + return false; + } + + isAttackedByKnight([x, y], color) { + // Square (x,y) must be on same line as a knight, + // and there must be empty square(s) behind. + const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + outerLoop: for (let step of steps) { + const [i0, j0] = [x + step[0], y + step[1]]; + if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) { + // Try in opposite direction: + let [i, j] = [x - step[0], y - step[1]]; + while (V.OnBoard(i, j)) { + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i -= step[0]; + j -= step[1]; + } + if (V.OnBoard(i, j)) { + if (this.getColor(i, j) == color) { + if ( + this.getPiece(i, j) == V.KNIGHT && + !this.isImmobilized([i, j]) + ) { + return true; + } + continue outerLoop; + } + // [else] Our color, + // could be captured *if there was an empty space* + if (this.board[i + step[0]][j + step[1]] != V.EMPTY) + continue outerLoop; + i -= step[0]; + j -= step[1]; + } + } + } + } + return false; + } + + isAttackedByBishop([x, y], color) { + // We cheat a little here: since this function is used exclusively for + // the king, it's enough to check the immediate surrounding of the square. + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let step of adjacentSteps) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.BISHOP && + !this.isImmobilized([i, j]) + ) { + return true; + } + } + return false; + } + + isAttackedByQueen([x, y], color) { + // Square (x,y) must be adjacent to a queen, and the queen must have + // some free space in the opposite direction from (x,y) + const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]); + for (let step of adjacentSteps) { + const sq2 = [x + 2 * step[0], y + 2 * step[1]]; + if (V.OnBoard(sq2[0], sq2[1]) && this.board[sq2[0]][sq2[1]] == V.EMPTY) { + const sq1 = [x + step[0], y + step[1]]; + if ( + this.board[sq1[0]][sq1[1]] != V.EMPTY && + this.getColor(sq1[0], sq1[1]) == color && + this.getPiece(sq1[0], sq1[1]) == V.QUEEN && + !this.isImmobilized(sq1) + ) { + return true; + } + } + } + return false; + } + + isAttackedByKing([x, y], color) { + for (let step of V.steps[V.KNIGHT]) { + let rx = x + step[0], + // Circular board for king-knight: + ry = (y + step[1]) % V.size.y; + if ( + V.OnBoard(rx, ry) && + this.getPiece(rx, ry) === V.KING && + this.getColor(rx, ry) == color && + !this.isImmobilized([rx, ry]) + ) { + return true; + } + } + return false; + } + + isAttackedByGuard(sq, color) { + return ( + super.isAttackedBySlideNJump( + sq, + color, + V.GUARD, + V.steps[V.ROOK].concat(V.steps[V.BISHOP]), + "oneStep" + ) + ); + } + + getNextMageCheck(step) { + if (step[0] == 0) { + if (step[1] == 1) return [[1, 1], [-1, 1]]; + return [[-1, -1], [1, -1]]; + } + if (step[0] == -1) return [[-1, -1], [-1, 1]]; + return [[1, 1], [1, -1]]; + } + + isAttackedByMage([x, y], color) { + for (let step of V.steps[V.BISHOP]) { + const [i, j] = [x + step[0], y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == color && + this.getPiece(i, j) == V.MAGE + ) { + return true; + } + } + for (let step of V.steps[V.ROOK]) { + let [i, j] = [x + step[0], y + step[1]]; + const stepM = this.getNextMageCheck(step); + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + for (let s of stepM) { + const [ii, jj] = [i + s[0], j + s[1]]; + if ( + V.OnBoard(ii, jj) && + this.board[ii][jj] != V.EMPTY && + this.getColor(ii, jj) == color && + this.getPiece(ii, jj) == V.MAGE + ) { + return true; + } + } + i += step[0]; + j += step[1]; + } + } + return false; + } + + getCurrentScore() { + const color = this.turn; + const getScoreLost = () => { + // Result if I lose: + return color == "w" ? "0-1" : "1-0"; + }; + if (!this.atLeastOneMove()) { + // No valid move: I lose or draw + if (this.underCheck(color)) return getScoreLost(); + return "1/2"; + } + // I lose also if no pieces left (except king) + let piecesLeft = 0; + outerLoop: for (let i=0; i 1 ? "x" : "") + finalSquare; + else notation = move.appear[0].p.toUpperCase() + finalSquare; + // Add a capture mark (not describing what is captured...): + if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X"; + return notation; + } +};