X-Git-Url: https://git.auder.net/?a=blobdiff_plain;f=client%2Fsrc%2Fvariants%2FInterweave.js;fp=client%2Fsrc%2Fvariants%2FInterweave.js;h=c776889f10be2e8a8482b9e023209573210cc8ec;hb=6e47d367d4b1b4500bc46d65b44c5e55b52221bb;hp=0000000000000000000000000000000000000000;hpb=383bf350afc0c2600f2774ebf8572398c072a5af;p=vchess.git diff --git a/client/src/variants/Interweave.js b/client/src/variants/Interweave.js new file mode 100644 index 00000000..c776889f --- /dev/null +++ b/client/src/variants/Interweave.js @@ -0,0 +1,664 @@ +import { ChessRules, PiPo, Move } from "@/base_rules"; +import { ArrayFun } from "@/utils/array"; +import { randInt, shuffle } from "@/utils/alea"; + +export class InterweaveRules extends ChessRules { + static get HasFlags() { + return false; + } + + static GenRandInitFen(randomness) { + if (randomness == 0) + return "rbnkknbr/pppppppp/8/8/8/8/PPPPPPPP/RBNKKNBR w 0 - 000000"; + + 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; + } + + // Each pair of pieces on 2 colors: + const composition = ['r', 'n', 'b', 'k', 'r', 'n', 'b', 'k']; + let positions = shuffle(ArrayFun.range(4)); + for (let i = 0; i < 4; i++) + pieces[c][2 * positions[i]] = composition[i]; + positions = shuffle(ArrayFun.range(4)); + for (let i = 0; i < 4; i++) + pieces[c][2 * positions[i] + 1] = composition[i]; + } + return ( + pieces["b"].join("") + + "/pppppppp/8/8/8/8/PPPPPPPP/" + + pieces["w"].join("").toUpperCase() + + // En-passant allowed, but no flags + " w 0 - 000000" + ); + } + + static IsGoodFen(fen) { + if (!ChessRules.IsGoodFen(fen)) return false; + const fenParsed = V.ParseFen(fen); + // 4) Check captures + if (!fenParsed.captured || !fenParsed.captured.match(/^[0-9]{6,6}$/)) + return false; + return true; + } + + 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 (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; + } + // Both kings should be on board. Exactly two per color. + if (Object.values(kings).some(v => v != 2)) return false; + return true; + } + + static ParseFen(fen) { + const fenParts = fen.split(" "); + return Object.assign( + ChessRules.ParseFen(fen), + { captured: fenParts[4] } + ); + } + + getFen() { + return super.getFen() + " " + this.getCapturedFen(); + } + + getFenForRepeat() { + return super.getFenForRepeat() + "_" + this.getCapturedFen(); + } + + getCapturedFen() { + let counts = [...Array(6).fill(0)]; + [V.ROOK, V.KNIGHT, V.BISHOP].forEach((p,idx) => { + counts[idx] = this.captured["w"][p]; + counts[3 + idx] = this.captured["b"][p]; + }); + return counts.join(""); + } + + scanKings() {} + + setOtherVariables(fen) { + super.setOtherVariables(fen); + const fenParsed = V.ParseFen(fen); + // Initialize captured pieces' counts from FEN + this.captured = { + w: { + [V.ROOK]: parseInt(fenParsed.captured[0]), + [V.KNIGHT]: parseInt(fenParsed.captured[1]), + [V.BISHOP]: parseInt(fenParsed.captured[2]), + }, + b: { + [V.ROOK]: parseInt(fenParsed.captured[3]), + [V.KNIGHT]: parseInt(fenParsed.captured[4]), + [V.BISHOP]: parseInt(fenParsed.captured[5]), + } + }; + // Stack of "last move" only for intermediate captures + this.lastMoveEnd = [null]; + } + + // Trim all non-capturing moves + static KeepCaptures(moves) { + return moves.filter(m => m.vanish.length >= 2 || m.appear.length == 0); + } + + // Stop at the first capture found (if any) + atLeastOneCapture() { + const color = this.turn; + 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 && + V.KeepCaptures(this.getPotentialMovesFrom([i, j])).length > 0 + ) { + return true; + } + } + } + return false; + } + + // En-passant after 2-sq jump + getEpSquare(moveOrSquare) { + if (!moveOrSquare) return undefined; + if (typeof moveOrSquare === "string") { + const square = moveOrSquare; + if (square == "-") return undefined; + // Enemy pawn initial column must be given too: + let res = []; + const epParts = square.split(","); + res.push(V.SquareToCoords(epParts[0])); + res.push(V.ColumnToCoord(epParts[1])); + return res; + } + // Argument is a move: + const move = moveOrSquare; + const [sx, ex, sy, ey] = + [move.start.x, move.end.x, move.start.y, move.end.y]; + if ( + move.vanish.length == 1 && + this.getPiece(sx, sy) == V.PAWN && + Math.abs(sx - ex) == 2 && + Math.abs(sy - ey) == 2 + ) { + return [ + { + x: (ex + sx) / 2, + y: (ey + sy) / 2 + }, + // The arrival column must be remembered, because + // potentially two pawns could be candidates to be captured: + // one on our left, and one on our right. + move.end.y + ]; + } + return undefined; //default + } + + static IsGoodEnpassant(enpassant) { + if (enpassant != "-") { + const epParts = enpassant.split(","); + const epSq = V.SquareToCoords(epParts[0]); + if (isNaN(epSq.x) || isNaN(epSq.y) || !V.OnBoard(epSq)) return false; + const arrCol = V.ColumnToCoord(epParts[1]); + if (isNaN(arrCol) || arrCol < 0 || arrCol >= V.size.y) return false; + } + return true; + } + + getEnpassantFen() { + const L = this.epSquares.length; + if (!this.epSquares[L - 1]) return "-"; //no en-passant + return ( + V.CoordsToSquare(this.epSquares[L - 1][0]) + + "," + + V.CoordToColumn(this.epSquares[L - 1][1]) + ); + } + + getPotentialMovesFrom([x, y], noPostprocess) { + const L = this.lastMoveEnd.length; + if ( + !!this.lastMoveEnd[L-1] && + ( + x != this.lastMoveEnd[L-1].x || + y != this.lastMoveEnd[L-1].y + ) + ) { + // A capture must continue: wrong square + return []; + } + let moves = []; + switch (this.getPiece(x, y)) { + case V.PAWN: + moves = this.getPotentialPawnMoves([x, y]); + break; + case V.ROOK: + moves = this.getPotentialRookMoves([x, y]); + break; + case V.KNIGHT: + moves = this.getPotentialKnightMoves([x, y]); + break; + case V.BISHOP: + moves = this.getPotentialBishopMoves([x, y]); + break; + case V.KING: + moves = this.getPotentialKingMoves([x, y]); + break; + // No queens + } + if (!noPostprocess) { + // Post-process: if capture, + // can another capture be achieved with the same piece? + moves.forEach(m => { + if (m.vanish.length >= 2 || m.appear.length == 0) { + this.play(m); + const moreCaptures = ( + V.KeepCaptures( + this.getPotentialMovesFrom([m.end.x, m.end.y], "noPostprocess") + ) + .length > 0 + ); + this.undo(m); + if (!moreCaptures) m.last = true; + } + else m.last = true; + }); + } + return moves; + } + + // Special pawns movements + getPotentialPawnMoves([x, y]) { + const color = this.turn; + const oppCol = V.GetOppCol(color); + let moves = []; + const [sizeX, sizeY] = [V.size.x, V.size.y]; + const shiftX = color == "w" ? -1 : 1; + const startRank = color == "w" ? sizeX - 2 : 1; + const potentialFinalPieces = + [V.ROOK, V.KNIGHT, V.BISHOP].filter(p => this.captured[color][p] > 0); + const lastRanks = (color == "w" ? [0, 1] : [sizeX - 1, sizeX - 2]); + if (x + shiftX == lastRanks[0] && potentialFinalPieces.length == 0) + // If no captured piece is available, the pawn cannot promote + return []; + + const finalPieces1 = + x + shiftX == lastRanks[0] + ? potentialFinalPieces + : + x + shiftX == lastRanks[1] + ? potentialFinalPieces.concat([V.PAWN]) + : [V.PAWN]; + // One square diagonally + for (let shiftY of [-1, 1]) { + if (this.board[x + shiftX][y + shiftY] == V.EMPTY) { + for (let piece of finalPieces1) { + moves.push( + this.getBasicMove([x, y], [x + shiftX, y + shiftY], { + c: color, + p: piece + }) + ); + } + if ( + V.PawnSpecs.twoSquares && + x == startRank && + y + 2 * shiftY >= 0 && + y + 2 * shiftY < sizeY && + this.board[x + 2 * shiftX][y + 2 * shiftY] == V.EMPTY + ) { + // Two squares jump + moves.push( + this.getBasicMove([x, y], [x + 2 * shiftX, y + 2 * shiftY]) + ); + } + } + } + // Capture + const finalPieces2 = + x + 2 * shiftX == lastRanks[0] + ? potentialFinalPieces + : + x + 2 * shiftX == lastRanks[1] + ? potentialFinalPieces.concat([V.PAWN]) + : [V.PAWN]; + if ( + this.board[x + shiftX][y] != V.EMPTY && + this.canTake([x, y], [x + shiftX, y]) && + V.OnBoard(x + 2 * shiftX, y) && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + const oppPiece = this.getPiece(x + shiftX, y); + for (let piece of finalPieces2) { + let mv = this.getBasicMove( + [x, y], [x + 2 * shiftX, y], { c: color, p: piece }); + mv.vanish.push({ + x: x + shiftX, + y: y, + p: oppPiece, + c: oppCol + }); + moves.push(mv); + } + } + + // En passant + const Lep = this.epSquares.length; + const epSquare = this.epSquares[Lep - 1]; //always at least one element + if ( + !!epSquare && + epSquare[0].x == x + shiftX && + epSquare[0].y == y && + this.board[x + 2 * shiftX][y] == V.EMPTY + ) { + for (let piece of finalPieces2) { + let enpassantMove = + this.getBasicMove( + [x, y], [x + 2 * shiftX, y], { c: color, p: piece}); + enpassantMove.vanish.push({ + x: x, + y: epSquare[1], + p: "p", + c: this.getColor(x, epSquare[1]) + }); + moves.push(enpassantMove); + } + } + + // Add custodian captures: + const steps = V.steps[V.ROOK]; + moves.forEach(m => { + // 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 + ) { + m.vanish.push( + new PiPo({ + x: sq1[0], + y: sq1[1], + c: oppCol, + p: this.getPiece(sq1[0], sq1[1]) + }) + ); + } + } + } + }); + + return moves; + } + + getSlides([x, y], steps, options) { + options = options || {}; + // No captures: + let moves = []; + outerLoop: for (let step of steps) { + let i = x + step[0]; + let j = y + step[1]; + let counter = 1; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + if (!options["doubleStep"] || counter % 2 == 0) + moves.push(this.getBasicMove([x, y], [i, j])); + if (!!options["oneStep"]) continue outerLoop; + i += step[0]; + j += step[1]; + counter++; + } + } + return moves; + } + + // Smasher + getPotentialRookMoves([x, y]) { + let moves = + this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true }) + .concat(this.getSlides([x, y], V.steps[V.BISHOP])); + // Add captures + const oppCol = V.GetOppCol(this.turn); + moves.forEach(m => { + const delta = [m.end.x - m.start.x, m.end.y - m.start.y]; + const step = [ + delta[0] / Math.abs(delta[0]) || 0, + delta[1] / Math.abs(delta[1]) || 0 + ]; + if (step[0] == 0 || step[1] == 0) { + // Rook-like move, candidate for capturing + const [i, j] = [m.end.x + step[0], m.end.y + step[1]]; + if ( + V.OnBoard(i, j) && + this.board[i][j] != V.EMPTY && + this.getColor(i, j) == oppCol + ) { + m.vanish.push({ + x: i, + y: j, + p: this.getPiece(i, j), + c: oppCol + }); + } + } + }); + return moves; + } + + // Leaper + getPotentialKnightMoves([x, y]) { + let moves = + this.getSlides([x, y], V.steps[V.ROOK], { doubleStep: true }) + .concat(this.getSlides([x, y], V.steps[V.BISHOP])); + const oppCol = V.GetOppCol(this.turn); + // Look for double-knight moves (could capture): + for (let step of V.steps[V.KNIGHT]) { + const [i, j] = [x + 2 * step[0], y + 2 * step[1]]; + if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + const [ii, jj] = [x + step[0], y + step[1]]; + if (this.board[ii][jj] == V.EMPTY || this.getColor(ii, jj) == oppCol) { + let mv = this.getBasicMove([x, y], [i, j]); + if (this.board[ii][jj] != V.EMPTY) { + mv.vanish.push({ + x: ii, + y: jj, + c: oppCol, + p: this.getPiece(ii, jj) + }); + } + moves.push(mv); + } + } + } + // Look for an enemy in every orthogonal direction + for (let step of V.steps[V.ROOK]) { + let [i, j] = [x + step[0], y+ step[1]]; + let counter = 1; + while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + i += step[0]; + j += step[1]; + counter++; + } + if ( + V.OnBoard(i, j) && + counter % 2 == 1 && + this.getColor(i, j) == oppCol + ) { + const oppPiece = this.getPiece(i, j); + // Candidate for capture: can I land after? + let [ii, jj] = [i + step[0], j + step[1]]; + counter++; + while (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY) { + if (counter % 2 == 0) { + // Same color: add capture + let mv = this.getBasicMove([x, y], [ii, jj]); + mv.vanish.push({ + x: i, + y: j, + c: oppCol, + p: oppPiece + }); + moves.push(mv); + } + ii += step[0]; + jj += step[1]; + counter++; + } + } + } + return moves; + } + + // Remover + getPotentialBishopMoves([x, y]) { + let moves = this.getSlides([x, y], V.steps[V.BISHOP]); + // Add captures + const oppCol = V.GetOppCol(this.turn); + let captures = []; + for (let step of V.steps[V.ROOK]) { + 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 + ) { + captures.push([i, j]); + } + } + captures.forEach(c => { + moves.push({ + start: { x: x, y: y }, + end: { x: c[0], y: c[1] }, + appear: [], + vanish: captures.map(ct => { + return { + x: ct[0], + y: ct[1], + c: oppCol, + p: this.getPiece(ct[0], ct[1]) + }; + }) + }); + }); + return moves; + } + + getPotentialKingMoves([x, y]) { + let moves = this.getSlides([x, y], V.steps[V.BISHOP], { oneStep: true }); + // Add captures + const oppCol = V.GetOppCol(this.turn); + for (let step of V.steps[V.ROOK]) { + const [i, j] = [x + 2 * step[0], y + 2 * step[1]]; + if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) { + const [ii, jj] = [x + step[0], y + step[1]]; + if (this.board[ii][jj] != V.EMPTY && this.getColor(ii, jj) == oppCol) { + let mv = this.getBasicMove([x, y], [i, j]); + mv.vanish.push({ + x: ii, + y: jj, + c: oppCol, + p: this.getPiece(ii, jj) + }); + moves.push(mv); + } + } + } + return moves; + } + + getPossibleMovesFrom(sq) { + const L = this.lastMoveEnd.length; + if ( + !!this.lastMoveEnd[L-1] && + ( + sq[0] != this.lastMoveEnd[L-1].x || + sq[1] != this.lastMoveEnd[L-1].y + ) + ) { + return []; + } + let moves = this.getPotentialMovesFrom(sq); + const captureMoves = V.KeepCaptures(moves); + if (captureMoves.length > 0) return captureMoves; + if (this.atLeastOneCapture()) return []; + return moves; + } + + getAllValidMoves() { + const moves = this.getAllPotentialMoves(); + const captures = V.KeepCaptures(moves); + if (captures.length > 0) return captures; + return moves; + } + + filterValid(moves) { + // No checks + return moves; + } + + play(move) { + this.epSquares.push(this.getEpSquare(move)); + V.PlayOnBoard(this.board, move); + if (move.vanish.length >= 2) { + // Capture: update this.captured + for (let i=1; i= 2) { + for (let i=1; i 0) { + const mv = moves[randInt(moves.length)]; + mvArray.push(mv); + if (!mv.last) { + this.play(mv); + moves = V.KeepCaptures( + this.getPotentialMovesFrom([mv.end.x, mv.end.y])); + } + else break; + } + for (let i = mvArray.length - 2; i >= 0; i--) this.undo(mvArray[i]); + return (mvArray.length > 1 ? mvArray : mvArray[0]); + } + + getNotation(move) { + const initialSquare = V.CoordsToSquare(move.start); + const finalSquare = V.CoordsToSquare(move.end); + if (move.appear.length == 0) + // Remover captures 'R' + return initialSquare + "R"; + let notation = move.appear[0].p.toUpperCase() + finalSquare; + // Add a capture mark (not describing what is captured...): + if (move.vanish.length >= 2) notation += "X"; + return notation; + } +};