/** * Compute attacks and rays. * * These are low-level functions that can be used to implement chess rules. * * Implementation notes: Sliding attacks are computed using * [Hyperbola Quintessence](https://www.chessprogramming.org/Hyperbola_Quintessence). * Magic Bitboards would deliver slightly faster lookups, but also require * initializing considerably larger attack tables. On the web, initialization * time is important, so the chosen method may strike a better balance. * * @packageDocumentation */ import { SquareSet } from './squareSet.js'; import { squareFile, squareRank } from './util.js'; const computeRange = (square, deltas) => { let range = SquareSet.empty(); for (const delta of deltas) { const sq = square + delta; if (0 <= sq && sq < 64 && Math.abs(squareFile(square) - squareFile(sq)) <= 2) { range = range.with(sq); } } return range; }; const tabulate = (f) => { const table = []; for (let square = 0; square < 64; square++) table[square] = f(square); return table; }; const KING_ATTACKS = tabulate(sq => computeRange(sq, [-9, -8, -7, -1, 1, 7, 8, 9])); const KNIGHT_ATTACKS = tabulate(sq => computeRange(sq, [-17, -15, -10, -6, 6, 10, 15, 17])); const PAWN_ATTACKS = { white: tabulate(sq => computeRange(sq, [7, 9])), black: tabulate(sq => computeRange(sq, [-7, -9])), }; /** * Gets squares attacked or defended by a king on `square`. */ export const kingAttacks = (square) => KING_ATTACKS[square]; /** * Gets squares attacked or defended by a knight on `square`. */ export const knightAttacks = (square) => KNIGHT_ATTACKS[square]; /** * Gets squares attacked or defended by a pawn of the given `color` * on `square`. */ export const pawnAttacks = (color, square) => PAWN_ATTACKS[color][square]; const FILE_RANGE = tabulate(sq => SquareSet.fromFile(squareFile(sq)).without(sq)); const RANK_RANGE = tabulate(sq => SquareSet.fromRank(squareRank(sq)).without(sq)); const DIAG_RANGE = tabulate(sq => { const diag = new SquareSet(134480385, 2151686160); const shift = 8 * (squareRank(sq) - squareFile(sq)); return (shift >= 0 ? diag.shl64(shift) : diag.shr64(-shift)).without(sq); }); const ANTI_DIAG_RANGE = tabulate(sq => { const diag = new SquareSet(270549120, 16909320); const shift = 8 * (squareRank(sq) + squareFile(sq) - 7); return (shift >= 0 ? diag.shl64(shift) : diag.shr64(-shift)).without(sq); }); const hyperbola = (sq, range, occupied) => { const o = range.intersect(occupied); const fwd = o.minus64(SquareSet.fromSquare(sq)); const rev = o.bswap64().minus64(SquareSet.fromSquare(56 ^ sq)); return fwd.xor(rev.bswap64()).intersect(range); }; const rankAttacks = (sq, occupied) => { const range = RANK_RANGE[sq]; const o = range.intersect(occupied); const fwd = o.minus64(SquareSet.fromSquare(sq)); const rev = o.rbit64().minus64(SquareSet.fromSquare(63 - sq)); return fwd.xor(rev.rbit64()).intersect(range); }; /** * Gets squares attacked or defended by a bishop on `square`, given `occupied` * squares. */ export const bishopAttacks = (square, occupied) => { return hyperbola(square, DIAG_RANGE[square], occupied).xor(hyperbola(square, ANTI_DIAG_RANGE[square], occupied)); }; /** * Gets squares attacked or defended by a rook on `square`, given `occupied` * squares. */ export const rookAttacks = (square, occupied) => hyperbola(square, FILE_RANGE[square], occupied).xor(rankAttacks(square, occupied)); /** * Gets squares attacked or defended by a queen on `square`, given `occupied` * squares. */ export const queenAttacks = (square, occupied) => bishopAttacks(square, occupied).xor(rookAttacks(square, occupied)); /** * Gets squares attacked or defended by a `piece` on `square`, given * `occupied` squares. */ export const attacks = (piece, square, occupied) => { switch (piece.role) { case 'pawn': return pawnAttacks(piece.color, square); case 'knight': return knightAttacks(square); case 'bishop': return bishopAttacks(square, occupied); case 'rook': return rookAttacks(square, occupied); case 'queen': return queenAttacks(square, occupied); case 'king': return kingAttacks(square); } }; /** * Gets all squares of the rank, file or diagonal with the two squares * `a` and `b`, or an empty set if they are not aligned. */ export const ray = (a, b) => { const other = SquareSet.fromSquare(b); if (RANK_RANGE[a].intersects(other)) return RANK_RANGE[a].with(a); if (ANTI_DIAG_RANGE[a].intersects(other)) return ANTI_DIAG_RANGE[a].with(a); if (DIAG_RANGE[a].intersects(other)) return DIAG_RANGE[a].with(a); if (FILE_RANGE[a].intersects(other)) return FILE_RANGE[a].with(a); return SquareSet.empty(); }; /** * Gets all squares between `a` and `b` (bounds not included), or an empty set * if they are not on the same rank, file or diagonal. */ export const between = (a, b) => ray(a, b) .intersect(SquareSet.full().shl64(a).xor(SquareSet.full().shl64(b))) .withoutFirst();