kokopu
Version:
A JavaScript/TypeScript library implementing the chess game rules and providing tools to read/write the standard chess file formats.
288 lines (246 loc) • 14.2 kB
text/typescript
/*!
* -------------------------------------------------------------------------- *
* *
* Kokopu - A JavaScript/TypeScript chess library. *
* <https://www.npmjs.com/package/kokopu> *
* Copyright (C) 2018-2026 Yoann Le Montagner <yo35 -at- melix.net> *
* *
* Kokopu is free software: you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public License *
* as published by the Free Software Foundation, either version 3 of *
* the License, or (at your option) any later version. *
* *
* Kokopu is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General *
* Public License along with this program. If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* -------------------------------------------------------------------------- */
import { ColorImpl, PieceImpl, CpI, SpI, SquareImpl, GameVariantImpl } from './base_types_impl';
const EMPTY_BOARD = [
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY,
];
const REGULAR_START_BOARD = [
/* eslint-disable @stylistic/comma-spacing, @stylistic/no-multi-spaces */
CpI.WR , CpI.WN , CpI.WB , CpI.WQ , CpI.WK , CpI.WB , CpI.WN , CpI.WR , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.BR , CpI.BN , CpI.BB , CpI.BQ , CpI.BK , CpI.BB , CpI.BN , CpI.BR ,
/* eslint-enable */
];
const HORDE_START_BOARD = [
/* eslint-disable @stylistic/comma-spacing, @stylistic/no-multi-spaces */
CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, CpI.WP , CpI.WP , SpI.EMPTY, SpI.EMPTY, CpI.WP , CpI.WP , SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.BR , CpI.BN , CpI.BB , CpI.BQ , CpI.BK , CpI.BB , CpI.BN , CpI.BR ,
/* eslint-enable */
];
/**
* Internal structure in `Position`, that encodes the state of the corresponding chess position.
*/
export interface PositionImpl {
// Board state
board: number[],
// Flags
turn: number,
castling: number[],
enPassant: number,
variant: number,
// Computed attributes
legal: boolean | null,
king: number[],
effectiveCastling: number[] | null,
effectiveEnPassant: number | null,
}
interface StartPositionInfo {
board: number[],
castling: number[],
king: number[],
}
interface ScharnaglInfo {
pieceScheme: number[],
castling: number,
kingFile: number,
}
const START_POSITION_INFO: (StartPositionInfo | null)[] = [
{ // Regular chess
board: REGULAR_START_BOARD,
castling: [ 129 /* (1 << A-file) | (1 << H-file) */, 129 /* (1 << A-file) | (1 << H-file) */ ],
king: [ SquareImpl.E1, SquareImpl.E8 ],
},
null, // Chess960
null, // no king
null, // white king only
null, // black king only
{ // Antichess
board: REGULAR_START_BOARD,
castling: [ 0, 0 ],
king: [ -1, -1 ],
},
{ // Horde
board: HORDE_START_BOARD,
castling: [ 0, 129 /* (1 << A-file) | (1 << H-file) */ ],
king: [ -1, SquareImpl.E8 ],
},
];
export function hasCanonicalStartPosition(variant: number) {
return START_POSITION_INFO[variant] !== null;
}
export function makeEmpty(variant: number): PositionImpl {
return {
board: EMPTY_BOARD.slice(),
turn: ColorImpl.WHITE,
castling: [ 0, 0 ],
enPassant: -1,
variant: variant,
legal: variant === GameVariantImpl.NO_KING,
king: [ -1, -1 ],
effectiveCastling: [ 0, 0 ],
effectiveEnPassant: -1,
};
}
/**
* @param variant - Must be a variant with a canonical start position.
*/
export function makeInitial(variant: number): PositionImpl {
const info = START_POSITION_INFO[variant]!; // WARNING: applicable only to variants with a canonical start position.
return {
board: info.board.slice(),
turn: ColorImpl.WHITE,
castling: info.castling.slice(),
enPassant: -1,
variant: variant,
legal: true,
king: info.king.slice(),
effectiveCastling: info.castling.slice(),
effectiveEnPassant: -1,
};
}
/**
* Chess960 initial position, following the numbering scheme proposed by Reinhard Scharnagl (see for instance https://chess960.net/start-positions/).
*
* @param scharnaglCode - Integer between 0 and 959 inclusive.
*/
export function make960FromScharnagl(scharnaglCode: number): PositionImpl {
const info = decodeScharnagl(scharnaglCode);
const rank1 = info.pieceScheme.map(piece => piece * 2 + ColorImpl.WHITE);
const rank8 = info.pieceScheme.map(piece => piece * 2 + ColorImpl.BLACK);
return {
board: [
/* eslint-disable @stylistic/comma-spacing, @stylistic/no-multi-spaces */
rank1[0] , rank1[1] , rank1[2] , rank1[3] , rank1[4] , rank1[5] , rank1[6] , rank1[7] , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , CpI.WP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.EMPTY, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , CpI.BP , SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID, SpI.INVALID,
rank8[0] , rank8[1] , rank8[2] , rank8[3] , rank8[4] , rank8[5] , rank8[6] , rank8[7] ,
/* eslint-enable */
],
turn: ColorImpl.WHITE,
castling: [ info.castling, info.castling ],
enPassant: -1,
variant: GameVariantImpl.CHESS960,
legal: true,
king: [ SquareImpl.A1 + info.kingFile, SquareImpl.A8 + info.kingFile ],
effectiveCastling: [ info.castling, info.castling ],
effectiveEnPassant: -1,
};
}
/**
* @param scharnaglCode - Integer between 0 and 959 inclusive.
*/
function decodeScharnagl(scharnaglCode: number): ScharnaglInfo {
const scheme = [ -1, -1, -1, -1, -1, -1, -1, -1 ];
let castling = 0;
let kingFile = -1;
function forEachEmpty(fun: (file: number, emptyIndex: number) => void) {
let emptyIndex = 0;
for (let file = 0; file < 8; ++file) {
if (scheme[file] >= 0) {
continue;
}
fun(file, emptyIndex);
++emptyIndex;
}
}
function setAt(piece: number, emptyIndexTarget1: number, emptyIndexTarget2: number) {
forEachEmpty((file, emptyIndex) => {
if (emptyIndex === emptyIndexTarget1 || emptyIndex === emptyIndexTarget2) {
scheme[file] = piece;
}
});
}
// Light-square bishop
scheme[(scharnaglCode % 4) * 2 + 1] = PieceImpl.BISHOP;
scharnaglCode = Math.trunc(scharnaglCode / 4);
// Dark-square bishop
scheme[(scharnaglCode % 4) * 2] = PieceImpl.BISHOP;
scharnaglCode = Math.trunc(scharnaglCode / 4);
// Queen
setAt(PieceImpl.QUEEN, scharnaglCode % 6, -1);
scharnaglCode = Math.trunc(scharnaglCode / 6);
// Knights
switch (scharnaglCode) { // `scharnaglCode` is guaranteed here to be between 0 and 9 inclusive
case 0: setAt(PieceImpl.KNIGHT, 0, 1); break;
case 1: setAt(PieceImpl.KNIGHT, 0, 2); break;
case 2: setAt(PieceImpl.KNIGHT, 0, 3); break;
case 3: setAt(PieceImpl.KNIGHT, 0, 4); break;
case 4: setAt(PieceImpl.KNIGHT, 1, 2); break;
case 5: setAt(PieceImpl.KNIGHT, 1, 3); break;
case 6: setAt(PieceImpl.KNIGHT, 1, 4); break;
case 7: setAt(PieceImpl.KNIGHT, 2, 3); break;
case 8: setAt(PieceImpl.KNIGHT, 2, 4); break;
case 9: setAt(PieceImpl.KNIGHT, 3, 4); break;
}
// Rooks and king
forEachEmpty((file, emptyIndex) => {
if (emptyIndex === 1) {
scheme[file] = PieceImpl.KING;
kingFile = file;
}
else {
scheme[file] = PieceImpl.ROOK;
castling |= 1 << file;
}
});
return {
pieceScheme: scheme,
castling: castling,
kingFile: kingFile,
};
}
export function makeCopy(position: PositionImpl): PositionImpl {
return {
board: position.board.slice(),
turn: position.turn,
castling: position.castling.slice(),
enPassant: position.enPassant,
variant: position.variant,
legal: position.legal,
king: position.king.slice(),
effectiveCastling: position.effectiveCastling === null ? null : position.effectiveCastling.slice(),
effectiveEnPassant: position.effectiveEnPassant,
};
}