1646 lines
46 KiB
JavaScript
1646 lines
46 KiB
JavaScript
// downloaded from https://github.com/jhlywa/chess.js/blob/master/chess.js
|
|
// on 27 July 2013
|
|
// commit: eed8f8a3b96d99fcd570b7ced105e8415409a800
|
|
|
|
'use strict';
|
|
|
|
/*
|
|
* Copyright (c) 2011, 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.
|
|
*
|
|
*----------------------------------------------------------------------------*/
|
|
|
|
window['Chess'] = window['Chess'] || function(fen) {
|
|
//var Chess = function(fen) {
|
|
|
|
/* jshint indent: false */
|
|
|
|
var BLACK = 'b';
|
|
var WHITE = 'w';
|
|
|
|
var EMPTY = -1;
|
|
|
|
var PAWN = 'p';
|
|
var KNIGHT = 'n';
|
|
var BISHOP = 'b';
|
|
var ROOK = 'r';
|
|
var QUEEN = 'q';
|
|
var KING = 'k';
|
|
|
|
var SYMBOLS = 'pnbrqkPNBRQK';
|
|
|
|
var DEFAULT_POSITION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
|
|
var POSSIBLE_RESULTS = ['1-0', '0-1', '1/2-1/2', '*'];
|
|
|
|
var PAWN_OFFSETS = {
|
|
b: [16, 32, 17, 15],
|
|
w: [-16, -32, -17, -15]
|
|
};
|
|
|
|
var 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]
|
|
};
|
|
|
|
var 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
|
|
];
|
|
|
|
var 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
|
|
];
|
|
|
|
var SHIFTS = { p: 0, n: 1, b: 2, r: 3, q: 4, k: 5 };
|
|
|
|
var FLAGS = {
|
|
NORMAL: 'n',
|
|
CAPTURE: 'c',
|
|
BIG_PAWN: 'b',
|
|
EP_CAPTURE: 'e',
|
|
PROMOTION: 'p',
|
|
KSIDE_CASTLE: 'k',
|
|
QSIDE_CASTLE: 'q'
|
|
};
|
|
|
|
var BITS = {
|
|
NORMAL: 1,
|
|
CAPTURE: 2,
|
|
BIG_PAWN: 4,
|
|
EP_CAPTURE: 8,
|
|
PROMOTION: 16,
|
|
KSIDE_CASTLE: 32,
|
|
QSIDE_CASTLE: 64
|
|
};
|
|
|
|
var RANK_1 = 7;
|
|
var RANK_2 = 6;
|
|
var RANK_3 = 5;
|
|
var RANK_4 = 4;
|
|
var RANK_5 = 3;
|
|
var RANK_6 = 2;
|
|
var RANK_7 = 1;
|
|
var RANK_8 = 0;
|
|
|
|
var SQUARES = {
|
|
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
|
|
};
|
|
|
|
var ROOKS = {
|
|
w: [{square: SQUARES.a1, flag: BITS.QSIDE_CASTLE},
|
|
{square: SQUARES.h1, flag: BITS.KSIDE_CASTLE}],
|
|
b: [{square: SQUARES.a8, flag: BITS.QSIDE_CASTLE},
|
|
{square: SQUARES.h8, flag: BITS.KSIDE_CASTLE}]
|
|
};
|
|
|
|
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 = {};
|
|
|
|
/* 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() {
|
|
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 = [];
|
|
header = {};
|
|
update_setup(generate_fen());
|
|
}
|
|
|
|
function reset() {
|
|
load(DEFAULT_POSITION);
|
|
}
|
|
|
|
function load(fen) {
|
|
var tokens = fen.split(/\s+/);
|
|
var position = tokens[0];
|
|
var square = 0;
|
|
var valid = SYMBOLS + '12345678/';
|
|
|
|
if (!validate_fen(fen).valid) {
|
|
return false;
|
|
}
|
|
|
|
clear();
|
|
|
|
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 : SQUARES[tokens[3]];
|
|
half_moves = parseInt(tokens[4], 10);
|
|
move_number = parseInt(tokens[5], 10);
|
|
|
|
update_setup(generate_fen());
|
|
|
|
return true;
|
|
}
|
|
|
|
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].',
|
|
};
|
|
|
|
/* 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(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(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]};
|
|
}
|
|
}
|
|
|
|
/* everything's okay! */
|
|
return {valid: true, error_number: 0, error: errors[0]};
|
|
}
|
|
|
|
function generate_fen() {
|
|
var empty = 0;
|
|
var fen = '';
|
|
|
|
for (var i = SQUARES.a8; i <= SQUARES.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 !== SQUARES.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'] = fen;
|
|
header['FEN'] = '1';
|
|
} else {
|
|
delete header['SetUp'];
|
|
delete header['FEN'];
|
|
}
|
|
}
|
|
|
|
function get(square) {
|
|
var piece = board[SQUARES[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 SQUARES)) {
|
|
return false;
|
|
}
|
|
|
|
var sq = SQUARES[square];
|
|
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[SQUARES[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 = SQUARES.a8;
|
|
var last_sq = SQUARES.h1;
|
|
var single_square = false;
|
|
|
|
/* do we want legal moves? */
|
|
var legal = (typeof options !== 'undefined' && 'legal' in options) ?
|
|
options.legal : true;
|
|
|
|
/* are we generating moves for a single square? */
|
|
if (typeof options !== 'undefined' && 'square' in options) {
|
|
if (options.square in SQUARES) {
|
|
first_sq = last_sq = SQUARES[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) {
|
|
/* 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 {
|
|
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 ((!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)
|
|
*/
|
|
function move_to_san(move) {
|
|
var output = '';
|
|
|
|
if (move.flags & BITS.KSIDE_CASTLE) {
|
|
output = 'O-O';
|
|
} else if (move.flags & BITS.QSIDE_CASTLE) {
|
|
output = 'O-O-O';
|
|
} else {
|
|
var disambiguator = get_disambiguator(move);
|
|
|
|
if (move.piece !== PAWN) {
|
|
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 = SQUARES.a8; i <= SQUARES.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 = SQUARES.a8; i<= SQUARES.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; }
|
|
|
|
/* k vs. kn .... or .... k vs. kb */
|
|
else if (num_pieces === 3 && (pieces[BISHOP] === 1 ||
|
|
pieces[KNIGHT] === 1)) { return true; }
|
|
|
|
/* kb vs. kb where any number of bishops are all on the same color */
|
|
else if (num_pieces === pieces[BISHOP] + 2) {
|
|
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;
|
|
}
|
|
|
|
/* this function is used to uniquely identify ambiguous moves */
|
|
function get_disambiguator(move) {
|
|
var moves = generate_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);
|
|
}
|
|
/* if the moving piece rests on the same file, use the rank symbol as the
|
|
* disambiguator
|
|
*/
|
|
else if (same_file > 0) {
|
|
return algebraic(from).charAt(1);
|
|
}
|
|
/* else use the file symbol */
|
|
else {
|
|
return algebraic(from).charAt(0);
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function ascii() {
|
|
var s = ' +------------------------+\n';
|
|
for (var i = SQUARES.a8; i <= SQUARES.h1; i++) {
|
|
/* display the rank */
|
|
if (file(i) === 0) {
|
|
s += ' ' + '87654321'[rank(i)] + ' |';
|
|
}
|
|
|
|
/* empty piece */
|
|
if (board[i] == null) {
|
|
s += ' . ';
|
|
} else {
|
|
var piece = board[i].type;
|
|
var color = board[i].color;
|
|
var symbol = (color === WHITE) ?
|
|
piece.toUpperCase() : piece.toLowerCase();
|
|
s += ' ' + symbol + ' ';
|
|
}
|
|
|
|
if ((i + 1) & 0x88) {
|
|
s += '|\n';
|
|
i += 8;
|
|
}
|
|
}
|
|
s += ' +------------------------+\n';
|
|
s += ' a b c d e f g h\n';
|
|
|
|
return s;
|
|
}
|
|
|
|
/*****************************************************************************
|
|
* 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;
|
|
}
|
|
|
|
/* pretty = external move object */
|
|
function make_pretty(ugly_move) {
|
|
var move = clone(ugly_move);
|
|
move.san = move_to_san(move);
|
|
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;
|
|
}
|
|
|
|
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, '');
|
|
}
|
|
|
|
/*****************************************************************************
|
|
* 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 CONSTANTS (is there a better way to do this?)
|
|
**************************************************************************/
|
|
WHITE: WHITE,
|
|
BLACK: BLACK,
|
|
PAWN: PAWN,
|
|
KNIGHT: KNIGHT,
|
|
BISHOP: BISHOP,
|
|
ROOK: ROOK,
|
|
QUEEN: QUEEN,
|
|
KING: KING,
|
|
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 = SQUARES.a8; i <= SQUARES.h1; i++) {
|
|
if (i & 0x88) { i += 7; continue; }
|
|
keys.push(algebraic(i));
|
|
}
|
|
return keys;
|
|
})(),
|
|
FLAGS: FLAGS,
|
|
|
|
/***************************************************************************
|
|
* 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]));
|
|
}
|
|
}
|
|
|
|
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();
|
|
},
|
|
|
|
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 headerrmation */
|
|
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);
|
|
}
|
|
|
|
/* pop all of history onto reversed_history */
|
|
var reversed_history = [];
|
|
while (history.length > 0) {
|
|
reversed_history.push(undo_move());
|
|
}
|
|
|
|
var moves = [];
|
|
var move_string = '';
|
|
var pgn_move_number = 1;
|
|
|
|
/* build the list of moves. a move_string looks like: "3. e3 e6" */
|
|
while (reversed_history.length > 0) {
|
|
var move = reversed_history.pop();
|
|
|
|
/* if the position started with black to move, start PGN with 1. ... */
|
|
if (pgn_move_number === 1 && move.color === 'b') {
|
|
move_string = '1. ...';
|
|
pgn_move_number++;
|
|
} 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 = pgn_move_number + '.';
|
|
pgn_move_number++;
|
|
}
|
|
|
|
move_string = move_string + ' ' + move_to_san(move);
|
|
make_move(move);
|
|
}
|
|
|
|
/* are there any other leftover moves? */
|
|
if (move_string.length) {
|
|
moves.push(move_string);
|
|
}
|
|
|
|
/* is there a result? */
|
|
if (typeof header.Result !== 'undefined') {
|
|
moves.push(header.Result);
|
|
}
|
|
|
|
/* history should be back to what is was before we started generating PGN,
|
|
* so join together moves
|
|
*/
|
|
if (max_width === 0) {
|
|
return result.join('') + moves.join(' ');
|
|
}
|
|
|
|
/* wrap the PGN output at max_width */
|
|
var current_width = 0;
|
|
for (var i = 0; i < moves.length; i++) {
|
|
/* if the current move will push past max_width */
|
|
if (current_width + moves[i].length > max_width && i !== 0) {
|
|
|
|
/* don't end the line with whitespace */
|
|
if (result[result.length - 1] === ' ') {
|
|
result.pop();
|
|
}
|
|
|
|
result.push(newline);
|
|
current_width = 0;
|
|
} else if (i !== 0) {
|
|
result.push(' ');
|
|
current_width++;
|
|
}
|
|
result.push(moves[i]);
|
|
current_width += moves[i].length;
|
|
}
|
|
|
|
return result.join('');
|
|
},
|
|
|
|
load_pgn: function(pgn, options) {
|
|
function mask(str) {
|
|
return str.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
}
|
|
|
|
/* convert a move from Standard Algebraic Notation (SAN) to 0x88
|
|
* coordinates
|
|
*/
|
|
function move_from_san(move) {
|
|
var to, from, flags = BITS.NORMAL, promotion;
|
|
var parse = move.match(/^([NBKRQ])?([abcdefgh12345678][12345678]?)?(x)?([abcdefgh][12345678])(=?[NBRQ])?/);
|
|
if (move.slice(0, 5) === 'O-O-O') {
|
|
from = kings[turn];
|
|
to = from - 2;
|
|
flags = BITS.QSIDE_CASTLE;
|
|
} else if (move.slice(0, 3) === 'O-O') {
|
|
from = kings[turn];
|
|
to = from + 2;
|
|
flags = BITS.KSIDE_CASTLE;
|
|
} else if (parse && parse[1]) {
|
|
// regular moves
|
|
var piece = parse[1].toLowerCase();
|
|
if (parse[3]) {
|
|
// capture
|
|
flags = BITS.CAPTURE;
|
|
}
|
|
to = SQUARES[parse[4]];
|
|
for (var j = 0, len = PIECE_OFFSETS[piece].length; j < len; j++) {
|
|
var offset = PIECE_OFFSETS[piece][j];
|
|
var square = to;
|
|
|
|
while (true) {
|
|
square += offset;
|
|
if (square & 0x88) break;
|
|
|
|
var b = board[square];
|
|
if (b) {
|
|
if (b.color === turn && b.type === piece && (!parse[2] || algebraic(square).indexOf(parse[2]) >= 0)) {
|
|
from = square;
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* break, if knight or king */
|
|
if (piece === 'n' || piece === 'k') break;
|
|
}
|
|
}
|
|
} else if (parse) {
|
|
// pawn move
|
|
if (parse[3]) {
|
|
// capture
|
|
to = SQUARES[parse[4]];
|
|
for (var j = 2; j < 4; j++) {
|
|
var square = to - PAWN_OFFSETS[turn][j];
|
|
if (square & 0x88) continue;
|
|
|
|
if (board[square] != null &&
|
|
board[square].color === turn &&
|
|
algebraic(square)[0] === parse[2]) {
|
|
from = square;
|
|
}
|
|
}
|
|
if (board[to]) {
|
|
flags = BITS.CAPTURE;
|
|
} else {
|
|
flags = BITS.EP_CAPTURE;
|
|
}
|
|
} else {
|
|
// normal move
|
|
to = SQUARES[move.slice(0,2)];
|
|
var c = to - PAWN_OFFSETS[turn][0],
|
|
b = board[c];
|
|
if (b && b.type === PAWN && b.color === turn) {
|
|
from = c;
|
|
} else {
|
|
c = to - PAWN_OFFSETS[turn][1];
|
|
b = board[c];
|
|
if (b && b.type === PAWN && b.color === turn) {
|
|
from = c;
|
|
flags = BITS.BIG_PAWN;
|
|
}
|
|
}
|
|
}
|
|
// promotion?
|
|
if (parse[5]) {
|
|
if(typeof parse[5][1] == 'undefined') {
|
|
promotion = parse[5][0].toLowerCase();
|
|
} else {
|
|
promotion = parse[5][1].toLowerCase();
|
|
}
|
|
}
|
|
}
|
|
if (from >=0 && to >=0 && flags) {
|
|
return build_move(board, from, to, flags, promotion);
|
|
} else if (move.length > 0) {
|
|
/* alert(move); // error in PGN, or in parsing. */
|
|
}
|
|
}
|
|
|
|
function get_move_obj(move) {
|
|
return move_from_san(trim(move));
|
|
}
|
|
|
|
function has_keys(object) {
|
|
var has_keys = false;
|
|
for (var key in object) {
|
|
has_keys = true;
|
|
}
|
|
return has_keys;
|
|
}
|
|
|
|
function parse_pgn_header(header, options) {
|
|
var newline_char = (typeof options === 'object' &&
|
|
typeof options.newline_char === 'string') ?
|
|
options.newline_char : '\r?\n';
|
|
var header_obj = {};
|
|
var headers = header.split(newline_char);
|
|
var key = '';
|
|
var value = '';
|
|
|
|
for (var i = 0; i < headers.length; i++) {
|
|
key = headers[i].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/, '$1');
|
|
value = headers[i].replace(/^\[[A-Za-z]+\s"(.*)"\]$/, '$1');
|
|
if (trim(key).length > 0) {
|
|
header_obj[key] = value;
|
|
}
|
|
}
|
|
|
|
return header_obj;
|
|
}
|
|
|
|
var newline_char = (typeof options === 'object' &&
|
|
typeof options.newline_char === 'string') ?
|
|
options.newline_char : '\r?\n';
|
|
var regex = new RegExp('^(\\[(.|' + mask(newline_char) + ')*\\])' +
|
|
'(' + mask(newline_char) + ')*' +
|
|
'1.(' + mask(newline_char) + '|.)*$', 'g');
|
|
|
|
/* get header part of the PGN file */
|
|
var header_string = pgn.replace(regex, '$1');
|
|
|
|
/* no info part given, begins with moves */
|
|
if (header_string[0] !== '[') {
|
|
header_string = '';
|
|
}
|
|
|
|
reset();
|
|
|
|
/* parse PGN header */
|
|
var headers = parse_pgn_header(header_string, options);
|
|
for (var key in headers) {
|
|
set_header([key, headers[key]]);
|
|
}
|
|
|
|
/* delete header to get the moves */
|
|
var ms = pgn.replace(header_string, '').replace(new RegExp(mask(newline_char), 'g'), ' ');
|
|
|
|
/* delete comments */
|
|
ms = ms.replace(/(\{[^}]+\})+?/g, '');
|
|
|
|
/* delete move numbers */
|
|
ms = ms.replace(/\d+\./g, '');
|
|
|
|
|
|
/* trim and get array of moves */
|
|
var moves = trim(ms).split(new RegExp(/\s+/));
|
|
|
|
/* delete empty entries */
|
|
moves = moves.join(',').replace(/,,+/g, ',').split(',');
|
|
var move = '';
|
|
|
|
for (var half_move = 0; half_move < moves.length - 1; half_move++) {
|
|
move = get_move_obj(moves[half_move]);
|
|
|
|
/* move not possible! (don't clear the board to examine to show the
|
|
* latest valid position)
|
|
*/
|
|
if (move == null) {
|
|
return false;
|
|
} else {
|
|
make_move(move);
|
|
}
|
|
}
|
|
|
|
/* examine last move */
|
|
move = moves[moves.length - 1];
|
|
if (POSSIBLE_RESULTS.indexOf(move) > -1) {
|
|
if (has_keys(header) && typeof header.Result === 'undefined') {
|
|
set_header(['Result', move]);
|
|
}
|
|
}
|
|
else {
|
|
move = get_move_obj(move);
|
|
if (move == null) {
|
|
return false;
|
|
} else {
|
|
make_move(move);
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
header: function() {
|
|
return set_header(arguments);
|
|
},
|
|
|
|
ascii: function() {
|
|
return ascii();
|
|
},
|
|
|
|
turn: function() {
|
|
return turn;
|
|
},
|
|
|
|
move: function(move) {
|
|
/* The move function can be called with in the following parameters:
|
|
*
|
|
* .move('Nxb7') <- where 'move' is a case-sensitive SAN string
|
|
*
|
|
* .move({ from: 'h7', <- where the 'move' is a move object (additional
|
|
* to :'h8', fields are ignored)
|
|
* promotion: 'q',
|
|
* })
|
|
*/
|
|
var move_obj = null;
|
|
var moves = generate_moves();
|
|
|
|
if (typeof move === 'string') {
|
|
/* convert the move string to a move object */
|
|
for (var i = 0, len = moves.length; i < len; i++) {
|
|
if (move === move_to_san(moves[i])) {
|
|
move_obj = moves[i];
|
|
break;
|
|
}
|
|
}
|
|
} else if (typeof move === 'object') {
|
|
/* convert the pretty move object to an ugly move object */
|
|
for (var i = 0, len = moves.length; i < len; i++) {
|
|
if (move.from === algebraic(moves[i].from) &&
|
|
move.to === algebraic(moves[i].to) &&
|
|
(!('promotion' in moves[i]) ||
|
|
move.promotion === moves[i].promotion)) {
|
|
move_obj = moves[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* failed to find move */
|
|
if (!move_obj) {
|
|
return null;
|
|
}
|
|
|
|
/* need to make a copy of move because we can't generate SAN after the
|
|
* move is made
|
|
*/
|
|
var pretty_move = make_pretty(move_obj);
|
|
|
|
make_move(move_obj);
|
|
|
|
return pretty_move;
|
|
},
|
|
|
|
undo: function() {
|
|
var move = undo_move();
|
|
return (move) ? make_pretty(move) : null;
|
|
},
|
|
|
|
clear: function() {
|
|
return clear();
|
|
},
|
|
|
|
put: function(piece, square) {
|
|
return put(piece, square);
|
|
},
|
|
|
|
get: function(square) {
|
|
return get(square);
|
|
},
|
|
|
|
remove: function(square) {
|
|
return remove(square);
|
|
},
|
|
|
|
perft: function(depth) {
|
|
return perft(depth);
|
|
},
|
|
|
|
square_color: function(square) {
|
|
if (square in SQUARES) {
|
|
var sq_0x88 = SQUARES[square];
|
|
return ((rank(sq_0x88) + file(sq_0x88)) % 2 === 0) ? 'light' : 'dark';
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
history: function(options) {
|
|
var reversed_history = [];
|
|
var move_history = [];
|
|
var verbose = (typeof options !== 'undefined' && 'verbose' in options &&
|
|
options.verbose);
|
|
|
|
while (history.length > 0) {
|
|
reversed_history.push(undo_move());
|
|
}
|
|
|
|
while (reversed_history.length > 0) {
|
|
var move = reversed_history.pop();
|
|
if (verbose) {
|
|
move_history.push(make_pretty(move));
|
|
} else {
|
|
move_history.push(move_to_san(move));
|
|
}
|
|
make_move(move);
|
|
}
|
|
|
|
return move_history;
|
|
}
|
|
|
|
};
|
|
};
|
|
|
|
/* export Chess object if using node or any other CommonJS compatible
|
|
* environment */
|
|
if (typeof exports !== 'undefined') exports.Chess = Chess; |