Skip to content

Add JSDoc comments and documentation #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 115 additions & 19 deletions src/attacks.ts
Original file line number Diff line number Diff line change
@@ -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<T>} The table of values for each square.
*/
const tabulate = <T>(f: (square: Square) => T): BySquare<T> => {
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<SquareSet>}
*/
const KING_ATTACKS: BySquare<SquareSet> = 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<SquareSet>}
*/
const KNIGHT_ATTACKS: BySquare<SquareSet> = 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<SquareSet>; black: BySquare<SquareSet> }}
*/
const PAWN_ATTACKS: {
white: BySquare<SquareSet>;
black: BySquare<SquareSet>;
} = {
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,31 +159,51 @@ 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);
return hyperbola(bit, DIAG_RANGE[square], occupied).xor(hyperbola(bit, ANTI_DIAG_RANGE[square], occupied));
};

/**
* 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)
88 changes: 82 additions & 6 deletions src/board.ts
Original file line number Diff line number Diff line change
@@ -12,26 +12,41 @@ import { ByColor, ByRole, Color, COLORS, Piece, Role, ROLES, Square } from './ty
export class Board implements Iterable<[Square, Piece]>, ByRole<SquareSet>, ByColor<SquareSet> {
/**
* 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,19 +69,30 @@ export class Board implements Iterable<[Square, Piece]>, ByRole<SquareSet>, 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();
for (const color of COLORS) this[color] = SquareSet.empty();
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,19 +102,34 @@ export class Board implements Iterable<[Square, Piece]>, ByRole<SquareSet>, 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;
}
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<SquareSet>, 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<SquareSet>, 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<SquareSet>, 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)
348 changes: 347 additions & 1 deletion src/chess.ts

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions src/compat.ts
Original file line number Diff line number Diff line change
@@ -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<SquareName, SquareName[]> => {
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' => {
149 changes: 149 additions & 0 deletions src/fen.ts
Original file line number Diff line number Diff line change
@@ -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<Board, FenError>}
*/
export const parseBoardFen = (boardPart: string): Result<Board, FenError> => {
const board = Board.empty();
let rank = 7;
@@ -72,6 +81,28 @@ export const parseBoardFen = (boardPart: string): Result<Board, FenError> => {
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<Material, FenError>} 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<Material, FenError> => {
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<Material, FenError> =>
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<SquareSet, FenError>} The castling rights as a SquareSet if parsing is successful, or a FenError if parsing fails.
*/
export const parseCastlingFen = (board: Board, castlingPart: string): Result<SquareSet, FenError> => {
let castlingRights = SquareSet.empty();
if (castlingPart === '-') return Result.ok(castlingRights);
@@ -109,6 +147,32 @@ export const parseCastlingFen = (board: Board, castlingPart: string): Result<Squ
return Result.ok(castlingRights);
};

/**
* Useful for three-check, four-check, n-check variants.
*
* Parses the remaining checks part of a FEN string and returns the corresponding RemainingChecks object.
*
* @param {string} part The remaining checks part of the FEN string.
* @returns {Result<RemainingChecks, FenError>} 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<RemainingChecks, FenError> => {
const parts = part.split('+');
if (parts.length === 3 && parts[0] === '') {
@@ -128,6 +192,28 @@ export const parseRemainingChecks = (part: string): Result<RemainingChecks, FenE
} else return Result.err(new FenError(InvalidFen.RemainingChecks));
};

/**
* Parses a FEN (Forsyth-Edwards Notation) string and returns a Setup object.
*
* @param {string} fen The FEN string to parse.
* @returns {Result<Setup, FenError>} 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<Setup, FenError> => {
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,13 +324,32 @@ 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();
if (piece.promoted) r += '~';
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)}]` : ''),
40 changes: 40 additions & 0 deletions src/san.ts
Original file line number Diff line number Diff line change
@@ -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();

225 changes: 224 additions & 1 deletion src/setup.ts
Original file line number Diff line number Diff line change
@@ -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<number>}
* @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<number> {
/**
* 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<MaterialSide>}
* @property {MaterialSide} white The material configuration for white.
* @property {MaterialSide} black The material configuration for black.
*/
export class Material implements ByColor<MaterialSide> {
/**
* 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<number>}
*/
export class RemainingChecks implements ByColor<number> {
/**
* 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))
236 changes: 236 additions & 0 deletions src/squareSet.ts

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions src/transform.ts
Original file line number Diff line number Diff line change
@@ -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(),
102 changes: 98 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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> = 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<T> = {
[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<T> = {
[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<T> = {
[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;
}
89 changes: 87 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -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 = <A>(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,16 +104,34 @@ 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 {
if (str.length !== 2) return;
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,23 +150,46 @@ 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;
else return isNormal(right) && left.from === right.from && left.promotion === right.promotion;
};

/**
* 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;