X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=www%2Fjs%2Fchess.js;h=4598758205ed2173b1bf75fe59129ed2b572fed1;hb=HEAD;hp=201b9d80f2f4e468f5a569ed08a4f8382feba320;hpb=5243478f883e4997134e3abc8da57b7680129c89;p=remoteglot diff --git a/www/js/chess.js b/www/js/chess.js index 201b9d8..4598758 100644 --- a/www/js/chess.js +++ b/www/js/chess.js @@ -1,6 +1,5 @@ -'use strict'; /* - * Copyright (c) 2014, Jeff Hlywa (jhlywa@gmail.com) + * Copyright (c) 2017, Jeff Hlywa (jhlywa@gmail.com) * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,12 +27,12 @@ /* minified license below */ -/*! Copyright (c) 2014, Jeff Hlywa (jhlywa@gmail.com) - * Released under the BSD license - * https://github.com/jhlywa/chess.js/blob/master/LICENSE +/* @license + * Copyright (c) 2017, Jeff Hlywa (jhlywa@gmail.com) + * Released under the BSD license + * https://github.com/jhlywa/chess.js/blob/master/LICENSE */ -/** @constructor */ var Chess = function(fen) { /* jshint indent: false */ @@ -69,44 +68,6 @@ var Chess = function(fen) { 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', @@ -147,15 +108,9 @@ var Chess = function(fen) { 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 rooks = {w: [], b: []}; var turn = WHITE; var castling = {w: 0, b: 0}; var ep_square = EMPTY; @@ -194,7 +149,6 @@ var Chess = function(fen) { var tokens = fen.split(/\s+/); var position = tokens[0]; var square = 0; - var valid = SYMBOLS + '12345678/'; if (!validate_fen(fen).valid) { return false; @@ -218,17 +172,63 @@ var Chess = function(fen) { turn = tokens[1]; + rooks = {w: [], b: []}; + if (tokens[2].indexOf('K') > -1) { castling.w |= BITS.KSIDE_CASTLE; + for (var sq = SQUARES.h1; sq >= SQUARES.c1; --sq) { + if (is_rook(board[sq], WHITE)) { + rooks[WHITE].push({square: sq, flag: BITS.KSIDE_CASTLE}); + break; + } + } } if (tokens[2].indexOf('Q') > -1) { castling.w |= BITS.QSIDE_CASTLE; + for (var sq = SQUARES.a1; sq <= SQUARES.g1; ++sq) { + if (is_rook(board[sq], WHITE)) { + rooks[WHITE].push({square: sq, flag: BITS.QSIDE_CASTLE}); + break; + } + } + } + var white_frc_columns = tokens[2].match(/[A-H]/g); + var i, flag; + if (white_frc_columns !== null) { + for (i = 0; i < white_frc_columns.length; ++i) { + var sq = SQUARES.a1 + (white_frc_columns[i].charCodeAt(0) - "A".charCodeAt(0)); + flag = sq < kings[WHITE] ? BITS.QSIDE_CASTLE : BITS.KSIDE_CASTLE; + castling.w |= flag; + rooks[WHITE].push({square: sq, flag: flag}); + } } + if (tokens[2].indexOf('k') > -1) { castling.b |= BITS.KSIDE_CASTLE; + for (var sq = SQUARES.h8; sq >= SQUARES.c8; --sq) { + if (is_rook(board[sq], BLACK)) { + rooks[BLACK].push({square: sq, flag: BITS.KSIDE_CASTLE}); + break; + } + } } if (tokens[2].indexOf('q') > -1) { castling.b |= BITS.QSIDE_CASTLE; + for (var sq = SQUARES.a8; sq <= SQUARES.g8; ++sq) { + if (is_rook(board[sq], BLACK)) { + rooks[BLACK].push({square: sq, flag: BITS.QSIDE_CASTLE}); + break; + } + } + } + var black_frc_columns = tokens[2].match(/[a-h]/g); + if (black_frc_columns !== null) { + for (i = 0; i < black_frc_columns.length; ++i) { + var sq = SQUARES.a8 + (black_frc_columns[i].charCodeAt(0) - "a".charCodeAt(0)); + flag = sq < kings[BLACK] ? BITS.QSIDE_CASTLE : BITS.KSIDE_CASTLE; + castling.b |= flag; + rooks[BLACK].push({square: sq, flag: flag}); + } } ep_square = (tokens[3] === '-') ? EMPTY : SQUARES[tokens[3]]; @@ -240,6 +240,11 @@ var Chess = function(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.', @@ -253,6 +258,7 @@ var Chess = function(fen) { 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? */ @@ -277,7 +283,8 @@ var Chess = function(fen) { } /* 5th criterion: 3th field is a valid castle-string? */ - if( !/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(tokens[2])) { + if( !/^[C-HK]?[A-FQ]?[c-hk]?[a-fq]?$/.test(tokens[2]) && + tokens[2] !== '-') { return {valid: false, error_number: 5, error: errors[5]}; } @@ -318,6 +325,11 @@ var Chess = function(fen) { } } + 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]}; } @@ -356,10 +368,39 @@ var Chess = function(fen) { } 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'; } + var sq; + if (castling[WHITE] & BITS.KSIDE_CASTLE) { + sq = search_rook(board, WHITE, BITS.KSIDE_CASTLE); + if (is_outermost_rook(board, WHITE, BITS.KSIDE_CASTLE, sq)) { + cflags += 'K'; + } else { + cflags += 'ABCDEFGH'.substring(file(sq), file(sq) + 1); + } + } + if (castling[WHITE] & BITS.QSIDE_CASTLE) { + sq = search_rook(board, WHITE, BITS.QSIDE_CASTLE); + if (is_outermost_rook(board, WHITE, BITS.QSIDE_CASTLE, sq)) { + cflags += 'Q'; + } else { + cflags += 'ABCDEFGH'.substring(file(sq), file(sq) + 1); + } + } + if (castling[BLACK] & BITS.KSIDE_CASTLE) { + sq = search_rook(board, BLACK, BITS.KSIDE_CASTLE); + if (is_outermost_rook(board, BLACK, BITS.KSIDE_CASTLE, sq)) { + cflags += 'k'; + } else { + cflags += 'abcdefgh'.substring(file(sq), file(sq) + 1); + } + } + if (castling[BLACK] & BITS.QSIDE_CASTLE) { + sq = search_rook(board, BLACK, BITS.QSIDE_CASTLE); + if (is_outermost_rook(board, BLACK, BITS.QSIDE_CASTLE, sq)) { + cflags += 'q'; + } else { + cflags += 'abcdefgh'.substring(file(sq), file(sq) + 1); + } + } /* do we have an empty castling flag? */ cflags = cflags || '-'; @@ -447,7 +488,7 @@ var Chess = function(fen) { return piece; } - function build_move(board, from, to, flags, promotion) { + function build_move(board, from, to, flags, promotion, rook_sq) { var move = { color: turn, from: from, @@ -461,7 +502,9 @@ var Chess = function(fen) { move.promotion = promotion; } - if (board[to]) { + if (flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) { + move.rook_sq = rook_sq; // remember the position of the rook + } else if (board[to]) { move.captured = board[to].type; } else if (flags & BITS.EP_CAPTURE) { move.captured = PAWN; @@ -470,7 +513,7 @@ var Chess = function(fen) { } function generate_moves(options) { - function add_move(board, moves, from, to, flags) { + function add_move(board, moves, from, to, flags, rook_sq) { /* if pawn promotion */ if (board[from].type === PAWN && (rank(to) === RANK_8 || rank(to) === RANK_1)) { @@ -479,8 +522,33 @@ var Chess = function(fen) { moves.push(build_move(board, from, to, flags, pieces[i])); } } else { - moves.push(build_move(board, from, to, flags)); + moves.push(build_move(board, from, to, flags, undefined, rook_sq)); + } + } + + function check_castle(board, king_from, king_to, rook_from, rook_to, them) { + var sq; + + // Check that no pieces are standing between the king and its destination + // square, and also between the rook and its destination square. + var king_left = Math.min(king_from, king_to); + var king_right = Math.max(king_from, king_to); + var left = Math.min(king_left, Math.min(rook_from, rook_to)); + var right = Math.max(king_right, Math.max(rook_from, rook_to)); + for (sq = left; sq <= right; ++sq) { + if (sq != king_from && sq != rook_from && board[sq]) { + return false; + } + } + + // Check that none of the squares on the king's way are under attack. + for (sq = king_left; sq <= king_right; ++sq) { + if (attacked(them, sq)) { + return false; + } } + + return true; } var moves = []; @@ -523,15 +591,15 @@ var Chess = function(fen) { add_move(board, moves, i, square, BITS.NORMAL); /* double square */ - square = i + PAWN_OFFSETS[us][1]; + 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 (var j = 2; j < 4; j++) { - square = i + PAWN_OFFSETS[us][j]; + for (j = 2; j < 4; j++) { + var square = i + PAWN_OFFSETS[us][j]; if (square & 0x88) continue; if (board[square] != null && @@ -571,36 +639,33 @@ var Chess = function(fen) { 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); + var king_from = kings[us]; + var king_to = us === WHITE ? SQUARES.g1 : SQUARES.g8; + var rook_from = search_rook(board, us, BITS.KSIDE_CASTLE); + var rook_to = king_to - 1; + + if (check_castle(board, king_from, king_to, rook_from, rook_to, them)) { + add_move(board, moves, king_from, king_to, BITS.KSIDE_CASTLE, rook_from); } } /* 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); + var king_from = kings[us]; + var king_to = us === WHITE ? SQUARES.c1 : SQUARES.c8; + var rook_from = search_rook(board, us, BITS.QSIDE_CASTLE); + var rook_to = king_to + 1; + + if (check_castle(board, king_from, king_to, rook_from, rook_to, them)) { + add_move(board, moves, king_from, king_to, BITS.QSIDE_CASTLE, rook_from); } } } + return possibly_filter_moves(moves, us, legal); + } + + function possibly_filter_moves(moves, us, legal) { /* return all pseudo-legal moves (this includes moves that allow the king * to be captured) */ @@ -621,10 +686,52 @@ var Chess = function(fen) { return legal_moves; } + function is_rook(piece, color) { + return (typeof piece !== 'undefined' && piece !== null && + piece.type === ROOK && piece.color == color); + } + + function search_rook(board, us, flag) { + for (var i = 0, len = rooks[us].length; i < len; i++) { + if (flag & rooks[us][i].flag) { + return rooks[us][i].square; + } + } + return null; + } + + function is_outermost_rook(board, us, flag, sq) { + var end_sq; + if (flag == BITS.KSIDE_CASTLE) { + var end_sq = (us == WHITE) ? SQUARES.h1 : SQUARES.h8; + while (++sq <= end_sq) { + if (is_rook(board[sq], us)) { + return false; + } + } + } else { + var end_sq = (us == WHITE) ? SQUARES.a1 : SQUARES.a8; + while (--sq >= end_sq) { + if (is_rook(board[sq], us)) { + return false; + } + } + } + return true; + } + /* 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) { + function move_to_san(move, sloppy) { + var output = ''; if (move.flags & BITS.KSIDE_CASTLE) { @@ -632,7 +739,7 @@ var Chess = function(fen) { } else if (move.flags & BITS.QSIDE_CASTLE) { output = 'O-O-O'; } else { - var disambiguator = get_disambiguator(move); + var disambiguator = get_disambiguator(move, sloppy); if (move.piece !== PAWN) { output += move.piece.toUpperCase() + disambiguator; @@ -665,41 +772,77 @@ var Chess = function(fen) { return output; } + // parses all of the decorators out of a SAN string + function stripped_san(move) { + return move.replace(/=/,'').replace(/[+#]?[?!]*$/,''); + } + 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; } + // Check for attacks by the king. + if (Math.abs(rank(kings[color]) - rank(square)) <= 1 && + Math.abs(file(kings[color]) - file(square)) <= 1) { + return true; + } - /* if empty square or wrong color */ - if (board[i] == null || board[i].color !== color) continue; + // Check for attacks by knights. + for (const offset of PIECE_OFFSETS[KNIGHT]) { + let knight_sq = square + offset; + if (knight_sq & 0x88) continue; - var piece = board[i]; - var difference = i - square; - var index = difference + 119; + if (board[knight_sq] != null && + board[knight_sq].type === KNIGHT && + board[knight_sq].color === color) { + return true; + } + } - 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; + // Check for attacks by pawns. + const p1sq = square - PAWN_OFFSETS[color][2]; + const p2sq = square - PAWN_OFFSETS[color][3]; + if (!(p1sq & 0x88) && + board[p1sq] != null && + board[p1sq].type === PAWN && + board[p1sq].color === color) { + return true; + } + if (!(p2sq & 0x88) && + board[p2sq] != null && + board[p2sq].type === PAWN && + board[p2sq].color === color) { + return true; + } + + // Check for attacks by rooks (where queens count as rooks). + for (const offset of PIECE_OFFSETS[ROOK]) { + let rook_sq = square; + while (true) { + rook_sq += offset; + if (rook_sq & 0x88) break; + + if (board[rook_sq] != null) { + if ((board[rook_sq].type === ROOK || board[rook_sq].type === QUEEN) && + board[rook_sq].color === color) { + return true; } - continue; + break; } + } + } - /* 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; + // And similarly for attacks by bishops (where queens count as bishops). + for (const offset of PIECE_OFFSETS[BISHOP]) { + let bishop_sq = square; + while (true) { + bishop_sq += offset; + if (bishop_sq & 0x88) break; + + if (board[bishop_sq] != null) { + if ((board[bishop_sq].type === BISHOP || board[bishop_sq].type === QUEEN) && + board[bishop_sq].color === color) { + return true; + } + break; } - - if (!blocked) return true; } } @@ -817,7 +960,9 @@ var Chess = function(fen) { push(move); board[move.to] = board[move.from]; - board[move.from] = null; + if (move.from != move.to) { + board[move.from] = null; + } /* if ep capture, remove the captured pawn */ if (move.flags & BITS.EP_CAPTURE) { @@ -840,14 +985,16 @@ var Chess = function(fen) { /* 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; + var castling_from = move.rook_sq; + board[castling_to] = {type: ROOK, color: us}; + if(castling_from !== move.to && castling_from !== castling_to) + 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; + var castling_from = move.rook_sq; + board[castling_to] = {type: ROOK, color: us}; + if(castling_from !== move.to && castling_from !== castling_to) + board[castling_from] = null; } /* turn off castling */ @@ -856,10 +1003,10 @@ var Chess = function(fen) { /* 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; + 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; } } @@ -867,10 +1014,10 @@ var Chess = function(fen) { /* 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; + 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; } } @@ -917,9 +1064,11 @@ var Chess = function(fen) { 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.from != move.to) { + 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}; @@ -937,28 +1086,41 @@ var Chess = function(fen) { 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_to = move.rook_sq; castling_from = move.to - 1; } else if (move.flags & BITS.QSIDE_CASTLE) { - castling_to = move.to - 2; + castling_to = move.rook_sq; castling_from = move.to + 1; } - board[castling_to] = board[castling_from]; - board[castling_from] = null; + board[castling_to] = {type: ROOK, color: us}; + if(castling_from !== move.from && castling_from !== castling_to) + board[castling_from] = null; } return move; } /* this function is used to uniquely identify ambiguous moves */ - function get_disambiguator(move) { - var moves = generate_moves(); - + function get_disambiguator(move, sloppy) { var from = move.from; var to = move.to; var piece = move.piece; + if (piece === 'p' || piece === 'k') { + // Pawn or king moves are never ambiguous. + return ''; + } + + let moves = find_attacking_moves(move.to, piece, move.color); + if (moves.length <= 1) { + // There can be no ambiguity, so don't bother checking legality + // (we assume the move has already been found legal). + return ''; + } + + moves = possibly_filter_moves(moves, move.color, !sloppy); + var ambiguities = 0; var same_rank = 0; var same_file = 0; @@ -1006,6 +1168,42 @@ var Chess = function(fen) { return ''; } + // Find all pseudolegal moves featuring the given piece moving to + // the given square (using symmetry of all non-pawn-or-castle moves, + // we simply generate moves backwards). Does not support pawns. + // Assumes there's not already a piece of our own color + // on the destination square. + function find_attacking_moves(to, piece, us) { + let moves = []; + + function add_move(board, moves, from, to, flags, rook_sq) { + moves.push(build_move(board, from, to, flags, undefined, rook_sq)); + } + for (let offset of PIECE_OFFSETS[piece]) { + var square = to; + + while (true) { + square += offset; + if (square & 0x88) break; + + if (board[square] != null) { + if (board[square].color !== us || board[square].type !== piece) break; + if (board[to] == null) { + add_move(board, moves, square, to, BITS.NORMAL); + } else { + add_move(board, moves, square, to, BITS.CAPTURE); + } + break; + } + + /* break if knight or king */ + if (piece === 'n' || piece === 'k') break; + } + } + + return moves; + } + function ascii() { var s = ' +------------------------+\n'; for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { @@ -1036,6 +1234,61 @@ var Chess = function(fen) { return s; } + // 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+?! + var clean_move = stripped_san(move); + + // if we're using the sloppy parser run a regex to grab piece, to, and from + // this should parse invalid SAN like: Pe2-e4, Rc1c4, Qf3xf7 + if (sloppy) { + 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]; + } + } + + let moves; + let piece_matches = clean_move.match(/^([NBRQK])x?([a-h][1-8])$/); + if (piece_matches) { + // Only look for moves by the given piece to the given square. + let to = SQUARES[piece_matches[2]]; + if (board[to] != null && board[to].color === turn) { + // Cannot capture our own piece. + return null; + } + moves = find_attacking_moves(to, piece_matches[1].toLowerCase(), turn); + // Legal moves only. + moves = possibly_filter_moves(moves, turn, true); + } else { + // Fallback (also used for pawns): Any (legal) moves. + moves = generate_moves(); + } + + for (var i = 0, len = moves.length; i < len; i++) { + // try the strict parser first, then the sloppy parser if requested + // by the user + if ((clean_move === stripped_san(move_to_san(moves[i]))) || + (sloppy && clean_move === stripped_san(move_to_san(moves[i], true)))) { + return moves[i]; + } else { + if (matches && + (!piece || piece.toLowerCase() == moves[i].piece) && + SQUARES[from] == moves[i].from && + SQUARES[to] == moves[i].to && + (!promotion || promotion.toLowerCase() == moves[i].promotion)) { + return moves[i]; + } + } + } + + return null; + } + + /***************************************************************************** * UTILITY FUNCTIONS ****************************************************************************/ @@ -1063,7 +1316,7 @@ var Chess = function(fen) { /* pretty = external move object */ function make_pretty(ugly_move) { var move = clone(ugly_move); - move.san = move_to_san(move); + move.san = move_to_san(move, false); move.to = algebraic(move.to); move.from = algebraic(move.from); @@ -1156,9 +1409,9 @@ var Chess = function(fen) { return load(fen); }, - reset: function() { - return reset(); - }, + // reset: function() { + // return reset(); + // }, moves: function(options) { /* The internal representation of a chess move is in 0x88 format, and @@ -1179,39 +1432,39 @@ var Chess = function(fen) { options.verbose) { moves.push(make_pretty(ugly_moves[i])); } else { - moves.push(move_to_san(ugly_moves[i])); + moves.push(move_to_san(ugly_moves[i], false)); } } return moves; }, - in_check: function() { - return in_check(); - }, + // in_check: function() { + // return in_check(); + // }, - in_checkmate: function() { - return in_checkmate(); - }, + // in_checkmate: function() { + // return in_checkmate(); + // }, - in_stalemate: function() { - return in_stalemate(); - }, + // in_stalemate: function() { + // return in_stalemate(); + // }, - in_draw: function() { - return half_moves >= 100 || - in_stalemate() || - insufficient_material() || - in_threefold_repetition(); - }, + // in_draw: function() { + // return half_moves >= 100 || + // in_stalemate() || + // insufficient_material() || + // in_threefold_repetition(); + // }, - insufficient_material: function() { - return insufficient_material(); - }, + // insufficient_material: function() { + // return insufficient_material(); + // }, - in_threefold_repetition: function() { - return in_threefold_repetition(); - }, + // in_threefold_repetition: function() { + // return in_threefold_repetition(); + // }, game_over: function() { return half_moves >= 100 || @@ -1221,248 +1474,269 @@ var Chess = function(fen) { in_threefold_repetition(); }, - validate_fen: function(fen) { - return validate_fen(fen); - }, + // 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: "
" }) - */ - 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(/\\/g, '\\'); - } - - /* convert a move from Standard Algebraic Notation (SAN) to 0x88 - * coordinates - */ - function move_from_san(move) { - var moves = generate_moves(); - for (var i = 0, len = moves.length; i < len; i++) { - /* strip off any trailing move decorations: e.g Nf3+?! */ - if (move.replace(/[+#?!=]+$/,'') == - move_to_san(moves[i]).replace(/[+#?!=]+$/,'')) { - return moves[i]; - } - } - return null; - } - - 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(new RegExp(mask(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(); - }, + // board: function() { + // var output = [], + // row = []; + + // for (var i = SQUARES.a8; i <= SQUARES.h1; i++) { + // if (board[i] == null) { + // row.push(null) + // } else { + // row.push({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: "
" }) + // */ + // 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 = ''; + + // /* 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 (!history.length && move.color === 'b') { + // move_string = 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 = move_number + '.'; + // } + + // move_string = move_string + ' ' + move_to_san(move, false); + // 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) { + // // allow the user to specify the sloppy move parser to work around over + // // disambiguation bugs in Fritz and Chessbase + // var sloppy = (typeof options !== 'undefined' && 'sloppy' in options) ? + // options.sloppy : false; + + // function mask(str) { + // return str.replace(/\\/g, '\\'); + // } + + // function has_keys(object) { + // for (var key in object) { + // return true; + // } + // return false; + // } + + // 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(new RegExp(mask(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]]); + // } + + // /* load the starting position indicated by [Setup '1'] and + // * [FEN position] */ + // if (headers['SetUp'] === '1') { + // if (!(('FEN' in headers) && load(headers['FEN'], true ))) { // second argument to load: don't clear the headers + // return false; + // } + // } + + // /* 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 recursive annotation variations */ + // var rav_regex = /(\([^\(\)]+\))+?/g + // while (rav_regex.test(ms)) { + // ms = ms.replace(rav_regex, ''); + // } + + // /* delete move numbers */ + // ms = ms.replace(/\d+\.(\.\.)?/g, ''); + + // /* delete ... indicating black to move */ + // ms = ms.replace(/\.\.\./g, ''); + + // /* delete numeric annotation glyphs */ + // 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 = move_from_san(moves[half_move], sloppy); + + // /* 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 = move_from_san(move, sloppy); + // 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) { + move: function(move, options) { /* The move function can be called with in the following parameters: * * .move('Nxb7') <- where 'move' is a case-sensitive SAN string @@ -1472,18 +1746,19 @@ var Chess = function(fen) { * promotion: 'q', * }) */ + + // allow the user to specify the sloppy move parser to work around over + // disambiguation bugs in Fritz and Chessbase + var sloppy = (typeof options !== 'undefined' && 'sloppy' in options) ? + options.sloppy : false; + 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; - } - } + move_obj = move_from_san(move, sloppy); } else if (typeof move === 'object') { + var moves = generate_moves(); + /* 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) && @@ -1520,30 +1795,30 @@ var Chess = function(fen) { return clear(); }, - put: function(piece, square) { - return put(piece, square); - }, + // put: function(piece, square) { + // return put(piece, square); + // }, - get: function(square) { - return get(square); - }, + // get: function(square) { + // return get(square); + // }, - remove: function(square) { - return remove(square); - }, + // remove: function(square) { + // return remove(square); + // }, - perft: function(depth) { - return perft(depth); - }, + // 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'; - } + // 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; - }, + // return null; + // }, history: function(options) { var reversed_history = []; @@ -1570,3 +1845,9 @@ var Chess = function(fen) { }; }; + +/* export Chess object if using node or any other CommonJS compatible + * environment */ +if (typeof exports !== 'undefined') exports.Chess = Chess; +/* export Chess object for any RequireJS compatible environment */ +if (typeof define !== 'undefined') define( function () { return Chess; });