chess.mjs
Version:
It's just the ES6 module version of chess.js
1,529 lines (1,311 loc) • 66.1 kB
JavaScript
/*
* Copyright (c) 2022, Jeff Hlywa (jhlywa@gmail.com)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*----------------------------------------------------------------------------*/
const SYMBOLS = 'pnbrqkPNBRQK'
const DEFAULT_POSITION =
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
const TERMINATION_MARKERS = ['1-0', '0-1', '1/2-1/2', '*']
const PAWN_OFFSETS = {
b: [16, 32, 17, 15],
w: [-16, -32, -17, -15],
}
const PIECE_OFFSETS = {
n: [-18, -33, -31, -14, 18, 33, 31, 14],
b: [-17, -15, 17, 15],
r: [-16, 1, 16, -1],
q: [-17, -16, -15, 1, 17, 16, 15, -1],
k: [-17, -16, -15, 1, 17, 16, 15, -1],
}
// prettier-ignore
const ATTACKS = [
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20, 0,
0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
24,24,24,24,24,24,56, 0, 56,24,24,24,24,24,24, 0,
0, 0, 0, 0, 0, 2,53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,20, 2, 24, 2,20, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,20, 0, 0, 24, 0, 0,20, 0, 0, 0, 0, 0,
0, 0, 0,20, 0, 0, 0, 24, 0, 0, 0,20, 0, 0, 0, 0,
0, 0,20, 0, 0, 0, 0, 24, 0, 0, 0, 0,20, 0, 0, 0,
0,20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0,20, 0, 0,
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0,20
];
// prettier-ignore
const RAYS = [
17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1,-1, -1, -1, -1, 0,
0, 0, 0, 0, 0, 0,-15,-16,-17, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,-15, 0,-16, 0,-17, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,-15, 0, 0,-16, 0, 0,-17, 0, 0, 0, 0, 0,
0, 0, 0,-15, 0, 0, 0,-16, 0, 0, 0,-17, 0, 0, 0, 0,
0, 0,-15, 0, 0, 0, 0,-16, 0, 0, 0, 0,-17, 0, 0, 0,
0,-15, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0,-17, 0, 0,
-15, 0, 0, 0, 0, 0, 0,-16, 0, 0, 0, 0, 0, 0,-17
];
const SHIFTS = { p: 0, n: 1, b: 2, r: 3, q: 4, k: 5 }
const BITS = {
NORMAL: 1,
CAPTURE: 2,
BIG_PAWN: 4,
EP_CAPTURE: 8,
PROMOTION: 16,
KSIDE_CASTLE: 32,
QSIDE_CASTLE: 64,
}
const RANK_1 = 7
const RANK_2 = 6
const RANK_3 = 5
const RANK_4 = 4
const RANK_5 = 3
const RANK_6 = 2
const RANK_7 = 1
const RANK_8 = 0
// prettier-ignore
const SQUARE_MAP = {
a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7,
a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23,
a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39,
a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55,
a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71,
a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87,
a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103,
a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119
};
const ROOKS = {
w: [
{ square: SQUARE_MAP.a1, flag: BITS.QSIDE_CASTLE },
{ square: SQUARE_MAP.h1, flag: BITS.KSIDE_CASTLE },
],
b: [
{ square: SQUARE_MAP.a8, flag: BITS.QSIDE_CASTLE },
{ square: SQUARE_MAP.h8, flag: BITS.KSIDE_CASTLE },
],
}
const PARSER_STRICT = 0
const PARSER_SLOPPY = 1
/* this function is used to uniquely identify ambiguous moves */
function get_disambiguator(move, moves) {
var from = move.from
var to = move.to
var piece = move.piece
var ambiguities = 0
var same_rank = 0
var same_file = 0
for (var i = 0, len = moves.length; i < len; i++) {
var ambig_from = moves[i].from
var ambig_to = moves[i].to
var ambig_piece = moves[i].piece
/* if a move of the same piece type ends on the same to square, we'll
* need to add a disambiguator to the algebraic notation
*/
if (piece === ambig_piece && from !== ambig_from && to === ambig_to) {
ambiguities++
if (rank(from) === rank(ambig_from)) {
same_rank++
}
if (file(from) === file(ambig_from)) {
same_file++
}
}
}
if (ambiguities > 0) {
/* if there exists a similar moving piece on the same rank and file as
* the move in question, use the square as the disambiguator
*/
if (same_rank > 0 && same_file > 0) {
return algebraic(from)
} else if (same_file > 0) {
/* if the moving piece rests on the same file, use the rank symbol as the
* disambiguator
*/
return algebraic(from).charAt(1)
} else {
/* else use the file symbol */
return algebraic(from).charAt(0)
}
}
return ''
}
function infer_piece_type(san) {
var piece_type = san.charAt(0)
if (piece_type >= 'a' && piece_type <= 'h') {
var matches = san.match(/[a-h]\d.*[a-h]\d/)
if (matches) {
return undefined
}
return PAWN
}
piece_type = piece_type.toLowerCase()
if (piece_type === 'o') {
return KING
}
return piece_type
}
// parses all of the decorators out of a SAN string
function stripped_san(move) {
return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '')
}
/*****************************************************************************
* UTILITY FUNCTIONS
****************************************************************************/
function rank(i) {
return i >> 4
}
function file(i) {
return i & 15
}
function algebraic(i) {
var f = file(i),
r = rank(i)
return 'abcdefgh'.substring(f, f + 1) + '87654321'.substring(r, r + 1)
}
function swap_color(c) {
return c === WHITE ? BLACK : WHITE
}
function is_digit(c) {
return '0123456789'.indexOf(c) !== -1
}
function clone(obj) {
var dupe = obj instanceof Array ? [] : {}
for (var property in obj) {
if (typeof property === 'object') {
dupe[property] = clone(obj[property])
} else {
dupe[property] = obj[property]
}
}
return dupe
}
function trim(str) {
return str.replace(/^\s+|\s+$/g, '')
}
/***************************************************************************
* PUBLIC CONSTANTS
**************************************************************************/
export const BLACK = 'b'
export const WHITE = 'w'
export const EMPTY = -1
export const PAWN = 'p'
export const KNIGHT = 'n'
export const BISHOP = 'b'
export const ROOK = 'r'
export const QUEEN = 'q'
export const KING = 'k'
export const SQUARES = (function () {
/* from the ECMA-262 spec (section 12.6.4):
* "The mechanics of enumerating the properties ... is
* implementation dependent"
* so: for (var sq in SQUARES) { keys.push(sq); } might not be
* ordered correctly
*/
var keys = []
for (var i = SQUARE_MAP.a8; i <= SQUARE_MAP.h1; i++) {
if (i & 0x88) {
i += 7
continue
}
keys.push(algebraic(i))
}
return keys
})()
export const FLAGS = {
NORMAL: 'n',
CAPTURE: 'c',
BIG_PAWN: 'b',
EP_CAPTURE: 'e',
PROMOTION: 'p',
KSIDE_CASTLE: 'k',
QSIDE_CASTLE: 'q',
}
export const Chess = function (fen) {
var board = new Array(128)
var kings = { w: EMPTY, b: EMPTY }
var turn = WHITE
var castling = { w: 0, b: 0 }
var ep_square = EMPTY
var half_moves = 0
var move_number = 1
var history = []
var header = {}
var comments = {}
/* if the user passes in a fen string, load it, else default to
* starting position
*/
if (typeof fen === 'undefined') {
load(DEFAULT_POSITION)
} else {
load(fen)
}
function clear(keep_headers) {
if (typeof keep_headers === 'undefined') {
keep_headers = false
}
board = new Array(128)
kings = { w: EMPTY, b: EMPTY }
turn = WHITE
castling = { w: 0, b: 0 }
ep_square = EMPTY
half_moves = 0
move_number = 1
history = []
if (!keep_headers) header = {}
comments = {}
update_setup(generate_fen())
}
function prune_comments() {
var reversed_history = []
var current_comments = {}
var copy_comment = function (fen) {
if (fen in comments) {
current_comments[fen] = comments[fen]
}
}
while (history.length > 0) {
reversed_history.push(undo_move())
}
copy_comment(generate_fen())
while (reversed_history.length > 0) {
make_move(reversed_history.pop())
copy_comment(generate_fen())
}
comments = current_comments
}
function reset() {
load(DEFAULT_POSITION)
}
function load(fen, keep_headers) {
if (typeof keep_headers === 'undefined') {
keep_headers = false
}
var tokens = fen.split(/\s+/)
var position = tokens[0]
var square = 0
if (!validate_fen(fen).valid) {
return false
}
clear(keep_headers)
for (var i = 0; i < position.length; i++) {
var piece = position.charAt(i)
if (piece === '/') {
square += 8
} else if (is_digit(piece)) {
square += parseInt(piece, 10)
} else {
var color = piece < 'a' ? WHITE : BLACK
put({ type: piece.toLowerCase(), color: color }, algebraic(square))
square++
}
}
turn = tokens[1]
if (tokens[2].indexOf('K') > -1) {
castling.w |= BITS.KSIDE_CASTLE
}
if (tokens[2].indexOf('Q') > -1) {
castling.w |= BITS.QSIDE_CASTLE
}
if (tokens[2].indexOf('k') > -1) {
castling.b |= BITS.KSIDE_CASTLE
}
if (tokens[2].indexOf('q') > -1) {
castling.b |= BITS.QSIDE_CASTLE
}
ep_square = tokens[3] === '-' ? EMPTY : SQUARE_MAP[tokens[3]]
half_moves = parseInt(tokens[4], 10)
move_number = parseInt(tokens[5], 10)
update_setup(generate_fen())
return true
}
/* TODO: this function is pretty much crap - it validates structure but
* completely ignores content (e.g. doesn't verify that each side has a king)
* ... we should rewrite this, and ditch the silly error_number field while
* we're at it
*/
function validate_fen(fen) {
var errors = {
0: 'No errors.',
1: 'FEN string must contain six space-delimited fields.',
2: '6th field (move number) must be a positive integer.',
3: '5th field (half move counter) must be a non-negative integer.',
4: '4th field (en-passant square) is invalid.',
5: '3rd field (castling availability) is invalid.',
6: '2nd field (side to move) is invalid.',
7: "1st field (piece positions) does not contain 8 '/'-delimited rows.",
8: '1st field (piece positions) is invalid [consecutive numbers].',
9: '1st field (piece positions) is invalid [invalid piece].',
10: '1st field (piece positions) is invalid [row too large].',
11: 'Illegal en-passant square',
}
/* 1st criterion: 6 space-seperated fields? */
var tokens = fen.split(/\s+/)
if (tokens.length !== 6) {
return { valid: false, error_number: 1, error: errors[1] }
}
/* 2nd criterion: move number field is a integer value > 0? */
if (isNaN(parseInt(tokens[5])) || parseInt(tokens[5], 10) <= 0) {
return { valid: false, error_number: 2, error: errors[2] }
}
/* 3rd criterion: half move counter is an integer >= 0? */
if (isNaN(parseInt(tokens[4])) || parseInt(tokens[4], 10) < 0) {
return { valid: false, error_number: 3, error: errors[3] }
}
/* 4th criterion: 4th field is a valid e.p.-string? */
if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) {
return { valid: false, error_number: 4, error: errors[4] }
}
/* 5th criterion: 3th field is a valid castle-string? */
if (!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) {
return { valid: false, error_number: 5, error: errors[5] }
}
/* 6th criterion: 2nd field is "w" (white) or "b" (black)? */
if (!/^(w|b)$/.test(tokens[1])) {
return { valid: false, error_number: 6, error: errors[6] }
}
/* 7th criterion: 1st field contains 8 rows? */
var rows = tokens[0].split('/')
if (rows.length !== 8) {
return { valid: false, error_number: 7, error: errors[7] }
}
/* 8th criterion: every row is valid? */
for (var i = 0; i < rows.length; i++) {
/* check for right sum of fields AND not two numbers in succession */
var sum_fields = 0
var previous_was_number = false
for (var k = 0; k < rows[i].length; k++) {
if (!isNaN(rows[i][k])) {
if (previous_was_number) {
return { valid: false, error_number: 8, error: errors[8] }
}
sum_fields += parseInt(rows[i][k], 10)
previous_was_number = true
} else {
if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) {
return { valid: false, error_number: 9, error: errors[9] }
}
sum_fields += 1
previous_was_number = false
}
}
if (sum_fields !== 8) {
return { valid: false, error_number: 10, error: errors[10] }
}
}
if (
(tokens[3][1] == '3' && tokens[1] == 'w') ||
(tokens[3][1] == '6' && tokens[1] == 'b')
) {
return { valid: false, error_number: 11, error: errors[11] }
}
/* everything's okay! */
return { valid: true, error_number: 0, error: errors[0] }
}
function generate_fen() {
var empty = 0
var fen = ''
for (var i = SQUARE_MAP.a8; i <= SQUARE_MAP.h1; i++) {
if (board[i] == null) {
empty++
} else {
if (empty > 0) {
fen += empty
empty = 0
}
var color = board[i].color
var piece = board[i].type
fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase()
}
if ((i + 1) & 0x88) {
if (empty > 0) {
fen += empty
}
if (i !== SQUARE_MAP.h1) {
fen += '/'
}
empty = 0
i += 8
}
}
var cflags = ''
if (castling[WHITE] & BITS.KSIDE_CASTLE) {
cflags += 'K'
}
if (castling[WHITE] & BITS.QSIDE_CASTLE) {
cflags += 'Q'
}
if (castling[BLACK] & BITS.KSIDE_CASTLE) {
cflags += 'k'
}
if (castling[BLACK] & BITS.QSIDE_CASTLE) {
cflags += 'q'
}
/* do we have an empty castling flag? */
cflags = cflags || '-'
var epflags = ep_square === EMPTY ? '-' : algebraic(ep_square)
return [fen, turn, cflags, epflags, half_moves, move_number].join(' ')
}
function set_header(args) {
for (var i = 0; i < args.length; i += 2) {
if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') {
header[args[i]] = args[i + 1]
}
}
return header
}
/* called when the initial board setup is changed with put() or remove().
* modifies the SetUp and FEN properties of the header object. if the FEN is
* equal to the default position, the SetUp and FEN are deleted
* the setup is only updated if history.length is zero, ie moves haven't been
* made.
*/
function update_setup(fen) {
if (history.length > 0) return
if (fen !== DEFAULT_POSITION) {
header['SetUp'] = '1'
header['FEN'] = fen
} else {
delete header['SetUp']
delete header['FEN']
}
}
function get(square) {
var piece = board[SQUARE_MAP[square]]
return piece ? { type: piece.type, color: piece.color } : null
}
function put(piece, square) {
/* check for valid piece object */
if (!('type' in piece && 'color' in piece)) {
return false
}
/* check for piece */
if (SYMBOLS.indexOf(piece.type.toLowerCase()) === -1) {
return false
}
/* check for valid square */
if (!(square in SQUARE_MAP)) {
return false
}
var sq = SQUARE_MAP[square]
/* don't let the user place more than one king */
if (
piece.type == KING &&
!(kings[piece.color] == EMPTY || kings[piece.color] == sq)
) {
return false
}
board[sq] = { type: piece.type, color: piece.color }
if (piece.type === KING) {
kings[piece.color] = sq
}
update_setup(generate_fen())
return true
}
function remove(square) {
var piece = get(square)
board[SQUARE_MAP[square]] = null
if (piece && piece.type === KING) {
kings[piece.color] = EMPTY
}
update_setup(generate_fen())
return piece
}
function build_move(board, from, to, flags, promotion) {
var move = {
color: turn,
from: from,
to: to,
flags: flags,
piece: board[from].type,
}
if (promotion) {
move.flags |= BITS.PROMOTION
move.promotion = promotion
}
if (board[to]) {
move.captured = board[to].type
} else if (flags & BITS.EP_CAPTURE) {
move.captured = PAWN
}
return move
}
function generate_moves(options) {
function add_move(board, moves, from, to, flags) {
/* if pawn promotion */
if (
board[from].type === PAWN &&
(rank(to) === RANK_8 || rank(to) === RANK_1)
) {
var pieces = [QUEEN, ROOK, BISHOP, KNIGHT]
for (var i = 0, len = pieces.length; i < len; i++) {
moves.push(build_move(board, from, to, flags, pieces[i]))
}
} else {
moves.push(build_move(board, from, to, flags))
}
}
var moves = []
var us = turn
var them = swap_color(us)
var second_rank = { b: RANK_7, w: RANK_2 }
var first_sq = SQUARE_MAP.a8
var last_sq = SQUARE_MAP.h1
var single_square = false
/* do we want legal moves? */
var legal =
typeof options !== 'undefined' && 'legal' in options
? options.legal
: true
var piece_type =
typeof options !== 'undefined' &&
'piece' in options &&
typeof options.piece === 'string'
? options.piece.toLowerCase()
: true
/* are we generating moves for a single square? */
if (typeof options !== 'undefined' && 'square' in options) {
if (options.square in SQUARE_MAP) {
first_sq = last_sq = SQUARE_MAP[options.square]
single_square = true
} else {
/* invalid square */
return []
}
}
for (var i = first_sq; i <= last_sq; i++) {
/* did we run off the end of the board */
if (i & 0x88) {
i += 7
continue
}
var piece = board[i]
if (piece == null || piece.color !== us) {
continue
}
if (piece.type === PAWN && (piece_type === true || piece_type === PAWN)) {
/* single square, non-capturing */
var square = i + PAWN_OFFSETS[us][0]
if (board[square] == null) {
add_move(board, moves, i, square, BITS.NORMAL)
/* double square */
var square = i + PAWN_OFFSETS[us][1]
if (second_rank[us] === rank(i) && board[square] == null) {
add_move(board, moves, i, square, BITS.BIG_PAWN)
}
}
/* pawn captures */
for (j = 2; j < 4; j++) {
var square = i + PAWN_OFFSETS[us][j]
if (square & 0x88) continue
if (board[square] != null && board[square].color === them) {
add_move(board, moves, i, square, BITS.CAPTURE)
} else if (square === ep_square) {
add_move(board, moves, i, ep_square, BITS.EP_CAPTURE)
}
}
} else if (piece_type === true || piece_type === piece.type) {
for (var j = 0, len = PIECE_OFFSETS[piece.type].length; j < len; j++) {
var offset = PIECE_OFFSETS[piece.type][j]
var square = i
while (true) {
square += offset
if (square & 0x88) break
if (board[square] == null) {
add_move(board, moves, i, square, BITS.NORMAL)
} else {
if (board[square].color === us) break
add_move(board, moves, i, square, BITS.CAPTURE)
break
}
/* break, if knight or king */
if (piece.type === 'n' || piece.type === 'k') break
}
}
}
}
/* check for castling if: a) we're generating all moves, or b) we're doing
* single square move generation on the king's square
*/
if (piece_type === true || piece_type === KING) {
if (!single_square || last_sq === kings[us]) {
/* king-side castling */
if (castling[us] & BITS.KSIDE_CASTLE) {
var castling_from = kings[us]
var castling_to = castling_from + 2
if (
board[castling_from + 1] == null &&
board[castling_to] == null &&
!attacked(them, kings[us]) &&
!attacked(them, castling_from + 1) &&
!attacked(them, castling_to)
) {
add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE)
}
}
/* queen-side castling */
if (castling[us] & BITS.QSIDE_CASTLE) {
var castling_from = kings[us]
var castling_to = castling_from - 2
if (
board[castling_from - 1] == null &&
board[castling_from - 2] == null &&
board[castling_from - 3] == null &&
!attacked(them, kings[us]) &&
!attacked(them, castling_from - 1) &&
!attacked(them, castling_to)
) {
add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE)
}
}
}
}
/* return all pseudo-legal moves (this includes moves that allow the king
* to be captured)
*/
if (!legal) {
return moves
}
/* filter out illegal moves */
var legal_moves = []
for (var i = 0, len = moves.length; i < len; i++) {
make_move(moves[i])
if (!king_attacked(us)) {
legal_moves.push(moves[i])
}
undo_move()
}
return legal_moves
}
/* convert a move from 0x88 coordinates to Standard Algebraic Notation
* (SAN)
*
* @param {boolean} sloppy Use the sloppy SAN generator to work around over
* disambiguation bugs in Fritz and Chessbase. See below:
*
* r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
* 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
* 4. ... Ne7 is technically the valid SAN
*/
function move_to_san(move, moves) {
var output = ''
if (move.flags & BITS.KSIDE_CASTLE) {
output = 'O-O'
} else if (move.flags & BITS.QSIDE_CASTLE) {
output = 'O-O-O'
} else {
if (move.piece !== PAWN) {
var disambiguator = get_disambiguator(move, moves)
output += move.piece.toUpperCase() + disambiguator
}
if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
if (move.piece === PAWN) {
output += algebraic(move.from)[0]
}
output += 'x'
}
output += algebraic(move.to)
if (move.flags & BITS.PROMOTION) {
output += '=' + move.promotion.toUpperCase()
}
}
make_move(move)
if (in_check()) {
if (in_checkmate()) {
output += '#'
} else {
output += '+'
}
}
undo_move()
return output
}
function attacked(color, square) {
for (var i = SQUARE_MAP.a8; i <= SQUARE_MAP.h1; i++) {
/* did we run off the end of the board */
if (i & 0x88) {
i += 7
continue
}
/* if empty square or wrong color */
if (board[i] == null || board[i].color !== color) continue
var piece = board[i]
var difference = i - square
var index = difference + 119
if (ATTACKS[index] & (1 << SHIFTS[piece.type])) {
if (piece.type === PAWN) {
if (difference > 0) {
if (piece.color === WHITE) return true
} else {
if (piece.color === BLACK) return true
}
continue
}
/* if the piece is a knight or a king */
if (piece.type === 'n' || piece.type === 'k') return true
var offset = RAYS[index]
var j = i + offset
var blocked = false
while (j !== square) {
if (board[j] != null) {
blocked = true
break
}
j += offset
}
if (!blocked) return true
}
}
return false
}
function king_attacked(color) {
return attacked(swap_color(color), kings[color])
}
function in_check() {
return king_attacked(turn)
}
function in_checkmate() {
return in_check() && generate_moves().length === 0
}
function in_stalemate() {
return !in_check() && generate_moves().length === 0
}
function insufficient_material() {
var pieces = {}
var bishops = []
var num_pieces = 0
var sq_color = 0
for (var i = SQUARE_MAP.a8; i <= SQUARE_MAP.h1; i++) {
sq_color = (sq_color + 1) % 2
if (i & 0x88) {
i += 7
continue
}
var piece = board[i]
if (piece) {
pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1
if (piece.type === BISHOP) {
bishops.push(sq_color)
}
num_pieces++
}
}
/* k vs. k */
if (num_pieces === 2) {
return true
} else if (
/* k vs. kn .... or .... k vs. kb */
num_pieces === 3 &&
(pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)
) {
return true
} else if (num_pieces === pieces[BISHOP] + 2) {
/* kb vs. kb where any number of bishops are all on the same color */
var sum = 0
var len = bishops.length
for (var i = 0; i < len; i++) {
sum += bishops[i]
}
if (sum === 0 || sum === len) {
return true
}
}
return false
}
function in_threefold_repetition() {
/* TODO: while this function is fine for casual use, a better
* implementation would use a Zobrist key (instead of FEN). the
* Zobrist key would be maintained in the make_move/undo_move functions,
* avoiding the costly that we do below.
*/
var moves = []
var positions = {}
var repetition = false
while (true) {
var move = undo_move()
if (!move) break
moves.push(move)
}
while (true) {
/* remove the last two fields in the FEN string, they're not needed
* when checking for draw by rep */
var fen = generate_fen().split(' ').slice(0, 4).join(' ')
/* has the position occurred three or move times */
positions[fen] = fen in positions ? positions[fen] + 1 : 1
if (positions[fen] >= 3) {
repetition = true
}
if (!moves.length) {
break
}
make_move(moves.pop())
}
return repetition
}
function push(move) {
history.push({
move: move,
kings: { b: kings.b, w: kings.w },
turn: turn,
castling: { b: castling.b, w: castling.w },
ep_square: ep_square,
half_moves: half_moves,
move_number: move_number,
})
}
function make_move(move) {
var us = turn
var them = swap_color(us)
push(move)
board[move.to] = board[move.from]
board[move.from] = null
/* if ep capture, remove the captured pawn */
if (move.flags & BITS.EP_CAPTURE) {
if (turn === BLACK) {
board[move.to - 16] = null
} else {
board[move.to + 16] = null
}
}
/* if pawn promotion, replace with new piece */
if (move.flags & BITS.PROMOTION) {
board[move.to] = { type: move.promotion, color: us }
}
/* if we moved the king */
if (board[move.to].type === KING) {
kings[board[move.to].color] = move.to
/* if we castled, move the rook next to the king */
if (move.flags & BITS.KSIDE_CASTLE) {
var castling_to = move.to - 1
var castling_from = move.to + 1
board[castling_to] = board[castling_from]
board[castling_from] = null
} else if (move.flags & BITS.QSIDE_CASTLE) {
var castling_to = move.to + 1
var castling_from = move.to - 2
board[castling_to] = board[castling_from]
board[castling_from] = null
}
/* turn off castling */
castling[us] = ''
}
/* turn off castling if we move a rook */
if (castling[us]) {
for (var i = 0, len = ROOKS[us].length; i < len; i++) {
if (
move.from === ROOKS[us][i].square &&
castling[us] & ROOKS[us][i].flag
) {
castling[us] ^= ROOKS[us][i].flag
break
}
}
}
/* turn off castling if we capture a rook */
if (castling[them]) {
for (var i = 0, len = ROOKS[them].length; i < len; i++) {
if (
move.to === ROOKS[them][i].square &&
castling[them] & ROOKS[them][i].flag
) {
castling[them] ^= ROOKS[them][i].flag
break
}
}
}
/* if big pawn move, update the en passant square */
if (move.flags & BITS.BIG_PAWN) {
if (turn === 'b') {
ep_square = move.to - 16
} else {
ep_square = move.to + 16
}
} else {
ep_square = EMPTY
}
/* reset the 50 move counter if a pawn is moved or a piece is captured */
if (move.piece === PAWN) {
half_moves = 0
} else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
half_moves = 0
} else {
half_moves++
}
if (turn === BLACK) {
move_number++
}
turn = swap_color(turn)
}
function undo_move() {
var old = history.pop()
if (old == null) {
return null
}
var move = old.move
kings = old.kings
turn = old.turn
castling = old.castling
ep_square = old.ep_square
half_moves = old.half_moves
move_number = old.move_number
var us = turn
var them = swap_color(turn)
board[move.from] = board[move.to]
board[move.from].type = move.piece // to undo any promotions
board[move.to] = null
if (move.flags & BITS.CAPTURE) {
board[move.to] = { type: move.captured, color: them }
} else if (move.flags & BITS.EP_CAPTURE) {
var index
if (us === BLACK) {
index = move.to - 16
} else {
index = move.to + 16
}
board[index] = { type: PAWN, color: them }
}
if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
var castling_to, castling_from
if (move.flags & BITS.KSIDE_CASTLE) {
castling_to = move.to + 1
castling_from = move.to - 1
} else if (move.flags & BITS.QSIDE_CASTLE) {
castling_to = move.to - 2
castling_from = move.to + 1
}
board[castling_to] = board[castling_from]
board[castling_from] = null
}
return move
}
// convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
function move_from_san(move, sloppy) {
// strip off any move decorations: e.g Nf3+?! becomes Nf3
var clean_move = stripped_san(move)
// the move parsers is a 2-step state
for (var parser = 0; parser < 2; parser++) {
if (parser == PARSER_SLOPPY) {
// only run the sloppy parse if explicitly requested
if (!sloppy) {
return null
}
// The sloppy parser allows the user to parse non-standard chess
// notations. This parser is opt-in (by specifying the
// '{ sloppy: true }' setting) and is only run after the Standard
// Algebraic Notation (SAN) parser has failed.
//
// When running the sloppy parser, we'll run a regex to grab the piece,
// the to/from square, and an optional promotion piece. This regex will
// parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7,
// f7f8q, b1c3
// NOTE: Some positions and moves may be ambiguous when using the
// sloppy parser. For example, in this position:
// 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1, the move b1c3 may be interpreted
// as Nc3 or B1c3 (a disambiguated bishop move). In these cases, the
// sloppy parser will default to the most most basic interpretation
// (which is b1c3 parsing to Nc3).
// FIXME: these var's are hoisted into function scope, this will need
// to change when switching to const/let
var overly_disambiguated = false
var matches = clean_move.match(
/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/
)
if (matches) {
var piece = matches[1]
var from = matches[2]
var to = matches[3]
var promotion = matches[4]
if (from.length == 1) {
overly_disambiguated = true
}
} else {
// The [a-h]?[1-8]? portion of the regex below handles moves that may
// be overly disambiguated (e.g. Nge7 is unnecessary and non-standard
// when there is one legal knight move to e7). In this case, the value
// of 'from' variable will be a rank or file, not a square.
var matches = clean_move.match(
/([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?/
)
if (matches) {
var piece = matches[1]
var from = matches[2]
var to = matches[3]
var promotion = matches[4]
if (from.length == 1) {
var overly_disambiguated = true
}
}
}
}
var piece_type = infer_piece_type(clean_move)
var moves = generate_moves({
legal: true,
piece: piece ? piece : piece_type,
})
for (var i = 0, len = moves.length; i < len; i++) {
switch (parser) {
case PARSER_STRICT: {
if (clean_move === stripped_san(move_to_san(moves[i], moves))) {
return moves[i]
}
break
}
case PARSER_SLOPPY: {
if (matches) {
// hand-compare move properties with the results from our sloppy
// regex
if (
(!piece || piece.toLowerCase() == moves[i].piece) &&
SQUARE_MAP[from] == moves[i].from &&
SQUARE_MAP[to] == moves[i].to &&
(!promotion || promotion.toLowerCase() == moves[i].promotion)
) {
return moves[i]
} else if (overly_disambiguated) {
// SPECIAL CASE: we parsed a move string that may have an
// unneeded rank/file disambiguator (e.g. Nge7). The 'from'
// variable will
var square = algebraic(moves[i].from)
if (
(!piece || piece.toLowerCase() == moves[i].piece) &&
SQUARE_MAP[to] == moves[i].to &&
(from == square[0] || from == square[1]) &&
(!promotion || promotion.toLowerCase() == moves[i].promotion)
) {
return moves[i]
}
}
}
}
}
}
}
return null
}
/* pretty = external move object */
function make_pretty(ugly_move) {
var move = clone(ugly_move)
move.san = move_to_san(move, generate_moves({ legal: true }))
move.to = algebraic(move.to)
move.from = algebraic(move.from)
var flags = ''
for (var flag in BITS) {
if (BITS[flag] & move.flags) {
flags += FLAGS[flag]
}
}
move.flags = flags
return move
}
/*****************************************************************************
* DEBUGGING UTILITIES
****************************************************************************/
function perft(depth) {
var moves = generate_moves({ legal: false })
var nodes = 0
var color = turn
for (var i = 0, len = moves.length; i < len; i++) {
make_move(moves[i])
if (!king_attacked(color)) {
if (depth - 1 > 0) {
var child_nodes = perft(depth - 1)
nodes += child_nodes
} else {
nodes++
}
}
undo_move()
}
return nodes
}
return {
/***************************************************************************
* PUBLIC API
**************************************************************************/
load: function (fen) {
return load(fen)
},
reset: function () {
return reset()
},
moves: function (options) {
/* The internal representation of a chess move is in 0x88 format, and
* not meant to be human-readable. The code below converts the 0x88
* square coordinates to algebraic coordinates. It also prunes an
* unnecessary move keys resulting from a verbose call.
*/
var ugly_moves = generate_moves(options)
var moves = []
for (var i = 0, len = ugly_moves.length; i < len; i++) {
/* does the user want a full move object (most likely not), or just
* SAN
*/
if (
typeof options !== 'undefined' &&
'verbose' in options &&
options.verbose
) {
moves.push(make_pretty(ugly_moves[i]))
} else {
moves.push(
move_to_san(ugly_moves[i], generate_moves({ legal: true }))
)
}
}
return moves
},
in_check: function () {
return in_check()
},
in_checkmate: function () {
return in_checkmate()
},
in_stalemate: function () {
return in_stalemate()
},
in_draw: function () {
return (
half_moves >= 100 ||
in_stalemate() ||
insufficient_material() ||
in_threefold_repetition()
)
},
insufficient_material: function () {
return insufficient_material()
},
in_threefold_repetition: function () {
return in_threefold_repetition()
},
game_over: function () {
return (
half_moves >= 100 ||
in_checkmate() ||
in_stalemate() ||
insufficient_material() ||
in_threefold_repetition()
)
},
validate_fen: function (fen) {
return validate_fen(fen)
},
fen: function () {
return generate_fen()
},
board: function () {
var output = [],
row = []
for (var i = SQUARE_MAP.a8; i <= SQUARE_MAP.h1; i++) {
if (board[i] == null) {
row.push(null)
} else {
row.push({
square: algebraic(i),
type: board[i].type,
color: board[i].color,
})
}
if ((i + 1) & 0x88) {
output.push(row)
row = []
i += 8
}
}
return output
},
pgn: function (options) {
/* using the specification from http://www.chessclub.com/help/PGN-spec
* example for html usage: .pgn({ max_width: 72, newline_char: "<br />" })
*/
var newline =
typeof options === 'object' && typeof options.newline_char === 'string'
? options.newline_char
: '\n'
var max_width =
typeof options === 'object' && typeof options.max_width === 'number'
? options.max_width
: 0
var result = []
var header_exists = false
/* add the PGN header information */
for (var i in header) {
/* TODO: order of enumerated properties in header object is not
* guaranteed, see ECMA-262 spec (section 12.6.4)
*/
result.push('[' + i + ' "' + header[i] + '"]' + newline)
header_exists = true
}
if (header_exists && history.length) {
result.push(newline)
}
var append_comment = function (move_string) {
var comment = comments[generate_fen()]
if (typeof comment !== 'undefined') {
var delimiter = move_string.length > 0 ? ' ' : ''
move_string = `${move_string}${delimiter}{${comment}}`
}
return move_string
}
/* pop all of history onto reversed_history */
var reversed_history = []
while (history.length > 0) {
reversed_history.push(undo_move())
}
var moves = []
var move_string = ''
/* special case of a commented starting position with no moves */
if (reversed_history.length === 0) {
moves.push(append_comment(''))
}
/* build the list of moves. a move_string looks like: "3. e3 e6" */
while (reversed_history.length > 0) {
move_string = append_comment(move_string)
var move = reversed_history.pop()
/* if the position started with black to move, start PGN with #. ... */
if (!history.length && move.color === 'b') {
const prefix = `${move_number}. ...`
/* is there a comment preceding the first move? */
move_string = move_string ? `${move_string} ${prefix}` : prefix
} else if (move.color === 'w') {
/* store the previous generated move_string if we have one */
if (move_string.length) {
moves.push(move_string)
}
move_string = move_number + '.'
}
move_string =
move_string + ' ' + move_to_san(move, generate_moves({ legal: true }))
make_move(move)
}
/* are there any other leftover moves? */
if (move_string.length) {
moves.push(append_comment(move_string))