diff --git a/src/attacks.ts b/src/attacks.ts index 7cdbd357..585c649f 100644 --- a/src/attacks.ts +++ b/src/attacks.ts @@ -16,6 +16,12 @@ import { SquareSet } from './squareSet.js'; import { BySquare, Color, Piece, Square } from './types.js'; import { squareFile, squareRank } from './util.js'; +/** + * Computes the range of squares that can be reached from a given square by a set of deltas. + * @param {Square} square The starting square. + * @param {number[]} deltas An array of deltas representing the offsets from the starting square. + * @returns {SquareSet} The set of squares that can be reached from the starting square. + */ const computeRange = (square: Square, deltas: number[]): SquareSet => { let range = SquareSet.empty(); for (const delta of deltas) { @@ -27,50 +33,99 @@ const computeRange = (square: Square, deltas: number[]): SquareSet => { return range; }; +/** + * Creates a table of values for each square on the chessboard by applying a given function. + * @template T The type of the values in the table. + * @param {(square: Square) => T} f The function to apply to each square. + * @returns {BySquare} The table of values for each square. + */ const tabulate = (f: (square: Square) => T): BySquare => { 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 = { +/** + * A pre-computed table of king attacks for each square on the chessboard. + * @type {BySquare} + */ +const KING_ATTACKS: BySquare = tabulate(sq => computeRange(sq, [-9, -8, -7, -1, 1, 7, 8, 9])); + +/** + * A pre-computed table of knight attacks for each square on the chessboard. + * @type {BySquare} + */ +const KNIGHT_ATTACKS: BySquare = tabulate(sq => computeRange(sq, [-17, -15, -10, -6, 6, 10, 15, 17])); + +/** + * A pre-computed table of pawn attacks for each square on the chessboard, separated by color. + * @type {{ white: BySquare; black: BySquare }} + */ +const PAWN_ATTACKS: { + white: BySquare; + black: BySquare; +} = { white: tabulate(sq => computeRange(sq, [7, 9])), black: tabulate(sq => computeRange(sq, [-7, -9])), }; /** - * Gets squares attacked or defended by a king on `square`. + * Returns the set of squares attacked by a king on a given square. + * @param {Square} square The square occupied by the king. + * @returns {SquareSet} The set of squares attacked by the king. */ export const kingAttacks = (square: Square): SquareSet => KING_ATTACKS[square]; /** - * Gets squares attacked or defended by a knight on `square`. + * Returns the set of squares attacked by a knight on a given square. + * @param {Square} square The square occupied by the knight. + * @returns {SquareSet} The set of squares attacked by the knight. */ export const knightAttacks = (square: Square): SquareSet => KNIGHT_ATTACKS[square]; /** - * Gets squares attacked or defended by a pawn of the given `color` - * on `square`. + * Returns the set of squares attacked by a pawn of a given color on a given square. + * @param {Color} color The color of the pawn. + * @param {Square} square The square occupied by the pawn. + * @returns {SquareSet} The set of squares attacked by the pawn. */ export const pawnAttacks = (color: Color, square: Square): SquareSet => PAWN_ATTACKS[color][square]; +/** + * A pre-computed table of file ranges for each square on the chessboard. + */ const FILE_RANGE = tabulate(sq => SquareSet.fromFile(squareFile(sq)).without(sq)); + +/** + * A pre-computed table of rank ranges for each square on the chessboard. + */ const RANK_RANGE = tabulate(sq => SquareSet.fromRank(squareRank(sq)).without(sq)); +/** + * A pre-computed table of diagonal ranges for each square on the chessboard. + */ const DIAG_RANGE = tabulate(sq => { const diag = new SquareSet(0x0804_0201, 0x8040_2010); const shift = 8 * (squareRank(sq) - squareFile(sq)); return (shift >= 0 ? diag.shl64(shift) : diag.shr64(-shift)).without(sq); }); +/** + * A pre-computed table of anti-diagonal ranges for each square on the chessboard. + */ const ANTI_DIAG_RANGE = tabulate(sq => { const diag = new SquareSet(0x1020_4080, 0x0102_0408); const shift = 8 * (squareRank(sq) + squareFile(sq) - 7); return (shift >= 0 ? diag.shl64(shift) : diag.shr64(-shift)).without(sq); }); +/** + * Computes the attacks on a given bit position using a hyperbola quintessence algorithm. + * @param {SquareSet} bit The bit position to compute attacks for. + * @param {SquareSet} range The range of squares to consider for attacks. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares that attack the given bit position. + */ const hyperbola = (bit: SquareSet, range: SquareSet, occupied: SquareSet): SquareSet => { let forward = occupied.intersect(range); let reverse = forward.bswap64(); // Assumes no more than 1 bit per rank @@ -79,9 +134,21 @@ const hyperbola = (bit: SquareSet, range: SquareSet, occupied: SquareSet): Squar return forward.xor(reverse.bswap64()).intersect(range); }; +/** + * Computes the file attacks for a given square and occupied squares on the chessboard. + * @param {Square} square The square to compute file attacks for. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares that attack the given square along the file. + */ const fileAttacks = (square: Square, occupied: SquareSet): SquareSet => hyperbola(SquareSet.fromSquare(square), FILE_RANGE[square], occupied); +/** + * Computes the rank attacks for a given square and occupied squares on the chessboard. + * @param {Square} square The square to compute rank attacks for. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares that attack the given square along the rank. + */ const rankAttacks = (square: Square, occupied: SquareSet): SquareSet => { const range = RANK_RANGE[square]; let forward = occupied.intersect(range); @@ -92,8 +159,13 @@ const rankAttacks = (square: Square, occupied: SquareSet): SquareSet => { }; /** - * Gets squares attacked or defended by a bishop on `square`, given `occupied` - * squares. + * Returns squares attacked or defended by a bishop on `square`, given `occupied` squares. + * + * @param {Square} square The bitboard square index where the bishop is located. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares attacked or defended by the bishop. + * @description Uses `occupied` to determine blocked squares and exclude them from the result. + * The hyperbola quintessence algorithm is used to efficiently compute the bishop attacks. */ export const bishopAttacks = (square: Square, occupied: SquareSet): SquareSet => { const bit = SquareSet.fromSquare(square); @@ -101,22 +173,37 @@ export const bishopAttacks = (square: Square, occupied: SquareSet): SquareSet => }; /** - * Gets squares attacked or defended by a rook on `square`, given `occupied` - * squares. + * Returns squares attacked or defended by a rook on `square`, given `occupied` squares. + * + * @param {Square} square The bitboard square index where the rook is located. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares attacked or defended by the rook. + * @description Uses `occupied` to determine blocked squares and exclude them from the result. + * The hyperbola quintessence algorithm is used to efficiently compute the rook attacks. */ export const rookAttacks = (square: Square, occupied: SquareSet): SquareSet => fileAttacks(square, occupied).xor(rankAttacks(square, occupied)); /** - * Gets squares attacked or defended by a queen on `square`, given `occupied` - * squares. + * Returns squares attacked or defended by a queen on `square`, given `occupied` squares. + * + * @param {Square} square The bitboard square index where the queen is located. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares attacked or defended by the queen. + * @description Uses `occupied` to determine blocked squares and exclude them from the result. + * The hyperbola quintessence algorithm is used to efficiently compute the queen attacks. */ export const queenAttacks = (square: Square, occupied: SquareSet): SquareSet => bishopAttacks(square, occupied).xor(rookAttacks(square, occupied)); /** - * Gets squares attacked or defended by a `piece` on `square`, given - * `occupied` squares. + * Returns squares attacked or defended by a `piece` on `square`, given `occupied` squares. + * + * @param {Piece} piece The chess piece object. + * @param {Square} square The bitboard square index where the piece is located. + * @param {SquareSet} occupied The set of occupied squares on the chessboard. + * @returns {SquareSet} The set of squares attacked or defended by the piece. + * @description Uses `occupied` to determine blocked squares and exclude them from the result. */ export const attacks = (piece: Piece, square: Square, occupied: SquareSet): SquareSet => { switch (piece.role) { @@ -136,8 +223,12 @@ export const attacks = (piece: Piece, square: Square, occupied: SquareSet): Squa }; /** - * 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. + * Returns all squares of the rank, file, or diagonal with the two squares `a` and `b`. + * + * @param {Square} a The first bitboard square index. + * @param {Square} b The second bitboard square index. + * @returns {SquareSet} The set of squares aligned with `a` and `b`. + * @description Returns an empty set if `a` and `b` are not on the same rank, file, or diagonal. */ export const ray = (a: Square, b: Square): SquareSet => { const other = SquareSet.fromSquare(b); @@ -149,8 +240,13 @@ export const ray = (a: Square, b: Square): SquareSet => { }; /** - * 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. + * Returns all squares between `a` and `b` (bounds not included). + * Works in all directions and diagonals. + * + * @param {Square} a The first bitboard square index. + * @param {Square} b The second bitboard square index. + * @returns {SquareSet} The set of squares between `a` and `b`. + * @description Returns an empty set if `a` and `b` are not on the same rank, file, or diagonal. */ export const between = (a: Square, b: Square): SquareSet => ray(a, b) diff --git a/src/board.ts b/src/board.ts index 14deafec..79380619 100644 --- a/src/board.ts +++ b/src/board.ts @@ -12,26 +12,41 @@ import { ByColor, ByRole, Color, COLORS, Piece, Role, ROLES, Square } from './ty export class Board implements Iterable<[Square, Piece]>, ByRole, ByColor { /** * All occupied squares. + * @type {SquareSet} */ occupied: SquareSet; + /** - * All squares occupied by pieces known to be promoted. This information is - * relevant in chess variants like Crazyhouse. + * All squares occupied by pieces known to be promoted. + * This information is relevant in chess variants like Crazyhouse. + * @type {SquareSet} */ promoted: SquareSet; + /** @type {SquareSet} */ white: SquareSet; + /** @type {SquareSet} */ black: SquareSet; + /** @type {SquareSet} */ pawn: SquareSet; + /** @type {SquareSet} */ knight: SquareSet; + /** @type {SquareSet} */ bishop: SquareSet; + /** @type {SquareSet} */ rook: SquareSet; + /** @type {SquareSet} */ queen: SquareSet; + /** @type {SquareSet} */ king: SquareSet; private constructor() {} + /** + * Creates a new board with the default starting position for standard chess. + * @returns {Board} The default board. + */ static default(): Board { const board = new Board(); board.reset(); @@ -54,12 +69,19 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo this.king = new SquareSet(0x10, 0x1000_0000); } + /** + * Creates a new empty board. + * @returns {Board} The empty board. + */ static empty(): Board { const board = new Board(); board.clear(); return board; } + /** + * Clears the board by removing all pieces. + */ clear(): void { this.occupied = SquareSet.empty(); this.promoted = SquareSet.empty(); @@ -67,6 +89,10 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo for (const role of ROLES) this[role] = SquareSet.empty(); } + /** + * Creates a clone of the current board. + * @returns {Board} The cloned board. + */ clone(): Board { const board = new Board(); board.occupied = this.occupied; @@ -76,12 +102,22 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo return board; } + /** + * Gets the color of the piece on the given square. + * @param {Square} square The square to check. + * @returns {Color | undefined} The color of the piece on the square, or undefined if the square is empty. + */ getColor(square: Square): Color | undefined { if (this.white.has(square)) return 'white'; if (this.black.has(square)) return 'black'; return; } + /** + * Gets the role of the piece on the given square. + * @param {Square} square The square to check. + * @returns {Role | undefined} The role of the piece on the square, or undefined if the square is empty. + */ getRole(square: Square): Role | undefined { for (const role of ROLES) { if (this[role].has(square)) return role; @@ -89,6 +125,11 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo return; } + /** + * Gets the piece on the given square. + * @param {Square} square The square to check. + * @returns {Piece | undefined} The piece on the square, or undefined if the square is empty. + */ get(square: Square): Piece | undefined { const color = this.getColor(square); if (!color) return; @@ -98,7 +139,9 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo } /** - * Removes and returns the piece from the given `square`, if any. + * Removes and returns the piece from the given square, if any. + * @param {Square} square The square to remove the piece from. + * @returns {Piece | undefined} The removed piece, or undefined if the square was empty. */ take(square: Square): Piece | undefined { const piece = this.get(square); @@ -112,8 +155,10 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo } /** - * Put `piece` onto `square`, potentially replacing an existing piece. - * Returns the existing piece, if any. + * Puts a piece onto the given square, potentially replacing an existing piece. + * @param {Square} square The square to put the piece on. + * @param {Piece} piece The piece to put on the square. + * @returns {Piece | undefined} The replaced piece, or undefined if the square was empty. */ set(square: Square, piece: Piece): Piece | undefined { const old = this.take(square); @@ -124,36 +169,67 @@ export class Board implements Iterable<[Square, Piece]>, ByRole, ByCo return old; } + /** + * Checks if the given square is occupied by a piece. + * @param {Square} square The square to check. + * @returns {boolean} `true` if the square is occupied, `false` otherwise. + */ has(square: Square): boolean { return this.occupied.has(square); } + /** + * Iterates over all occupied squares and their corresponding pieces. + * @yields {[Square, Piece]} The square and piece for each occupied square. + */ *[Symbol.iterator](): Iterator<[Square, Piece]> { for (const square of this.occupied) { yield [square, this.get(square)!]; } } + /** + * Gets the set of squares occupied by pieces of the given color and role. + * @param {Color} color The color of the pieces. + * @param {Role} role The role of the pieces. + * @returns {SquareSet} The set of squares occupied by pieces of the given color and role. + */ pieces(color: Color, role: Role): SquareSet { return this[color].intersect(this[role]); } + /** + * Gets the set of squares occupied by rooks and queens. + * @returns {SquareSet} The set of squares occupied by rooks and queens. + */ rooksAndQueens(): SquareSet { return this.rook.union(this.queen); } + /** + * Gets the set of squares occupied by bishops and queens. + * @returns {SquareSet} The set of squares occupied by bishops and queens. + */ bishopsAndQueens(): SquareSet { return this.bishop.union(this.queen); } /** - * Finds the unique king of the given `color`, if any. + * Finds the unique king of the given color, if any. + * @param {Color} color The color of the king. + * @returns {Square | undefined} The square of the king, or undefined if the king is not on the board. */ kingOf(color: Color): Square | undefined { return this.pieces(color, 'king').singleSquare(); } } +/** + * Checks if two boards are equal. + * @param {Board} left The first board. + * @param {Board} right The second board. + * @returns {boolean} `true` if the boards are equal, `false` otherwise. + */ export const boardEquals = (left: Board, right: Board): boolean => left.white.equals(right.white) && left.promoted.equals(right.promoted) diff --git a/src/chess.ts b/src/chess.ts index 50b3e32e..949db50f 100644 --- a/src/chess.ts +++ b/src/chess.ts @@ -30,6 +30,11 @@ import { } from './types.js'; import { defined, kingCastlesTo, opposite, rookCastlesTo, squareRank } from './util.js'; +/** + * Enum representing illegal setup states. + * @readonly + * @enum {string} + */ export enum IllegalSetup { Empty = 'ERR_EMPTY', OppositeCheck = 'ERR_OPPOSITE_CHECK', @@ -38,8 +43,20 @@ export enum IllegalSetup { Variant = 'ERR_VARIANT', } +/** + * Custom error class for position errors. + * @extends Error + */ export class PositionError extends Error {} +/** + * Calculates the attacking squares for a given square and attacker color. + * @param {Square} square The target square. + * @param {Color} attacker The attacking color. + * @param {Board} board The chess board. + * @param {SquareSet} occupied The occupied squares on the board. + * @returns {SquareSet} The squares from which the target square is attacked. + */ const attacksTo = (square: Square, attacker: Color, board: Board, occupied: SquareSet): SquareSet => board[attacker].intersect( rookAttacks(square, occupied) @@ -50,13 +67,38 @@ const attacksTo = (square: Square, attacker: Color, board: Board, occupied: Squa .union(pawnAttacks(opposite(attacker), square).intersect(board.pawn)), ); +/** + * Represents the castling rights and related information for a chess position. + */ export class Castles { + /** + * The castling rights as a set of squares. + * @type {SquareSet} + */ castlingRights: SquareSet; + + /** + * The rook positions for each color and castling side. + * @type {ByColor>} + */ rook: ByColor>; + + /** + * The castling paths for each color and castling side. + * @type {ByColor>} + */ path: ByColor>; + /** + * Creates a new instance of the Castles class. + * @private + */ private constructor() {} + /** + * Returns the default castling rights and setup. + * @returns {Castles} The default castling rights and setup. + */ static default(): Castles { const castles = new Castles(); castles.castlingRights = SquareSet.corners(); @@ -71,6 +113,10 @@ export class Castles { return castles; } + /** + * Returns an empty castling rights setup. + * @returns {Castles} The empty castling rights setup. + */ static empty(): Castles { const castles = new Castles(); castles.castlingRights = SquareSet.empty(); @@ -85,6 +131,10 @@ export class Castles { return castles; } + /** + * Creates a clone of the Castles instance. + * @returns {Castles} A new instance with the same castling rights and setup. + */ clone(): Castles { const castles = new Castles(); castles.castlingRights = this.castlingRights; @@ -99,6 +149,14 @@ export class Castles { return castles; } + /** + * Adds a castling right for the given color, side, king, and rook positions. + * @param {Color} color The color of the castling side. + * @param {CastlingSide} side The castling side ('a' for queenside, 'h' for kingside). + * @param {Square} king The position of the king. + * @param {Square} rook The position of the rook. + * @private + */ private add(color: Color, side: CastlingSide, king: Square, rook: Square): void { const kingTo = kingCastlesTo(color, side); const rookTo = rookCastlesTo(color, side); @@ -111,6 +169,11 @@ export class Castles { .without(rook); } + /** + * Creates a Castles instance from the given setup. + * @param {Setup} setup The chess position setup. + * @returns {Castles} The Castles instance created from the setup. + */ static fromSetup(setup: Setup): Castles { const castles = Castles.empty(); const rooks = setup.castlingRights.intersect(setup.board.rook); @@ -127,6 +190,10 @@ export class Castles { return castles; } + /** + * Discards the castling rights for the given rook square. + * @param {Square} square The square of the rook to discard. + */ discardRook(square: Square): void { if (this.castlingRights.has(square)) { this.castlingRights = this.castlingRights.without(square); @@ -138,6 +205,10 @@ export class Castles { } } + /** + * Discards the castling rights for the given color. + * @param {Color} color The color to discard the castling rights for. + */ discardColor(color: Color): void { this.castlingRights = this.castlingRights.diff(SquareSet.backrank(color)); this.rook[color].a = undefined; @@ -145,26 +216,100 @@ export class Castles { } } +/** + * Represents the context of a chess position. + * @interface Context + */ export interface Context { + /** + * The position of the king. + * @type {Square | undefined} + */ king: Square | undefined; + + /** + * The set of blocking squares. + * @type {SquareSet} + */ blockers: SquareSet; + + /** + * The set of checking squares. + * @type {SquareSet} + */ checkers: SquareSet; + + /** + * Indicates if the variant has ended. + * @type {boolean} + */ variantEnd: boolean; + + /** + * Indicates if a capture is required. + * @type {boolean} + */ mustCapture: boolean; } +/** + * Abstract base class for chess positions. + * @abstract + */ export abstract class Position { + /** + * The board state of the position. + * @type {Board} + */ board: Board; + + /** + * The pocket pieces (captured pieces) of the position, if any. + * @type {Material | undefined} + */ pockets: Material | undefined; + + /** + * The color of the side to move in the position. + * @type {Color} + */ turn: Color; + + /** + * The castling rights of the position. + * @type {Castles} + */ castles: Castles; + + /** + * The en passant square of the position, if any. + * @type {Square | undefined} + */ epSquare: Square | undefined; + + /** + * The remaining checks count of the position, if applicable. + * @type {RemainingChecks | undefined} + */ remainingChecks: RemainingChecks | undefined; + + /** + * The number of halfmoves since the last pawn advance or capture. + * @type {number} + */ halfmoves: number; + + /** + * The number of fullmoves (each player's turn counts as one fullmove). + * @type {number} + */ fullmoves: number; protected constructor(readonly rules: Rules) {} + /** + * Resets the position to the starting position. + */ reset() { this.board = Board.default(); this.pockets = undefined; @@ -176,6 +321,11 @@ export abstract class Position { this.fullmoves = 1; } + /** + * Sets up the position from the given setup without validation. + * @param {Setup} setup The chess setup. + * @protected + */ protected setupUnchecked(setup: Setup) { this.board = setup.board.clone(); this.board.promoted = SquareSet.empty(); @@ -200,16 +350,33 @@ export abstract class Position { // - hasInsufficientMaterial() // - isStandardMaterial() + /** + * Calculates the attacking squares for the king on the given square by the given color. + * @param {Square} square The square of the king. + * @param {Color} attacker The attacking color. + * @param {SquareSet} occupied The occupied squares on the board. + * @returns {SquareSet} The squares from which the king is attacked. + */ kingAttackers(square: Square, attacker: Color, occupied: SquareSet): SquareSet { return attacksTo(square, attacker, this.board, occupied); } + /** + * Executes a capture at the given square. + * @param {Square} square The square where the capture occurs. + * @param {Piece} captured The captured piece. + * @protected + */ protected playCaptureAt(square: Square, captured: Piece): void { this.halfmoves = 0; if (captured.role === 'rook') this.castles.discardRook(square); if (this.pockets) this.pockets[opposite(captured.color)][captured.promoted ? 'pawn' : captured.role]++; } + /** + * Returns the context of the current position. + * @returns {Context} The position context. + */ ctx(): Context { const variantEnd = this.isVariantEnd(); const king = this.board.kingOf(this.turn); @@ -235,6 +402,10 @@ export abstract class Position { }; } + /** + * Creates a clone of the current position. + * @returns {Position} The cloned position. + */ clone(): Position { const pos = new (this as any).constructor(); pos.board = this.board.clone(); @@ -248,6 +419,11 @@ export abstract class Position { return pos; } + /** + * Validates the current position. + * @returns {Result} The validation result. + * @protected + */ protected validate(): Result { if (this.board.occupied.isEmpty()) return Result.err(new PositionError(IllegalSetup.Empty)); if (this.board.king.size() !== 2) return Result.err(new PositionError(IllegalSetup.Kings)); @@ -267,10 +443,21 @@ export abstract class Position { return Result.ok(undefined); } + /** + * Calculates the possible destination squares for a drop move. + * @param {Context} [_ctx] The optional context for the move generation. + * @returns {SquareSet} The set of possible destination squares for a drop move. + */ dropDests(_ctx?: Context): SquareSet { return SquareSet.empty(); } + /** + * Calculates the possible destination squares for a piece on a given square. + * @param {Square} square The square of the piece. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {SquareSet} The set of possible destination squares. + */ dests(square: Square, ctx?: Context): SquareSet { ctx = ctx || this.ctx(); if (ctx.variantEnd) return SquareSet.empty(); @@ -323,14 +510,28 @@ export abstract class Position { return pseudo; } + /** + * Checks if the variant has ended. + * @returns {boolean} `false` by default. Subclasses can override this method. + */ isVariantEnd(): boolean { return false; } + /** + * Determines the outcome of the variant. + * @param {Context} [_ctx] The optional context for the position. + * @returns {Outcome | undefined} `undefined` by default. Subclasses can override this method. + */ variantOutcome(_ctx?: Context): Outcome | undefined { return; } + /** + * Returns whether the given side has insufficient material to continue the game. + * @param {Color} color the side to check + * @returns {boolean} `true` if the side has insufficient material, `false` otherwise. + */ hasInsufficientMaterial(color: Color): boolean { if (this.board[color].intersect(this.board.pawn.union(this.board.rooksAndQueens())).nonEmpty()) return false; if (this.board[color].intersects(this.board.knight)) { @@ -349,6 +550,10 @@ export abstract class Position { // The following should be identical in all subclasses + /** + * Returns a `Setup` instance representing the current position. + * @returns {Setup} The setup instance. + */ toSetup(): Setup { return { board: this.board.clone(), @@ -362,10 +567,19 @@ export abstract class Position { }; } + /** + * Returns whether both sides have insufficient material to continue the game. + * @returns {boolean} `true` if both sides have insufficient material to continue the game, `false` otherwise. + */ isInsufficientMaterial(): boolean { return COLORS.every(color => this.hasInsufficientMaterial(color)); } + /** + * Checks if there are any possible destination squares for the current player's moves. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {boolean} `true` if there are possible destination squares, `false` otherwise. + */ hasDests(ctx?: Context): boolean { ctx = ctx || this.ctx(); for (const square of this.board[this.turn]) { @@ -373,7 +587,12 @@ export abstract class Position { } return this.dropDests(ctx).nonEmpty(); } - + /** + * Checks if a given move is legal in the current position. + * @param {Move} move The move to check for legality. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {boolean} `true` if the move is legal, `false` otherwise. + */ isLegal(move: Move, ctx?: Context): boolean { if (isDrop(move)) { if (!this.pockets || this.pockets[this.turn][move.role] <= 0) return false; @@ -388,26 +607,50 @@ export abstract class Position { } } + /** + * Checks if the current position is a check. + * @returns {boolean} `true` if the current position is a check, `false` otherwise. + */ isCheck(): boolean { const king = this.board.kingOf(this.turn); return defined(king) && this.kingAttackers(king, opposite(this.turn), this.board.occupied).nonEmpty(); } + /** + * Checks if the current position is an end position. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {boolean} `true` if the current position is an end position, `false` otherwise. + */ isEnd(ctx?: Context): boolean { if (ctx ? ctx.variantEnd : this.isVariantEnd()) return true; return this.isInsufficientMaterial() || !this.hasDests(ctx); } + /** + * Checks if the current position is a checkmate. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {boolean} `true` if the current position is a checkmate, `false` otherwise. + */ isCheckmate(ctx?: Context): boolean { ctx = ctx || this.ctx(); return !ctx.variantEnd && ctx.checkers.nonEmpty() && !this.hasDests(ctx); } + /** + * Checks if the current position is a stalemate. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {boolean} `true` if the current position is a stalemate, `false` otherwise. + */ isStalemate(ctx?: Context): boolean { ctx = ctx || this.ctx(); return !ctx.variantEnd && ctx.checkers.isEmpty() && !this.hasDests(ctx); } + /** + * Determines the outcome of the current position. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {Outcome | undefined} The outcome of the current position, or undefined if the position is not an end position. + */ outcome(ctx?: Context): Outcome | undefined { const variantOutcome = this.variantOutcome(ctx); if (variantOutcome) return variantOutcome; @@ -417,6 +660,11 @@ export abstract class Position { else return; } + /** + * Calculates all possible destination squares for each piece of the current player. + * @param {Context} [ctx] The optional context for the move generation. + * @returns {Map} A map of source squares to their corresponding sets of possible destination squares. + */ allDests(ctx?: Context): Map { ctx = ctx || this.ctx(); const d = new Map(); @@ -427,6 +675,11 @@ export abstract class Position { return d; } + /** + * Plays the given move to the board. + * @param {Move} move A move to be played. + * @returns {void} + */ play(move: Move): void { const turn = this.turn; const epSquare = this.epSquare; @@ -490,23 +743,42 @@ export class Chess extends Position { super('chess'); } + /** + * Create a new chess game with a default setup. + * @returns {Chess} + */ static default(): Chess { const pos = new this(); pos.reset(); return pos; } + /** + * Creates a new chess position from the given setup. + * @param {Setup} setup The chess position setup. + * @returns {Result} The result containing the chess position or an error. + */ static fromSetup(setup: Setup): Result { const pos = new this(); pos.setupUnchecked(setup); return pos.validate().map(_ => pos); } + /** + * Clone the current chess game. + * @returns {Chess} The cloned chess game. + */ clone(): Chess { return super.clone() as Chess; } } +/** + * Returns the square the en passant can be played to from given position and square. + * @param pos {Position} + * @param square {Square} + * @returns {Square | undefined} Any square the en passant can be played to. + */ const validEpSquare = (pos: Position, square: Square | undefined): Square | undefined => { if (!defined(square)) return; const epRank = pos.turn === 'white' ? 5 : 2; @@ -518,6 +790,11 @@ const validEpSquare = (pos: Position, square: Square | undefined): Square | unde return square; }; +/** + * Finds and returns all legal en passant squares in the position. + * @param {Position} pos + * @returns {Square | undefined} + */ const legalEpSquare = (pos: Position): Square | undefined => { if (!defined(pos.epSquare)) return; const ctx = pos.ctx(); @@ -529,6 +806,13 @@ const legalEpSquare = (pos: Position): Square | undefined => { return; }; +/** + * Checks if an en passant capture is possible from the given pawn square. + * @param {Position} pos The chess position. + * @param {Square} pawnFrom The square of the capturing pawn. + * @param {Context} ctx The context for the position. + * @returns {boolean} `true` if an en passant capture is possible, `false` otherwise. + */ const canCaptureEp = (pos: Position, pawnFrom: Square, ctx: Context): boolean => { if (!defined(pos.epSquare)) return false; if (!pawnAttacks(pos.turn, pawnFrom).has(pos.epSquare)) return false; @@ -545,15 +829,30 @@ const canCaptureEp = (pos: Position, pawnFrom: Square, ctx: Context): boolean => .isEmpty(); }; +/** + * Checks various castling conditions and returns a set of squares that can be castled to. + * @param {Position} pos + * @param {CastlingSide} side + * @param {Context} ctx + * @returns {SquareSet} A set of squares that can be castled to. Can be empty. + */ const castlingDest = (pos: Position, side: CastlingSide, ctx: Context): SquareSet => { if (!defined(ctx.king) || ctx.checkers.nonEmpty()) return SquareSet.empty(); const rook = pos.castles.rook[pos.turn][side]; if (!defined(rook)) return SquareSet.empty(); + + // If any square in the castilng path is occupied, return an empty set if (pos.castles.path[pos.turn][side].intersects(pos.board.occupied)) return SquareSet.empty(); + // Find the castling square const kingTo = kingCastlesTo(pos.turn, side); + + // Find the path of the king to the castling square const kingPath = between(ctx.king, kingTo); + + // Remove the king position const occ = pos.board.occupied.without(ctx.king); + for (const sq of kingPath) { if (pos.kingAttackers(sq, opposite(pos.turn), occ).nonEmpty()) return SquareSet.empty(); } @@ -565,6 +864,19 @@ const castlingDest = (pos: Position, side: CastlingSide, ctx: Context): SquareSe return SquareSet.fromSquare(rook); }; +/** + * Calculates the pseudo-legal destination squares for a given piece on a square. + * + * Pseudo-legal destinations refer to the set of squares that a piece can potentially move to, without + * considering the legality of the move in the context of the current position. They include moves that + * may be illegal, such as leaving the king in check. Pseudo-legal moves need to be further filtered to + * determine the actual legal moves in the given position. + * + * @param {Position} pos The chess position. + * @param {Square} square The square of the piece. + * @param {Context} ctx The context for the position. + * @returns {SquareSet} The set of pseudo-legal destination squares. + */ export const pseudoDests = (pos: Position, square: Square, ctx: Context): SquareSet => { if (ctx.variantEnd) return SquareSet.empty(); const piece = pos.board.get(square); @@ -593,6 +905,12 @@ export const pseudoDests = (pos: Position, square: Square, ctx: Context): Square else return pseudo; }; +/** + * Checks if two positions are equal, ignoring the move history. + * @param {Position} left The first position. + * @param {Position} right The second position. + * @returns {boolean} `true` if the positions are equal (ignoring move history), `false` otherwise. + */ export const equalsIgnoreMoves = (left: Position, right: Position): boolean => left.rules === right.rules && boardEquals(left.board, right.board) @@ -603,6 +921,12 @@ export const equalsIgnoreMoves = (left: Position, right: Position): boolean => && ((right.remainingChecks && left.remainingChecks?.equals(right.remainingChecks)) || (!left.remainingChecks && !right.remainingChecks)); +/** + * Determines the castling side for a given move in a chess position. + * @param {Position} pos The chess position. + * @param {Move} move The move to determine the castling side for. + * @returns {CastlingSide | undefined} The castling side ('a' for queenside, 'h' for kingside) or `undefined` if the move is not a castling move. + */ export const castlingSide = (pos: Position, move: Move): CastlingSide | undefined => { if (isDrop(move)) return; const delta = move.to - move.from; @@ -611,6 +935,12 @@ export const castlingSide = (pos: Position, move: Move): CastlingSide | undefine return delta > 0 ? 'h' : 'a'; }; +/** + * Normalizes a move by converting castling moves to their corresponding rook moves. + * @param {Position} pos The chess position. + * @param {Move} move The move to normalize. + * @returns {Move} The normalized move. + */ export const normalizeMove = (pos: Position, move: Move): Move => { const side = castlingSide(pos, move); if (!side) return move; @@ -621,6 +951,12 @@ export const normalizeMove = (pos: Position, move: Move): Move => { }; }; +/** + * Checks if the material on a given side is in a standard configuration. + * @param {Board} board The chessboard. + * @param {Color} color The color of the side to check. + * @returns {boolean} `true` if the material on the side is in a standard configuration, `false` otherwise. + */ export const isStandardMaterialSide = (board: Board, color: Color): boolean => { const promoted = Math.max(board.pieces(color, 'queen').size() - 1, 0) + Math.max(board.pieces(color, 'rook').size() - 2, 0) @@ -630,9 +966,19 @@ export const isStandardMaterialSide = (board: Board, color: Color): boolean => { return board.pieces(color, 'pawn').size() + promoted <= 8; }; +/** + * Checks if the material on both sides is in a standard configuration. + * @param {Chess} pos The chess position. + * @returns {boolean} `true` if the material on both sides is in a standard configuration, `false` otherwise. + */ export const isStandardMaterial = (pos: Chess): boolean => COLORS.every(color => isStandardMaterialSide(pos.board, color)); +/** + * Checks if the current position has an impossible check configuration. + * @param {Position} pos The chess position. + * @returns {boolean} `true` if the position has an impossible check configuration, `false` otherwise. + */ export const isImpossibleCheck = (pos: Position): boolean => { const ourKing = pos.board.kingOf(pos.turn); if (!defined(ourKing)) return false; diff --git a/src/compat.ts b/src/compat.ts index fbc4fcdf..35cfe89a 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -22,6 +22,8 @@ export interface ChessgroundDestsOpts { * Includes both possible representations of castling moves (unless * `chess960` mode is enabled), so that the `rookCastles` option will work * correctly. + * @param {Position} pos + * @param {ChessgroundDestsOpts} [opts] */ export const chessgroundDests = (pos: Position, opts?: ChessgroundDestsOpts): Map => { const result = new Map(); @@ -43,6 +45,11 @@ export const chessgroundDests = (pos: Position, opts?: ChessgroundDestsOpts): Ma return result; }; +/** + * Converts a move to Chessground format. + * @param {Move} move + * @returns {SquareName[]} + */ export const chessgroundMove = (move: Move): SquareName[] => isDrop(move) ? [makeSquare(move.to)] : [makeSquare(move.from), makeSquare(move.to)]; @@ -59,6 +66,11 @@ export const scalachessCharPair = (move: Move): string => : 35 + move.to, ); +/** + * Converts chessops chess variant names to lichess chess rule names + * @param variant + * @returns {Rules} + */ export const lichessRules = ( variant: | 'standard' @@ -88,6 +100,11 @@ export const lichessRules = ( } }; +/** + * Conversts chessops rule name to lichess variant name. + * @param rules + * @returns + */ export const lichessVariant = ( rules: Rules, ): 'standard' | 'antichess' | 'kingOfTheHill' | 'threeCheck' | 'atomic' | 'horde' | 'racingKings' | 'crazyhouse' => { diff --git a/src/fen.ts b/src/fen.ts index b257fc72..8aacc70c 100644 --- a/src/fen.ts +++ b/src/fen.ts @@ -12,6 +12,9 @@ export const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8'; export const EMPTY_EPD = EMPTY_BOARD_FEN + ' w - -'; export const EMPTY_FEN = EMPTY_EPD + ' 0 1'; +/** + * ENUM representing the possible FEN errors + */ export enum InvalidFen { Fen = 'ERR_FEN', Board = 'ERR_BOARD', @@ -42,6 +45,12 @@ const charToPiece = (ch: string): Piece | undefined => { return role && { role, color: ch.toLowerCase() === ch ? 'black' : 'white' }; }; +/** + * TODO: what is a "boardPart"? + * Takes a FEN and produces a Board object representing it + * @param boardPart + * @returns {Result} + */ export const parseBoardFen = (boardPart: string): Result => { const board = Board.empty(); let rank = 7; @@ -72,6 +81,28 @@ export const parseBoardFen = (boardPart: string): Result => { return Result.ok(board); }; +/** + * Parses the pockets part of a FEN (Forsyth-Edwards Notation) string and returns a Material object. + * + * @param {string} pocketPart The pockets part of the FEN string. + * @returns {Result} The parsed Material object if successful, or a FenError if parsing fails. + * + * @throws {FenError} Throws a FenError if the pockets part is invalid. + * + * @example + * const pocketPart = "RNBQKBNRPPPPPPPP"; + * const result = parsePockets(pocketPart); + * if (result.isOk()) { + * const pockets = result.value; + * // Access pockets properties + * const whitePawns = pockets.white.pawn; + * const blackRooks = pockets.black.rook; + * // ... + * } else { + * const error = result.error; + * console.error("Pockets parsing error:", error); + * } + */ export const parsePockets = (pocketPart: string): Result => { if (pocketPart.length > 64) return Result.err(new FenError(InvalidFen.Pockets)); const pockets = Material.empty(); @@ -83,6 +114,13 @@ export const parsePockets = (pocketPart: string): Result => return Result.ok(pockets); }; +/** + * Parses the castling part of a FEN string and returns the corresponding castling rights as a SquareSet. + * + * @param {Board} board The chess board. + * @param {string} castlingPart The castling part of the FEN string. + * @returns {Result} The castling rights as a SquareSet if parsing is successful, or a FenError if parsing fails. + */ export const parseCastlingFen = (board: Board, castlingPart: string): Result => { let castlingRights = SquareSet.empty(); if (castlingPart === '-') return Result.ok(castlingRights); @@ -109,6 +147,32 @@ export const parseCastlingFen = (board: Board, castlingPart: string): Result} The RemainingChecks object if parsing is successful, or a FenError if parsing fails. + * + * @example + * // Provided some arbitrary FEN containing a check + * // Parsing remaining checks in the format "+2+3" + * const result1 = parseRemainingChecks("+2+3"); + * if (result1.isOk()) { + * const remainingChecks = result1.value; // RemainingChecks object with white: 1, black: 0 + * } + * + * @example + * // Provided some arbitrary FEN containing a check + * // Parsing remaining checks in the format "2+3" + * const result2 = parseRemainingChecks("2+3"); + * if (result2.isOk()) { + * const remainingChecks = result2.value; // RemainingChecks object with white: 2, black: 3 + * } + * + * @throws {FenError} Throws a FenError if the remaining checks part is invalid. + */ export const parseRemainingChecks = (part: string): Result => { const parts = part.split('+'); if (parts.length === 3 && parts[0] === '') { @@ -128,6 +192,28 @@ export const parseRemainingChecks = (part: string): Result} The parsed Setup object if successful, or a FenError if parsing fails. + * + * @throws {FenError} Throws a FenError if the FEN string is invalid. + * + * @example + * const fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + * const result = parseFen(fen); + * if (result.isOk()) { + * const setup = result.value; + * // Access setup properties + * const board = setup.board; + * const pockets = setup.pockets; + * // ... + * } else { + * const error = result.error; + * console.error("FEN parsing error:", error); + * } + */ export const parseFen = (fen: string): Result => { const parts = fen.split(/[\s_]+/); const boardPart = parts.shift()!; @@ -217,6 +303,18 @@ export interface FenOpts { epd?: boolean; } +/** + * Parses a string representation of a chess piece and returns the corresponding Piece object. + * + * @param {string} str The string representation of the piece. + * @returns {Piece | undefined} The parsed Piece object, or undefined if the string is invalid. + * + * @example + * const piece1 = parsePiece('R'); // { color: 'white', role: 'rook', promoted: false } + * const piece2 = parsePiece('n~'); // { color: 'black', role: 'knight', promoted: true } + * const piece3 = parsePiece(''); // undefined + * const piece4 = parsePiece('Qx'); // undefined + */ export const parsePiece = (str: string): Piece | undefined => { if (!str) return; const piece = charToPiece(str[0]); @@ -226,6 +324,19 @@ export const parsePiece = (str: string): Piece | undefined => { return piece; }; +/** + * Converts a Piece object to its string representation. + * + * @param {Piece} piece The Piece object to convert. + * @returns {string} The string representation of the piece. + * + * @example + * const piece1 = { color: 'white', role: 'rook', promoted: false }; + * const str1 = makePiece(piece1); // 'R' + * + * const piece2 = { color: 'black', role: 'knight', promoted: true }; + * const str2 = makePiece(piece2); // 'n~' + */ export const makePiece = (piece: Piece): string => { let r = roleToChar(piece.role); if (piece.color === 'white') r = r.toUpperCase(); @@ -233,6 +344,12 @@ export const makePiece = (piece: Piece): string => { return r; }; +/** + * Converts a Board object to its FEN (Forsyth-Edwards Notation) string representation. + * + * @param {Board} board The Board object to convert. + * @returns {string} The FEN string representation of the board. + */ export const makeBoardFen = (board: Board): string => { let fen = ''; let empty = 0; @@ -261,12 +378,31 @@ export const makeBoardFen = (board: Board): string => { return fen; }; +/** + * Converts a MaterialSide object to its string representation. + * + * @param {MaterialSide} material The MaterialSide object to convert. + * @returns {string} The string representation of the material. + */ export const makePocket = (material: MaterialSide): string => ROLES.map(role => roleToChar(role).repeat(material[role])).join(''); +/** + * Converts a Material object to its string representation. + * + * @param {Material} pocket The Material object to convert. + * @returns {string} The string representation of the pocket. + */ export const makePockets = (pocket: Material): string => makePocket(pocket.white).toUpperCase() + makePocket(pocket.black); +/** + * Converts the castling rights of a board to its FEN string representation. + * + * @param {Board} board The Board object. + * @param {SquareSet} castlingRights The castling rights as a SquareSet. + * @returns {string} The FEN string representation of the castling rights. + */ export const makeCastlingFen = (board: Board, castlingRights: SquareSet): string => { let fen = ''; for (const color of COLORS) { @@ -288,8 +424,21 @@ export const makeCastlingFen = (board: Board, castlingRights: SquareSet): string return fen || '-'; }; +/** + * Converts a RemainingChecks object to its string representation. + * + * @param {RemainingChecks} checks The RemainingChecks object to convert. + * @returns {string} The string representation of the remaining checks. + */ export const makeRemainingChecks = (checks: RemainingChecks): string => `${checks.white}+${checks.black}`; +/** + * Converts a Setup object to its FEN string representation. + * + * @param {Setup} setup The Setup object to convert. + * @param {FenOpts} [opts] Optional FEN formatting options. + * @returns {string} The FEN string representation of the setup. + */ export const makeFen = (setup: Setup, opts?: FenOpts): string => [ makeBoardFen(setup.board) + (setup.pockets ? `[${makePockets(setup.pockets)}]` : ''), diff --git a/src/san.ts b/src/san.ts index 6ebb8e2c..1e3cc3af 100644 --- a/src/san.ts +++ b/src/san.ts @@ -4,6 +4,14 @@ import { SquareSet } from './squareSet.js'; import { CastlingSide, FILE_NAMES, isDrop, Move, RANK_NAMES, SquareName } from './types.js'; import { charToRole, defined, makeSquare, opposite, parseSquare, roleToChar, squareFile, squareRank } from './util.js'; +/** + * Generates the SAN (Standard Algebraic Notation) representation of a move + * in the given position without the move suffix (#, +). + * + * @param {Position} pos The chess position. + * @param {Move} move The move to generate the SAN for. + * @returns {string} The SAN representation of the move. + */ const makeSanWithoutSuffix = (pos: Position, move: Move): string => { let san = ''; if (isDrop(move)) { @@ -52,6 +60,14 @@ const makeSanWithoutSuffix = (pos: Position, move: Move): string => { return san; }; +/** + * Generates the SAN (Standard Algebraic Notation) representation of a move + * in the given position and plays the move on the position. + * + * @param {Position} pos The chess position. + * @param {Move} move The move to generate the SAN for and play. + * @returns {string} The SAN representation of the move with the move suffix. + */ export const makeSanAndPlay = (pos: Position, move: Move): string => { const san = makeSanWithoutSuffix(pos, move); pos.play(move); @@ -60,6 +76,14 @@ export const makeSanAndPlay = (pos: Position, move: Move): string => { return san; }; +/** + * Generates the SAN (Standard Algebraic Notation) representation of a variation + * (sequence of moves) in the given position. + * + * @param {Position} pos The starting position of the variation. + * @param {Move[]} variation The sequence of moves in the variation. + * @returns {string} The SAN representation of the variation. + */ export const makeSanVariation = (pos: Position, variation: Move[]): string => { pos = pos.clone(); const line = []; @@ -77,8 +101,24 @@ export const makeSanVariation = (pos: Position, variation: Move[]): string => { return line.join(''); }; +/** + * Generates the SAN (Standard Algebraic Notation) representation of a move + * in the given position without modifying the position. + * + * @param {Position} pos The chess position. + * @param {Move} move The move to generate the SAN for. + * @returns {string} The SAN representation of the move. + */ export const makeSan = (pos: Position, move: Move): string => makeSanAndPlay(pos.clone(), move); +/** + * Parses a SAN (Standard Algebraic Notation) string and returns the corresponding move + * in the given position. + * + * @param {Position} pos The chess position. + * @param {string} san The SAN string to parse. + * @returns {Move | undefined} The parsed move, or undefined if the SAN is invalid or ambiguous. + */ export const parseSan = (pos: Position, san: string): Move | undefined => { const ctx = pos.ctx(); diff --git a/src/setup.ts b/src/setup.ts index b54b727e..cb7c75f2 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -2,159 +2,371 @@ import { Board, boardEquals } from './board.js'; import { SquareSet } from './squareSet.js'; import { ByColor, ByRole, Color, Role, ROLES, Square } from './types.js'; +/** + * Represents the material configuration for one side (color) in a chess position. + * @implements {ByRole} + * @property {number} pawn The number of pawns on the side. + * @property {number} knight The number of knights on the side. + * @property {number} bishop The number of bishops on the side. + * @property {number} rook The number of rooks on the side. + * @property {number} queen The number of queens on the side. + * @property {number} king The number of kings on the side. + */ export class MaterialSide implements ByRole { + /** + * The number of pawns. + */ pawn: number; + + /** + * The number of knights. + */ knight: number; + + /** + * The number of bishops. + */ bishop: number; + + /** + * The number of rooks. + */ rook: number; + + /** + * The number of queens. + */ queen: number; + + /** + * The number of kings. + */ king: number; private constructor() {} + /** + * Creates an empty MaterialSide instance. + * @returns {MaterialSide} The empty MaterialSide instance. + */ static empty(): MaterialSide { const m = new MaterialSide(); for (const role of ROLES) m[role] = 0; return m; } + /** + * Creates a MaterialSide instance from a Board for a specific color. + * @param {Board} board The Board to create the MaterialSide from. + * @param {Color} color The color to create the MaterialSide for. + * @returns {MaterialSide} The MaterialSide instance derived from the Board. + */ static fromBoard(board: Board, color: Color): MaterialSide { const m = new MaterialSide(); for (const role of ROLES) m[role] = board.pieces(color, role).size(); return m; } + /** + * Creates a clone of the MaterialSide instance. + * @returns {MaterialSide} The cloned MaterialSide instance. + */ clone(): MaterialSide { const m = new MaterialSide(); for (const role of ROLES) m[role] = this[role]; return m; } + /** + * Checks if the MaterialSide instance is equal to another MaterialSide instance. + * @param {MaterialSide} other The other MaterialSide instance to compare. + * @returns {boolean} `true` if the MaterialSide instances are equal, `false` otherwise. + */ equals(other: MaterialSide): boolean { return ROLES.every(role => this[role] === other[role]); } + /** + * Adds another MaterialSide instance to the current MaterialSide instance. + * @param {MaterialSide} other The MaterialSide instance to add. + * @returns {MaterialSide} A new MaterialSide instance representing the sum. + */ add(other: MaterialSide): MaterialSide { const m = new MaterialSide(); for (const role of ROLES) m[role] = this[role] + other[role]; return m; } + /** + * Subtracts another MaterialSide instance from the current MaterialSide instance. + * @param {MaterialSide} other The MaterialSide instance to subtract. + * @returns {MaterialSide} A new MaterialSide instance representing the difference. + */ subtract(other: MaterialSide): MaterialSide { const m = new MaterialSide(); for (const role of ROLES) m[role] = this[role] - other[role]; return m; } + /** + * Checks if the MaterialSide is not empty (has pieces). + * @returns {boolean} `true` if the MaterialSide is not empty, `false` otherwise. + */ nonEmpty(): boolean { return ROLES.some(role => this[role] > 0); } + /** + * Checks if the MaterialSide is empty (no pieces). + * @returns {boolean} `true` if the MaterialSide is empty, `false` otherwise. + */ isEmpty(): boolean { return !this.nonEmpty(); } + /** + * Checks if the MaterialSide has pawns. + * @returns {boolean} `true` if the MaterialSide has pawns, `false` otherwise. + */ hasPawns(): boolean { return this.pawn > 0; } + /** + * Checks if the MaterialSide has non-pawn pieces. + * @returns {boolean} `true` if the MaterialSide has non-pawn pieces, `false` otherwise. + */ hasNonPawns(): boolean { return this.knight > 0 || this.bishop > 0 || this.rook > 0 || this.queen > 0 || this.king > 0; } + /** + * Calculates the total size of the MaterialSide (number of pieces). + * @returns {number} The total size of the MaterialSide. + */ size(): number { return this.pawn + this.knight + this.bishop + this.rook + this.queen + this.king; } } +/** + * Represents the material configuration of a chess position. + * @implements {ByColor} + * @property {MaterialSide} white The material configuration for white. + * @property {MaterialSide} black The material configuration for black. + */ export class Material implements ByColor { + /** + * Creates a new Material instance. + * @param {MaterialSide} white The material configuration for white. + * @param {MaterialSide} black The material configuration for black. + */ constructor( public white: MaterialSide, public black: MaterialSide, ) {} + /** + * Creates an empty Material instance. + * @returns {Material} The empty Material instance. + */ static empty(): Material { return new Material(MaterialSide.empty(), MaterialSide.empty()); } + /** + * Creates a Material instance from a Board. + * @param {Board} board The Board to create the Material from. + * @returns {Material} The Material instance derived from the Board. + */ static fromBoard(board: Board): Material { return new Material(MaterialSide.fromBoard(board, 'white'), MaterialSide.fromBoard(board, 'black')); } + /** + * Creates a clone of the Material instance. + * @returns {Material} The cloned Material instance. + */ clone(): Material { return new Material(this.white.clone(), this.black.clone()); } + /** + * Checks if the Material instance is equal to another Material instance. + * @param {Material} other The other Material instance to compare. + * @returns {boolean} `true` if the Material instances are equal, `false` otherwise. + */ equals(other: Material): boolean { return this.white.equals(other.white) && this.black.equals(other.black); } + /** + * Adds another Material instance to the current Material instance. + * @param {Material} other The Material instance to add. + * @returns {Material} A new Material instance representing the sum. + */ add(other: Material): Material { return new Material(this.white.add(other.white), this.black.add(other.black)); } + /** + * Subtracts another Material instance from the current Material instance. + * @param {Material} other The Material instance to subtract. + * @returns {Material} A new Material instance representing the difference. + */ subtract(other: Material): Material { return new Material(this.white.subtract(other.white), this.black.subtract(other.black)); } + /** + * Counts the number of pieces of a specific role. + * @param {Role} role The role to count. + * @returns {number} The count of pieces with the specified role. + */ count(role: Role): number { return this.white[role] + this.black[role]; } + /** + * Calculates the total size of the Material (number of pieces). + * @returns {number} The total size of the Material. + */ size(): number { return this.white.size() + this.black.size(); } + /** + * Checks if the Material is empty (no pieces). + * @returns {boolean} `true` if the Material is empty, `false` otherwise. + */ isEmpty(): boolean { return this.white.isEmpty() && this.black.isEmpty(); } + /** + * Checks if the Material is not empty (has pieces). + * @returns {boolean} `true` if the Material is not empty, `false` otherwise. + */ nonEmpty(): boolean { return !this.isEmpty(); } + /** + * Checks if the Material has pawns. + * @returns {boolean} `true` if the Material has pawns, `false` otherwise. + */ hasPawns(): boolean { return this.white.hasPawns() || this.black.hasPawns(); } + /** + * Checks if the Material has non-pawn pieces. + * @returns {boolean} `true` if the Material has non-pawn pieces, `false` otherwise. + */ hasNonPawns(): boolean { return this.white.hasNonPawns() || this.black.hasNonPawns(); } } +/** + * Represents the remaining checks count for each color. + * @implements {ByColor} + */ export class RemainingChecks implements ByColor { + /** + * Creates a new instance of the RemainingChecks class. + * @param {number} white The remaining checks count for the white player. + * @param {number} black The remaining checks count for the black player. + */ constructor( public white: number, public black: number, ) {} + /** + * Returns the default remaining checks count for each color. + * @returns {RemainingChecks} The default remaining checks count. + */ static default(): RemainingChecks { return new RemainingChecks(3, 3); } + /** + * Creates a clone of the RemainingChecks instance. + * @returns {RemainingChecks} A new instance with the same remaining checks count. + */ clone(): RemainingChecks { return new RemainingChecks(this.white, this.black); } + /** + * Checks if the RemainingChecks instance is equal to another instance. + * @param {RemainingChecks} other The other RemainingChecks instance to compare. + * @returns {boolean} `true` if the instances are equal, `false` otherwise. + */ equals(other: RemainingChecks): boolean { return this.white === other.white && this.black === other.black; } } /** - * A not necessarily legal chess or chess variant position. + * Represents the setup of a chess position. + * @interface Setup + * @property {Board} board The chess board. + * @property {Material | undefined} pockets The material in the pockets. + * @property {Color} turn The color of the side to move. + * @property {SquareSet} castlingRights The castling rights. + * @property {Square | undefined} epSquare A square where en passant is possible. + * @property {RemainingChecks | undefined} remainingChecks The remaining checks. + * @property {number} halfmoves The number of halfmoves since the last pawn advance or capture. + * @property {number} fullmoves The number of fullmoves. */ export interface Setup { + /** + * The chess board. + */ board: Board; + /** + * The material in the pockets. + */ pockets: Material | undefined; + /** + * The color of the side to move. + */ turn: Color; + /** + * The castling rights. + */ castlingRights: SquareSet; + /** + * A square where en passant is possible. + */ epSquare: Square | undefined; + + /** + * The remaining checks. Used in multi-check variants. + */ remainingChecks: RemainingChecks | undefined; + + /** + * The number of halfmoves since the last pawn advance or capture. + * + * A halfmove is when a side makes a move. + */ halfmoves: number; + + /** + * The number of fullmoves. + * + * A fullmove is when both sides make a move. + */ fullmoves: number; } +/** + * Creates a default setup for a standard chess position. + * @returns {Setup} The default setup. + */ export const defaultSetup = (): Setup => ({ board: Board.default(), pockets: undefined, @@ -166,6 +378,11 @@ export const defaultSetup = (): Setup => ({ fullmoves: 1, }); +/** + * Creates a clone of a given setup. + * @param {Setup} setup The setup to clone. + * @returns {Setup} The cloned setup. + */ export const setupClone = (setup: Setup): Setup => ({ board: setup.board.clone(), pockets: setup.pockets?.clone(), @@ -177,6 +394,12 @@ export const setupClone = (setup: Setup): Setup => ({ fullmoves: setup.fullmoves, }); +/** + * Checks if two setups are equal. + * @param {Setup} left The first setup. + * @param {Setup} right The second setup. + * @returns {boolean} `true` if the setups are equal, `false` otherwise. + */ export const setupEquals = (left: Setup, right: Setup): boolean => boardEquals(left.board, right.board) && ((right.pockets && left.pockets?.equals(right.pockets)) || (!left.pockets && !right.pockets)) diff --git a/src/squareSet.ts b/src/squareSet.ts index 7d1cc110..f1e444f8 100644 --- a/src/squareSet.ts +++ b/src/squareSet.ts @@ -30,86 +30,220 @@ export class SquareSet implements Iterable { this.hi = hi | 0; } + /** + * Returns a square set containing the given square. + * @param square + * @returns + */ static fromSquare(square: Square): SquareSet { return square >= 32 ? new SquareSet(0, 1 << (square - 32)) : new SquareSet(1 << square, 0); } + /** + * Returns a square set containing all squares on the given rank. + * @param rank A rank number (0-7) + * @returns {SquareSet} + */ static fromRank(rank: number): SquareSet { return new SquareSet(0xff, 0).shl64(8 * rank); } + /** + * Returns a square set containing all squares on the given file. + * @param file A file number (0-7) + * @returns {SquareSet} + */ static fromFile(file: number): SquareSet { return new SquareSet(0x0101_0101 << file, 0x0101_0101 << file); } + /** + * Returns an empty square set. + * @returns {SquareSet} + */ static empty(): SquareSet { return new SquareSet(0, 0); } + /** + * Returns a square set containing all squares. + * @returns {SquareSet} + */ static full(): SquareSet { return new SquareSet(0xffff_ffff, 0xffff_ffff); } + /** + * Returns a square set containing all corner squares. + * @returns {SquareSet} + */ static corners(): SquareSet { return new SquareSet(0x81, 0x8100_0000); } + /** + * TODO: Not sure about this one. + * Returns a square set containing the four center squares. + * @returns {SquareSet} + */ static center(): SquareSet { return new SquareSet(0x1800_0000, 0x18); } + /** + * Returns a square set containing all squares on the back ranks of both sides. + * @returns {SquareSet} + */ static backranks(): SquareSet { return new SquareSet(0xff, 0xff00_0000); } + /** + * Returns a square set containing all squares on the back rank of the given color. + * @param color The color of the back rank + * @returns {SquareSet} + */ static backrank(color: Color): SquareSet { return color === 'white' ? new SquareSet(0xff, 0) : new SquareSet(0, 0xff00_0000); } + /** + * Returns a square set containing all dark squares. + * @returns {SquareSet} + */ static lightSquares(): SquareSet { return new SquareSet(0x55aa_55aa, 0x55aa_55aa); } + /** + * Returns a square set containing all light squares. + * @returns {SquareSet} + */ static darkSquares(): SquareSet { return new SquareSet(0xaa55_aa55, 0xaa55_aa55); } + /** + * Returns the complement of the current SquareSet. + * + * The complement of a SquareSet is a new SquareSet that contains all the squares + * that are not present in the original set. + * + * @returns {SquareSet} A new SquareSet representing the complement of the current set. + */ complement(): SquareSet { return new SquareSet(~this.lo, ~this.hi); } + /** + * Performs a bitwise XOR operation between the current SquareSet and another SquareSet. + * + * The XOR operation returns a new SquareSet that contains the squares that are present + * in either the current set or the other set, but not both. + * + * @param {SquareSet} other The SquareSet to perform the XOR operation with. + * @returns {SquareSet} A new SquareSet representing the result of the XOR operation. + */ xor(other: SquareSet): SquareSet { return new SquareSet(this.lo ^ other.lo, this.hi ^ other.hi); } + /** + * Performs a bitwise OR operation between the current SquareSet and another SquareSet. + * + * The OR operation returns a new SquareSet that contains the squares that are present + * in either the current set or the other set, or both. + * + * @param {SquareSet} other The SquareSet to perform the OR operation with. + * @returns {SquareSet} A new SquareSet representing the result of the OR operation. + */ union(other: SquareSet): SquareSet { return new SquareSet(this.lo | other.lo, this.hi | other.hi); } + /** + * Performs a bitwise AND operation between the current SquareSet and another SquareSet. + * + * The AND operation returns a new SquareSet that contains the squares that are present + * in both the current set and the other set. + * + * @param {SquareSet} other The SquareSet to perform the AND operation with. + * @returns {SquareSet} A new SquareSet representing the result of the AND operation. + */ intersect(other: SquareSet): SquareSet { return new SquareSet(this.lo & other.lo, this.hi & other.hi); } + /** + * Performs a bitwise AND NOT operation between the current SquareSet and another SquareSet. + * + * The AND NOT operation returns a new SquareSet that contains the squares that are present + * in the current set, but not in the other set. + * + * @param {SquareSet} other The SquareSet to perform the AND NOT operation with. + * @returns {SquareSet} A new SquareSet representing the result of the AND NOT operation. + */ diff(other: SquareSet): SquareSet { return new SquareSet(this.lo & ~other.lo, this.hi & ~other.hi); } + /** + * Checks if the current SquareSet intersects with another SquareSet. + * + * Two SquareSets are considered to intersect if they have at least one square in common. + * + * @param {SquareSet} other The SquareSet to check for intersection. + * @returns {boolean} `true` if the current set intersects with the other set, `false` otherwise. + */ intersects(other: SquareSet): boolean { return this.intersect(other).nonEmpty(); } + /** + * Checks if the current SquareSet is disjoint with another SquareSet. + * + * Two SquareSets are considered to be disjoint if they have no squares in common. + * + * @param {SquareSet} other The SquareSet to check for disjointness. + * @returns {boolean} `true` if the current set is disjoint with the other set, `false` otherwise. + */ isDisjoint(other: SquareSet): boolean { return this.intersect(other).isEmpty(); } + /** + * Checks if the current SquareSet is a superset of another SquareSet. + * + * A SquareSet is a superset of another SquareSet if every square in the other set is also present in the current set. + * + * @param {SquareSet} other The SquareSet to check for supersetness. + * @returns {boolean} `true` if the current set is a superset of the other set, `false` otherwise. + */ supersetOf(other: SquareSet): boolean { return other.diff(this).isEmpty(); } + /** + * Checks if the current SquareSet is a subset of another SquareSet. + * + * A SquareSet is a subset of another SquareSet if every square in the current set is also present in the other set. + * + * @param {SquareSet} other The SquareSet to check for subsetness. + * @returns {boolean} `true` if the current set is a subset of the other set, `false` otherwise. + */ subsetOf(other: SquareSet): boolean { return this.diff(other).isEmpty(); } + /** + * Performs a logical right shift operation on the SquareSet by the specified number of positions. + * + * The right shift operation shifts the bits of the SquareSet towards the right by the given + * number of positions. The vacated bits on the left side are filled with zeros. + * + * @param {number} shift The number of positions to shift the bits to the right. + * @returns {SquareSet} A new SquareSet representing the result of the right shift operation. + */ shr64(shift: number): SquareSet { if (shift >= 64) return SquareSet.empty(); if (shift >= 32) return new SquareSet(this.hi >>> (shift - 32), 0); @@ -117,6 +251,15 @@ export class SquareSet implements Iterable { return this; } + /** + * Performs a logical left shift operation on the SquareSet by the specified number of positions. + * + * The left shift operation shifts the bits of the SquareSet towards the left by the given + * number of positions. The vacated bits on the right side are filled with zeros. + * + * @param {number} shift The number of positions to shift the bits to the left. + * @returns {SquareSet} A new SquareSet representing the result of the left shift operation. + */ shl64(shift: number): SquareSet { if (shift >= 64) return SquareSet.empty(); if (shift >= 32) return new SquareSet(0, this.lo << (shift - 32)); @@ -124,83 +267,176 @@ export class SquareSet implements Iterable { return this; } + /** + * Swaps the bytes of the SquareSet in a 64-bit manner. + * + * @returns {SquareSet} A new SquareSet with the bytes swapped. + */ bswap64(): SquareSet { return new SquareSet(bswap32(this.hi), bswap32(this.lo)); } + /** + * Reverses the bits of the SquareSet in a 64-bit manner. + * + * @returns {SquareSet} A new SquareSet with the bits reversed. + */ rbit64(): SquareSet { return new SquareSet(rbit32(this.hi), rbit32(this.lo)); } + /** + * Subtracts another SquareSet from the current SquareSet in a 64-bit manner. + * + * @param {SquareSet} other The SquareSet to subtract. + * @returns {SquareSet} A new SquareSet representing the result of the subtraction. + */ minus64(other: SquareSet): SquareSet { const lo = this.lo - other.lo; const c = ((lo & other.lo & 1) + (other.lo >>> 1) + (lo >>> 1)) >>> 31; return new SquareSet(lo, this.hi - (other.hi + c)); } + /** + * Checks if the current SquareSet is equal to another SquareSet. + * + * @param {SquareSet} other The SquareSet to compare with. + * @returns {boolean} `true` if the SquareSets are equal, `false` otherwise. + */ equals(other: SquareSet): boolean { return this.lo === other.lo && this.hi === other.hi; } + /** + * Returns the number of squares in the SquareSet. + * + * @returns {number} The count of squares in the SquareSet. + */ size(): number { return popcnt32(this.lo) + popcnt32(this.hi); } + /** + * Checks if the SquareSet is empty. + * + * @returns {boolean} `true` if the SquareSet is empty, `false` otherwise. + */ isEmpty(): boolean { return this.lo === 0 && this.hi === 0; } + /** + * Checks if the SquareSet is not empty. + * + * @returns {boolean} `true` if the SquareSet is not empty, `false` otherwise. + */ nonEmpty(): boolean { return this.lo !== 0 || this.hi !== 0; } + /** + * Checks if the SquareSet contains a specific square. + * + * @param {Square} square The square to check for presence. + * @returns {boolean} `true` if the SquareSet contains the square, `false` otherwise. + */ has(square: Square): boolean { return (square >= 32 ? this.hi & (1 << (square - 32)) : this.lo & (1 << square)) !== 0; } + /** + * Sets or unsets a square in the SquareSet. + * + * @param {Square} square The square to set or unset. + * @param {boolean} on `true` to set the square, `false` to unset it. + * @returns {SquareSet} A new SquareSet with the square set or unset. + */ set(square: Square, on: boolean): SquareSet { return on ? this.with(square) : this.without(square); } + /** + * Adds a square to the SquareSet. + * + * @param {Square} square The square to add. + * @returns {SquareSet} A new SquareSet with the square added. + */ with(square: Square): SquareSet { return square >= 32 ? new SquareSet(this.lo, this.hi | (1 << (square - 32))) : new SquareSet(this.lo | (1 << square), this.hi); } + /** + * Removes a square from the SquareSet. + * + * @param {Square} square The square to remove. + * @returns {SquareSet} A new SquareSet with the square removed. + */ without(square: Square): SquareSet { return square >= 32 ? new SquareSet(this.lo, this.hi & ~(1 << (square - 32))) : new SquareSet(this.lo & ~(1 << square), this.hi); } + /** + * Toggles the presence of a square in the SquareSet. + * + * @param {Square} square The square to toggle. + * @returns {SquareSet} A new SquareSet with the square toggled. + */ toggle(square: Square): SquareSet { return square >= 32 ? new SquareSet(this.lo, this.hi ^ (1 << (square - 32))) : new SquareSet(this.lo ^ (1 << square), this.hi); } + /** + * Returns the last square in the SquareSet. + * + * @returns {Square | undefined} The last square in the SquareSet, or undefined if the set is empty. + */ last(): Square | undefined { if (this.hi !== 0) return 63 - Math.clz32(this.hi); if (this.lo !== 0) return 31 - Math.clz32(this.lo); return; } + /** + * Returns the first square in the SquareSet. + * + * @returns {Square | undefined} The first square in the SquareSet, or undefined if the set is empty. + */ first(): Square | undefined { if (this.lo !== 0) return 31 - Math.clz32(this.lo & -this.lo); if (this.hi !== 0) return 63 - Math.clz32(this.hi & -this.hi); return; } + /** + * Returns a new SquareSet with the first square removed. + * + * @returns {SquareSet} A new SquareSet with the first square removed. + */ withoutFirst(): SquareSet { if (this.lo !== 0) return new SquareSet(this.lo & (this.lo - 1), this.hi); return new SquareSet(0, this.hi & (this.hi - 1)); } + /** + * Checks if the SquareSet contains more than one square. + * + * @returns {boolean} `true` if the SquareSet contains more than one square, `false` otherwise. + */ moreThanOne(): boolean { return (this.hi !== 0 && this.lo !== 0) || (this.lo & (this.lo - 1)) !== 0 || (this.hi & (this.hi - 1)) !== 0; } + /** + * Returns the single square in the SquareSet if it contains only one square. + * + * @returns {Square | undefined} The single square in the SquareSet, or undefined if the set is empty or contains more than one square. + */ singleSquare(): Square | undefined { return this.moreThanOne() ? undefined : this.last(); } diff --git a/src/transform.ts b/src/transform.ts index ea8ffd99..45a7e82f 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -4,8 +4,20 @@ import { SquareSet } from './squareSet.js'; import { COLORS, ROLES } from './types.js'; import { defined } from './util.js'; +/** + * Flips a SquareSet vertically. + * + * @param {SquareSet} s The SquareSet to flip. + * @returns {SquareSet} The flipped SquareSet. + */ export const flipVertical = (s: SquareSet): SquareSet => s.bswap64(); +/** + * Flips a SquareSet horizontally. + * + * @param {SquareSet} s The SquareSet to flip. + * @returns {SquareSet} The flipped SquareSet. + */ export const flipHorizontal = (s: SquareSet): SquareSet => { const k1 = new SquareSet(0x55555555, 0x55555555); const k2 = new SquareSet(0x33333333, 0x33333333); @@ -16,6 +28,12 @@ export const flipHorizontal = (s: SquareSet): SquareSet => { return s; }; +/** + * Flips a SquareSet diagonally. + * + * @param {SquareSet} s The SquareSet to flip. + * @returns {SquareSet} The flipped SquareSet. + */ export const flipDiagonal = (s: SquareSet): SquareSet => { let t = s.xor(s.shl64(28)).intersect(new SquareSet(0, 0x0f0f0f0f)); s = s.xor(t.xor(t.shr64(28))); @@ -26,8 +44,21 @@ export const flipDiagonal = (s: SquareSet): SquareSet => { return s; }; +/** + * Rotates a SquareSet by 180 degrees. + * + * @param {SquareSet} s The SquareSet to rotate. + * @returns {SquareSet} The rotated SquareSet. + */ export const rotate180 = (s: SquareSet): SquareSet => s.rbit64(); +/** + * Transforms a Board by applying a transformation function to each SquareSet. + * + * @param {Board} board The Board to transform. + * @param {function(SquareSet): SquareSet} f The transformation function. + * @returns {Board} The transformed Board. + */ export const transformBoard = (board: Board, f: (s: SquareSet) => SquareSet): Board => { const b = Board.empty(); b.occupied = f(board.occupied); @@ -37,6 +68,13 @@ export const transformBoard = (board: Board, f: (s: SquareSet) => SquareSet): Bo return b; }; +/** + * Transforms a Setup by applying a transformation function to each SquareSet. + * + * @param {Setup} setup The Setup to transform. + * @param {function(SquareSet): SquareSet} f The transformation function. + * @returns {Setup} The transformed Setup. + */ export const transformSetup = (setup: Setup, f: (s: SquareSet) => SquareSet): Setup => ({ board: transformBoard(setup.board, f), pockets: setup.pockets?.clone(), diff --git a/src/types.ts b/src/types.ts index 4310dd76..b815e885 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,76 +1,162 @@ +/** + * An array of file names in a chess board. + * @constant + */ export const FILE_NAMES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] as const; +/** + * The type representing a file name in a chess board. + */ export type FileName = (typeof FILE_NAMES)[number]; +/** + * An array of rank names in a chess board. + * @constant + */ export const RANK_NAMES = ['1', '2', '3', '4', '5', '6', '7', '8'] as const; +/** + * The type representing a rank name in a chess board. + */ export type RankName = (typeof RANK_NAMES)[number]; +/** + * The type representing a square on the chess board. + * + * A number between 0 and 63, inclusive. + */ export type Square = number; +/** + * The type representing the name of a square on the chess board. + */ export type SquareName = `${FileName}${RankName}`; /** - * Indexable by square indices. + * The type representing an array indexed by squares. + * @template T */ export type BySquare = T[]; +/** + * An array of chess piece colors. + * @constant + */ export const COLORS = ['white', 'black'] as const; +/** + * The type representing a chess piece color. + */ export type Color = (typeof COLORS)[number]; /** - * Indexable by `white` and `black`. + * The type representing an object indexed by colors. + * @template T */ export type ByColor = { [color in Color]: T; }; +/** + * An array of chess piece roles. + * @constant + */ export const ROLES = ['pawn', 'knight', 'bishop', 'rook', 'queen', 'king'] as const; +/** + * The type representing a chess piece role. + */ export type Role = (typeof ROLES)[number]; /** - * Indexable by `pawn`, `knight`, `bishop`, `rook`, `queen`, and `king`. + * The type representing an object indexed by roles. + * @template T */ export type ByRole = { [role in Role]: T; }; +/** + * An array of castling sides. + * @constant + */ export const CASTLING_SIDES = ['a', 'h'] as const; +/** + * The type representing a castling side. + */ export type CastlingSide = (typeof CASTLING_SIDES)[number]; /** - * Indexable by `a` and `h`. + * The type representing an object indexed by castling sides. + * @template T */ export type ByCastlingSide = { [side in CastlingSide]: T; }; +/** + * An interface representing a chess piece. + * @interface Piece + * @property {Role} role The role of the piece. + * @property {Color} color The color of the piece. + * @property {boolean} [promoted] Whether the piece is promoted (optional). + */ export interface Piece { role: Role; color: Color; promoted?: boolean; } +/** + * An interface representing a normal chess move. + * @interface NormalMove + * @property {Square} from The starting square of the move. + * @property {Square} to The destination square of the move. + * @property {Role} [promotion] The role to promote the pawn to (optional). + */ export interface NormalMove { from: Square; to: Square; promotion?: Role; } +/** + * An interface representing a drop move in chess variants. + * @interface DropMove + * @property {Role} role The role of the piece being dropped. + * @property {Square} to The square where the piece is dropped. + */ export interface DropMove { role: Role; to: Square; } +/** + * The type representing a chess move (either a normal move or a drop move). + */ export type Move = NormalMove | DropMove; +/** + * A type guard function to check if a move is a drop move. + * @function isDrop + * @param {Move} v The move to check. + * @returns {v is DropMove} - Returns true if the move is a drop move. + */ export const isDrop = (v: Move): v is DropMove => 'role' in v; +/** + * A type guard function to check if a move is a normal move. + * @function isNormal + * @param {Move} v The move to check. + * @returns {v is NormalMove} - Returns true if the move is a normal move. + */ export const isNormal = (v: Move): v is NormalMove => 'from' in v; +/** + * An array of chess variant rules. + * @constant + */ export const RULES = [ 'chess', 'antichess', @@ -82,8 +168,16 @@ export const RULES = [ 'crazyhouse', ] as const; +/** + * The type representing a chess variant rule. + */ export type Rules = (typeof RULES)[number]; +/** + * An interface representing the outcome of a chess game. + * @interface Outcome + * @property {Color | undefined} winner The color of the winning side, or undefined if the game is a draw. + */ export interface Outcome { winner: Color | undefined; } diff --git a/src/util.ts b/src/util.ts index 1b68ba02..dbe85704 100644 --- a/src/util.ts +++ b/src/util.ts @@ -11,17 +11,55 @@ import { SquareName, } from './types.js'; +/** + * A type guard function to check if a value is defined (not undefined). + * @function defined + * @template A + * @param {A | undefined} v The value to check. + * @returns {v is A} Returns true if the value is defined (not undefined). + */ export const defined = (v: A | undefined): v is A => v !== undefined; +/** + * A function to get the opposite color of a given chess piece color. + * @function opposite + * @param {Color} color The color to get the opposite of. + * @returns {Color} The opposite color. + */ export const opposite = (color: Color): Color => (color === 'white' ? 'black' : 'white'); +/** + * A function to get the rank of a square on the chess board. + * @function squareRank + * @param {Square} square The square to get the rank of. + * @returns {number} The rank of the square (0-7). + */ export const squareRank = (square: Square): number => square >> 3; +/** + * A function to get the file of a square on the chess board. + * @function squareFile + * @param {Square} square The square to get the file of. + * @returns {number} The file of the square (0-7). + */ export const squareFile = (square: Square): number => square & 0x7; +/** + * A function to get the square corresponding to the given file and rank coordinates. + * @function squareFromCoords + * @param {number} file The file coordinate (0-7). + * @param {number} rank The rank coordinate (0-7). + * @returns {Square | undefined} The corresponding square if the coordinates are valid, or undefined if the coordinates are out of bounds. + */ export const squareFromCoords = (file: number, rank: number): Square | undefined => 0 <= file && file < 8 && 0 <= rank && rank < 8 ? file + 8 * rank : undefined; +/** + * A function to convert a chess piece role to its corresponding character representation. + * @function roleToChar + * @param {Role} role The chess piece role. + * @returns {string} The character representation of the role. + */ export const roleToChar = (role: Role): string => { switch (role) { case 'pawn': @@ -39,6 +77,12 @@ export const roleToChar = (role: Role): string => { } }; +/** + * A function to convert a character to its corresponding chess piece role. + * @function charToRole + * @param {string} ch The character to convert. + * @returns {Role | undefined} The corresponding chess piece role, or undefined if the character is not valid. + */ export function charToRole(ch: 'p' | 'n' | 'b' | 'r' | 'q' | 'k' | 'P' | 'N' | 'B' | 'R' | 'Q' | 'K'): Role; export function charToRole(ch: string): Role | undefined; export function charToRole(ch: string): Role | undefined { @@ -60,6 +104,12 @@ export function charToRole(ch: string): Role | undefined { } } +/** + * A function to parse a square name and return the corresponding square. + * @function parseSquare + * @param {string} str The square name to parse. + * @returns {Square | undefined} The corresponding square, or undefined if the square name is not valid. + */ export function parseSquare(str: SquareName): Square; export function parseSquare(str: string): Square | undefined; export function parseSquare(str: string): Square | undefined { @@ -67,9 +117,21 @@ export function parseSquare(str: string): Square | undefined { return squareFromCoords(str.charCodeAt(0) - 'a'.charCodeAt(0), str.charCodeAt(1) - '1'.charCodeAt(0)); } +/** + * A function to convert a square to its corresponding square name. + * @function makeSquare + * @param {Square} square The square to convert. + * @returns {SquareName} The corresponding square name. + */ export const makeSquare = (square: Square): SquareName => (FILE_NAMES[squareFile(square)] + RANK_NAMES[squareRank(square)]) as SquareName; +/** + * A function to parse a UCI (Universal Chess Interface) string and return the corresponding move. + * @function parseUci + * @param {string} str The UCI string to parse. + * @returns {Move | undefined} The corresponding move, or undefined if the UCI string is not valid. + */ export const parseUci = (str: string): Move | undefined => { if (str[1] === '@' && str.length === 4) { const role = charToRole(str[0]); @@ -88,6 +150,13 @@ export const parseUci = (str: string): Move | undefined => { return; }; +/** + * A function to check if two moves are equal. + * @function moveEquals + * @param {Move} left The first move to compare. + * @param {Move} right The second move to compare. + * @returns {boolean} `true` if the moves are equal, `false` otherwise. + */ export const moveEquals = (left: Move, right: Move): boolean => { if (left.to !== right.to) return false; if (isDrop(left)) return isDrop(right) && left.role === right.role; @@ -95,16 +164,32 @@ export const moveEquals = (left: Move, right: Move): boolean => { }; /** - * Converts a move to UCI notation, like `g1f3` for a normal move, - * `a7a8q` for promotion to a queen, and `Q@f7` for a Crazyhouse drop. + * A function to convert a move to its corresponding UCI string representation. + * @function makeUci + * @param {Move} move The move to convert. + * @returns {string} The corresponding UCI string representation of the move. */ export const makeUci = (move: Move): string => isDrop(move) ? `${roleToChar(move.role).toUpperCase()}@${makeSquare(move.to)}` : makeSquare(move.from) + makeSquare(move.to) + (move.promotion ? roleToChar(move.promotion) : ''); +/** + * A function to get the square where the king castles to for a given color and castling side. + * @function kingCastlesTo + * @param {Color} color The color of the king. + * @param {CastlingSide} side The castling side ('a' for queenside, 'h' for kingside). + * @returns {Square} The square where the king castles to. + */ export const kingCastlesTo = (color: Color, side: CastlingSide): Square => color === 'white' ? (side === 'a' ? 2 : 6) : side === 'a' ? 58 : 62; +/** + * A function to get the square where the rook castles to for a given color and castling side. + * @function rookCastlesTo + * @param {Color} color The color of the rook. + * @param {CastlingSide} side The castling side ('a' for queenside, 'h' for kingside). + * @returns {Square} The square where the rook castles to. + */ export const rookCastlesTo = (color: Color, side: CastlingSide): Square => color === 'white' ? (side === 'a' ? 3 : 5) : side === 'a' ? 59 : 61;