From 7f5871e91c0fc10eb247870e8b04ec49db0d0b89 Mon Sep 17 00:00:00 2001 From: Atin M Date: Tue, 15 Dec 2020 22:55:05 -0500 Subject: [PATCH 1/4] Added Chess 960 support --- __tests__/chess.test.js | 62 ++++++++- chess.js | 280 ++++++++++++++++++++++++++++++++++------ 2 files changed, 301 insertions(+), 41 deletions(-) diff --git a/__tests__/chess.test.js b/__tests__/chess.test.js index d68d6a1a..3fb003b2 100644 --- a/__tests__/chess.test.js +++ b/__tests__/chess.test.js @@ -377,6 +377,8 @@ describe("FEN", function() { {fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', should_pass: true}, {fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1', should_pass: true}, {fen: '1nbqkbn1/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/1NBQKBN1 b - - 1 2', should_pass: true}, + {fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1', should_pass: true}, /* Chess960 */ + {fen: 'bnrqkbnr/pppppppp/8/8/8/8/PPPPPPPP/BNRQKBNR w HChc - 0 1', should_pass: true}, /* Chess960 */ /* incomplete FEN string */ {fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBN w KQkq - 0 1', should_pass: false}, @@ -442,7 +444,32 @@ describe("PGN", function() { max_width:20, pgn: '[SetUp "1"]\n[FEN "r1bqk1nr/pppp1ppp/2n5/4p3/1bB1P3/2P2N2/P2P1PPP/RNBQK2R b KQkq - 0 1"]\n\n1. ... Ba5 2. O-O d6\n3. d4', starting_position: 'r1bqk1nr/pppp1ppp/2n5/4p3/1bB1P3/2P2N2/P2P1PPP/RNBQK2R b KQkq - 0 1', - fen: 'r1bqk1nr/ppp2ppp/2np4/b3p3/2BPP3/2P2N2/P4PPP/RNBQ1RK1 b kq d3 0 3'} + fen: 'r1bqk1nr/ppp2ppp/2np4/b3p3/2BPP3/2P2N2/P4PPP/RNBQ1RK1 b kq d3 0 3'}, + {moves: ['d4', 'g6', 'Nab3', 'Nab6', 'e4', 'd5', 'exd5', 'Bb5+', 'Be2', 'Bxe2+', 'Nxe2', 'Nxd5', 'g3', 'c6', // testing chess 960 + 'c4', 'Nf6', 'Qf3', 'Nd6', 'Rc1', 'O-O', 'O-O', 'Qg7', 'Nc5', 'Re8', 'b3', 'g5', 'Bb4', 'Qg6', + 'Nc3', 'Rc8', 'Nd3', 'Qf5', 'Qe2', 'h5', 'Ne5', 'Nde4', 'f3', 'Nxc3', 'Bxc3', 'Qe6', 'f4', + 'Qf5', 'fxg5', 'Qxg5', 'Bd2', 'Qg7', 'Rf5', 'Nh7', 'Rxf7'], + header: ["Event", "GM Blitz Battle Chp - chess 960", + "Site", "Chess.com", + "Date", "2016.08.23", + "Round", "?", + "White", "MagnusCarlsen", + "Black", "Grischuk", + "Result", "1-0", + "ECO", "A40", + "WhiteElo", "2840", + "BlackElo", "2816", + "Variant", "chess 960", + "SetUp", "1", + "FEN", "nrnbbkrq/pppppppp/8/8/8/8/PPPPPPPP/NRNBBKRQ w GBgb - 0 1", + "PlyCount", "49", + "EventDate", "2016.??.??", + "TimeControl", "60+1"], + max_width: 125, + starting_position: 'nrnbbkrq/pppppppp/8/8/8/8/PPPPPPPP/NRNBBKRQ w GBgb - 0 1', + pgn: '[SetUp "1"]\n[FEN "nrnbbkrq/pppppppp/8/8/8/8/PPPPPPPP/NRNBBKRQ w GBgb - 0 1"]\n[Event "GM Blitz Battle Chp - chess 960"]\n[Site "Chess.com"]\n[Date "2016.08.23"]\n[Round "?"]\n[White "MagnusCarlsen"]\n[Black "Grischuk"]\n[Result "1-0"]\n[ECO "A40"]\n[WhiteElo "2840"]\n[BlackElo "2816"]\n[Variant "chess 960"]\n[PlyCount "49"]\n[EventDate "2016.??.??"]\n[TimeControl "60+1"]\n\n1. d4 g6 2. Nab3 Nab6 3. e4 d5 4. exd5 Bb5+ 5. Be2 Bxe2+ 6. Nxe2 Nxd5 7. g3 c6 8. c4 Nf6 9. Qf3 Nd6 10. Rc1 O-O 11. O-O Qg7\n12. Nc5 Re8 13. b3 g5 14. Bb4 Qg6 15. Nc3 Rc8 16. Nd3 Qf5 17. Qe2 h5 18. Ne5 Nde4 19. f3 Nxc3 20. Bxc3 Qe6 21. f4 Qf5\n22. fxg5 Qxg5 23. Bd2 Qg7 24. Rf5 Nh7 25. Rxf7 1-0', + fen: '2rbr1k1/pp2pRqn/2p5/4N2p/2PP4/1P4P1/P2BQ2P/2R3K1 b - - 0 25' + } ]; positions.forEach(function(position, i) { @@ -472,7 +499,6 @@ describe("PGN", function() { describe("Load PGN", function() { - var chess = new Chess(); var tests = [ {pgn: [ '[Event "Reykjavik WCh"]', @@ -741,11 +767,26 @@ describe("Load PGN", function() { expect: true, sloppy: true }, + // sloppy parse Chess 960 game (with castling) + {pgn: ['[Variant "chess 960"]', + '[SetUp "1"]', + '[FEN "nqrkbbrn/pppppppp/8/8/8/8/PPPPPPPP/NQRKBBRN w GCgc - 0 1"]', + '', + '1. c4 Ng6 2. f4 c5 3. g3 e6 4. Bg2 Nb6 5. Nb3 d5 6. cxd5 exd5 7. Bf2 d4 8. O-O', + 'Be7 9. Qf5 Qd6 10. Na5 Rc7 11. b4 c4 12. Nxb7+ Rxb7 13. Bxb7 Bd7 14. Qe4 Bf6', + '15. Qg2 Re8 16. e4 dxe3 17. dxe3 Qxb4 18. Bc6 c3 19. Rfd1 Kc7 20. Bxd7 Nxd7 21.', + 'g4 h6 22. Ng3 Nb6 23. Nh5 Nh4 24. Bxh4 Bxh4 25. Qd2 Kb7 26. Qxc3 Qe4 27. Qc7+', + 'Ka8 28. Qc6+ Qxc6 29. Rxc6 Kb7 30. Nxg7 1-0'], + fen: '4r3/pk3pN1/1nR4p/8/5PPb/4P3/P6P/3R2K1 b - - 0 30', + expect: true, + sloppy :true + } ]; var newline_chars = ['\n', '
', '\r\n', 'BLAH']; tests.forEach(function(t, i) { + var chess = new Chess(); newline_chars.forEach(function(newline, j) { it(i + String.fromCharCode(97 + j), function() { var sloppy = t.sloppy || false; @@ -778,6 +819,7 @@ describe("Load PGN", function() { // special case dirty file containing a mix of \n and \r\n it('dirty pgn', function() { + var chess = new Chess(); var pgn = '[Event "Reykjavik WCh"]\n' + '[Site "Reykjavik WCh"]\n' + @@ -1142,7 +1184,19 @@ describe("Make Move", function() { legal: true, sloppy: true, move: 'Ne7', - next: 'r2qkb1r/ppp1nppp/2n5/1B2pQ2/4P3/8/PPP2PPP/RNB1K2R w KQkq - 4 8'} + next: 'r2qkb1r/ppp1nppp/2n5/1B2pQ2/4P3/8/PPP2PPP/RNB1K2R w KQkq - 4 8'}, + // handle king side chess 960 castling + {fen: '1qrkbbr1/pp3ppp/1n4n1/2p5/3p1P2/1N4P1/PP1PPBBP/1QRK2RK w GCgc - 0 7', + legal: true, + move: 'O-O', + next: '1qrkbbr1/pp3ppp/1n4n1/2p5/3p1P2/1N4P1/PP1PPBBP/1QR2RK1 b gc - 1 7', + captured: 'r'}, // some chess 960 castling looks like we captured our own rook! + // handle queen side chess 960 castling + {fen: 'r1k1b1r1/pp3ppp/2n1p1n1/2bp3N/8/2NBP3/PPP2PPP/R1K1B1R1 w GAga - 0 8', + legal: true, + move: 'O-O-O', + next: 'r1k1b1r1/pp3ppp/2n1p1n1/2bp3N/8/2NBP3/PPP2PPP/2KRB1R1 b ga - 1 8', + captured: 'k'} // some chess 960 castling looks like we captured our own king! ]; positions.forEach(function(position) { @@ -1293,6 +1347,8 @@ describe("Validate FEN", function() { {fen: 'r3k2r/8/p4p2/3p2p1/4b3/2R2PP1/P6P/4R1K1 b kq - 0 27', error_number: 0}, {fen: 'r1rb2k1/5ppp/pqp5/3pPb2/QB1P4/2R2N2/P4PPP/2R3K1 b - - 7 23', error_number: 0}, {fen: '3r1r2/3P2pk/1p1R3p/1Bp2p2/6q1/4Q3/PP3P1P/7K w - - 4 30', error_number: 0}, + {fen: 'r1k1b1r1/pp3ppp/2n1p1n1/2bp3N/8/2NBP3/PPP2PPP/R1K1B1R1 w GAga - 0 8', error_number: 0}, + {fen: '1qrkbbr1/pp3ppp/1n4n1/2p5/3p1P2/1N4P1/PP1PPBBP/1QR2RK1 b gc - 1 7', error_number: 0} ]; positions.forEach(function(position) { diff --git a/chess.js b/chess.js index ae7e73ba..91476be5 100755 --- a/chess.js +++ b/chess.js @@ -25,7 +25,7 @@ * *----------------------------------------------------------------------------*/ -var Chess = function(fen) { +var Chess = function(fen = '', as_960 = false) { var BLACK = 'b' var WHITE = 'w' @@ -149,6 +149,10 @@ var Chess = function(fen) { { square: SQUARES.h8, flag: BITS.KSIDE_CASTLE } ] } + var QSIDE_CASTLE_INDEX = 0 + var KSIDE_CASTLE_INDEX = 1 + + var KNIGHT_TABLE_960 = [[0, 0], [0, 1], [0, 2], [0, 3], [1, 1], [1, 2], [1, 3], [2, 2], [2, 3], [3, 3]] var board = new Array(128) var kings = { w: EMPTY, b: EMPTY } @@ -160,12 +164,13 @@ var Chess = function(fen) { var history = [] var header = {} var comments = {} + var is_960 = as_960 /* if the user passes in a fen string, load it, else default to - * starting position + * starting position based on the is_960 setting */ - if (typeof fen === 'undefined') { - load(DEFAULT_POSITION) + if (typeof fen === 'undefined' || fen === null || fen === '') { + load(is_960 ? chess960_fen(Math.floor(Math.random() * 960)) : DEFAULT_POSITION) } else { load(fen) } @@ -242,17 +247,47 @@ var Chess = function(fen) { 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 + if (tokens[2] !== "-") { + for (var ch of tokens[2]) { + var i = Array.from("KQkq").indexOf(ch) + if (i > -1) { + /* Standard FEN castling */ + switch (ch) { + case 'K': + castling.w |= BITS.KSIDE_CASTLE + break + case 'Q': + castling.w |= BITS.QSIDE_CASTLE + break + case 'k': + castling.b |= BITS.KSIDE_CASTLE + break + case 'q': + castling.b |= BITS.QSIDE_CASTLE + break + } + } else if (('A' <= ch) && (ch <= 'H')) { + /* Shredder-FEN castling for White */ + is_960 = true + if (SQUARES[ch.toLowerCase() + '1'] > kings.w) { + castling.w |= BITS.KSIDE_CASTLE + ROOKS[WHITE][KSIDE_CASTLE_INDEX].square = SQUARES[ch.toLowerCase() + '1'] + } else { + castling.w |= BITS.QSIDE_CASTLE + ROOKS[WHITE][QSIDE_CASTLE_INDEX].square = SQUARES[ch.toLowerCase() + '1'] + } + } else if (('a' <= ch) && (ch <= 'h')) { + /* Shredder-FEN castling for Black */ + is_960 = true + if (SQUARES[ch + '8'] > kings.b) { + castling.b |= BITS.KSIDE_CASTLE + ROOKS[BLACK][KSIDE_CASTLE_INDEX].square = SQUARES[ch.toLowerCase() + '8'] + } else { + castling.b |= BITS.QSIDE_CASTLE + ROOKS[BLACK][QSIDE_CASTLE_INDEX].square = SQUARES[ch.toLowerCase() + '8'] + } + } + } } ep_square = tokens[3] === '-' ? EMPTY : SQUARES[tokens[3]] @@ -306,8 +341,8 @@ var Chess = function(fen) { 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])) { + /* 5th criterion: 3th field is a valid castle-string? Shredder-FEN compatible */ + if (!/^([A-H][A-H]?[a-h]?[a-h]?|KQ?k?q?|[A-H][a-h]?[a-h]?|Qk?q?|[a-h][a-h]?|kq?|[a-h]|q|-)$/.test(tokens[2])) { return { valid: false, error_number: 5, error: errors[5] } } @@ -391,22 +426,40 @@ var Chess = function(fen) { } } + /* castling */ 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' + if (!is_960) { + 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' + } + } else { + if (castling[WHITE] & BITS.KSIDE_CASTLE) { + cflags += squaretofile(ROOKS[WHITE][KSIDE_CASTLE_INDEX].square).toUpperCase() + } + if (castling[WHITE] & BITS.QSIDE_CASTLE) { + cflags += squaretofile(ROOKS[WHITE][QSIDE_CASTLE_INDEX].square).toUpperCase() + } + if (castling[BLACK] & BITS.KSIDE_CASTLE) { + cflags += squaretofile(ROOKS[BLACK][KSIDE_CASTLE_INDEX].square) + } + if (castling[BLACK] & BITS.QSIDE_CASTLE) { + cflags += squaretofile(ROOKS[BLACK][QSIDE_CASTLE_INDEX].square) + } } /* do we have an empty castling flag? */ cflags = cflags || '-' + + /* en-passant square */ var epflags = ep_square === EMPTY ? '-' : algebraic(ep_square) return [fen, turn, cflags, epflags, half_moves, move_number].join(' ') @@ -624,8 +677,12 @@ var Chess = function(fen) { if (castling[us] & BITS.KSIDE_CASTLE) { var castling_from = kings[us] var castling_to = castling_from + 2 + if (is_960) { + castling_to = ((us == WHITE) ? SQUARES.g1 : SQUARES.g8) /* always goes to g1/g8, whether standard or 960 */ + } - if ( + /* make sure all squares from the king square and the king's destination square are empty (except for king or rook for chess 960) and not attacked */ + if (!is_960 && board[castling_from + 1] == null && board[castling_to] == null && !attacked(them, kings[us]) && @@ -633,6 +690,21 @@ var Chess = function(fen) { !attacked(them, castling_to) ) { add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE) + } else if (is_960 && + !attacked(them, kings[us]) && + !attacked(them, castling_to) && + (board[castling_to] == null || board[castling_to].type == ROOK || board[castling_to].type == KING) && + (board[castling_to - 1] == null || board[castling_to - 1].type == KING)) { + var ok = true + for (var i = castling_from; i != castling_to; (castling_to >= castling_from) ? i++ : i--) { + if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { + ok = false + break + } + } + if (ok) { + add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE) + } } } @@ -641,7 +713,12 @@ var Chess = function(fen) { var castling_from = kings[us] var castling_to = castling_from - 2 - if ( + if (is_960) { + castling_to = ((us == WHITE) ? SQUARES.c1 : SQUARES.c8) /* always goes to c1/c8, whether standard or 960 */ + } + + /* make sure all squares from the king square and the king's destination square are empty (except for king or rook for chess 960) and not attacked */ + if (!is_960 && board[castling_from - 1] == null && board[castling_from - 2] == null && board[castling_from - 3] == null && @@ -650,6 +727,21 @@ var Chess = function(fen) { !attacked(them, castling_to) ) { add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE) + } else if (is_960 && + !attacked(them, kings[us]) && + !attacked(them, castling_to) && + (board[castling_to] == null || board[castling_to].type == ROOK || board[castling_to].type == KING) && + (board[castling_to + 1] == null || board[castling_to + 1].type == KING)) { + var ok = true + for (var i = castling_from; i != castling_to; (castling_to <= castling_from) ? i-- : i++) { + if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { + ok = false + break + } + } + if (ok) { + add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE) + } } } } @@ -897,7 +989,9 @@ var Chess = function(fen) { push(move) board[move.to] = board[move.from] - board[move.from] = null + if (move.to != move.from) { // chess 960 might have castling where king doesn't move + board[move.from] = null + } /* if ep capture, remove the captured pawn */ if (move.flags & BITS.EP_CAPTURE) { @@ -921,13 +1015,29 @@ var Chess = function(fen) { 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 + if (is_960) { + castling_from = ROOKS[us][KSIDE_CASTLE_INDEX].square + board[castling_to] = { type: ROOK, color: us } + if (board[castling_from].type != KING) { + board[castling_from] = null + } + } else { + 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 + if (is_960) { + castling_from = ROOKS[us][QSIDE_CASTLE_INDEX].square + board[castling_to] = { type: ROOK, color: us } + if (board[castling_from].type != KING) { + board[castling_from] = null + } + } else { + board[castling_to] = board[castling_from] + board[castling_from] = null + } } /* turn off castling */ @@ -1005,7 +1115,9 @@ var Chess = function(fen) { 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) { // chess 960 might have castling where the king does not move + board[move.to] = null + } if (move.flags & BITS.CAPTURE) { board[move.to] = { type: move.captured, color: them } @@ -1023,14 +1135,27 @@ var Chess = function(fen) { var castling_to, castling_from if (move.flags & BITS.KSIDE_CASTLE) { castling_to = move.to + 1 + if (is_960) { + castling_to = ROOKS[us][KSIDE_CASTLE_INDEX].square + } castling_from = move.to - 1 } else if (move.flags & BITS.QSIDE_CASTLE) { castling_to = move.to - 2 + if (is_960) { + castling_to = ROOKS[us][QSIDE_CASTLE_INDEX].square + } castling_from = move.to + 1 } - board[castling_to] = board[castling_from] - board[castling_from] = null + if (is_960) { + board[castling_to] = { type: ROOK, color: us } + if (board[castling_from].type != KING) { + board[castling_from] = null + } + } else { + board[castling_to] = board[castling_from] + board[castling_from] = null + } } return move @@ -1179,6 +1304,16 @@ var Chess = function(fen) { return 'abcdefgh'.substring(f, f + 1) + '87654321'.substring(r, r + 1) } + function squaretofile(sq) { + f = file(sq) + return 'abcdefgh'.substring(f, f + 1) + } + + function squaretorank(sq) { + r = rank(sq) + return '87654321'.substring(r, r + 1) + } + function swap_color(c) { return c === WHITE ? BLACK : WHITE } @@ -1224,6 +1359,63 @@ var Chess = function(fen) { return str.replace(/^\s+|\s+$/g, '') } + function fldmod(d, n) { + return { q: Math.floor(d/n), m: d%n } + } + + function insert_at_empty(a, item, index) { + for (var i = 0; i < a.length; i++) { + if (a[i] != '?' && i <= index) { + index += 1 + } + } + a[index] = item + return a + } + + /* calculate piece placement using Direct Derivation as per: https://en.wikipedia.org/wiki/Fischer_random_chess_numbering_scheme */ + function backrank(n) { + result = ['?','?','?','?','?','?','?','?'] + var {q, m} = fldmod(n, 4) + result[2*m+1] = 'b' + var {q, m} = fldmod(q, 4) + result[2*m] = 'b' + var {q, m} = fldmod(q, 6) + result = insert_at_empty(result, 'q', m) + knights = KNIGHT_TABLE_960[q] + result = insert_at_empty(result, 'n', knights[0]) + result = insert_at_empty(result, 'n', knights[1]) + result = insert_at_empty(result, 'r', 0) + result = insert_at_empty(result, 'k', 0) + result = insert_at_empty(result, 'r', 0) + return result.join('') + } + + function castle_string_960(setup) { + ch1 = '?' + ch2 = '?' + for (var i = 7; i > -1; i--) { + if (setup[i] === 'r') { + ch1 = String.fromCharCode(i + 'a'.charCodeAt(0)) + break + } + } + for (var i = 0; i < 8; i++) { + if (setup[i] == 'r') { + ch2 = String.fromCharCode(i + 'a'.charCodeAt(0)) + break + } + } + + return ch1.toUpperCase() + ch2.toUpperCase() + ch1 + ch2 + } + + function chess960_fen(i) { + pieces = backrank(i) + return pieces + "/pppppppp/8/8/8/8/PPPPPPPP/" + pieces.toUpperCase() + " w " + + castle_string_960(pieces) + " - 0 1" + } + /***************************************************************************** * DEBUGGING UTILITIES ****************************************************************************/ @@ -1279,6 +1471,14 @@ var Chess = function(fen) { })(), FLAGS: FLAGS, + is_960: function() { + return is_960 + }, + + set_960: function(v) { + is_960 = v + }, + /*************************************************************************** * PUBLIC API **************************************************************************/ @@ -1615,6 +1815,10 @@ var Chess = function(fen) { } } + if (headers['Variant'] && headers['Variant'].includes('960')) { + is_960 = true + } + /* NB: the regexes below that delete move numbers, recursive * annotations, and numeric annotation glyphs may also match * text in comments. To prevent this, we transform comments From dce4d2acce9e6381e73c2b94970f55f26117df3a Mon Sep 17 00:00:00 2001 From: Atin M Date: Wed, 16 Dec 2020 15:59:56 -0500 Subject: [PATCH 2/4] Allow any string to be passed into variant (currently only cares about a string with 960 in it somewhere). In the future, we could support other variants by parsing the variant string more. --- chess.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chess.js b/chess.js index 91476be5..12c015a1 100755 --- a/chess.js +++ b/chess.js @@ -25,7 +25,7 @@ * *----------------------------------------------------------------------------*/ -var Chess = function(fen = '', as_960 = false) { +var Chess = function(fen = '', variant = 'Standard') { var BLACK = 'b' var WHITE = 'w' @@ -164,7 +164,7 @@ var Chess = function(fen = '', as_960 = false) { var history = [] var header = {} var comments = {} - var is_960 = as_960 + var is_960 = variant.indexOf('960') >= 0 /* if the user passes in a fen string, load it, else default to * starting position based on the is_960 setting From 11744b656d38b867dfdfde1115723bf9a3a72a9a Mon Sep 17 00:00:00 2001 From: Atin M Date: Fri, 18 Dec 2020 15:19:26 -0500 Subject: [PATCH 3/4] Handle non-empty squares between king and rook square when castling in 960 --- chess.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/chess.js b/chess.js index 12c015a1..e437f16f 100755 --- a/chess.js +++ b/chess.js @@ -697,7 +697,13 @@ var Chess = function(fen = '', variant = 'Standard') { (board[castling_to - 1] == null || board[castling_to - 1].type == KING)) { var ok = true for (var i = castling_from; i != castling_to; (castling_to >= castling_from) ? i++ : i--) { - if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { + if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { // king's path should be safe and clear + ok = false + break + } + } + for (var i = ROOKS[us][KSIDE_CASTLE_INDEX].square; ok && i >= castling_to - 1; i--) { + if (board[i] != null && board[i].type != ROOK && board[i].type != KING) { // rook's path should be clear of other pieces ok = false break } @@ -734,7 +740,13 @@ var Chess = function(fen = '', variant = 'Standard') { (board[castling_to + 1] == null || board[castling_to + 1].type == KING)) { var ok = true for (var i = castling_from; i != castling_to; (castling_to <= castling_from) ? i-- : i++) { - if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { + if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { // king's path should be safe and clear + ok = false + break + } + } + for (var i = ROOKS[us][QSIDE_CASTLE_INDEX].square; ok && i <= castling_to; i++) { + if (board[i] != null && board[i].type != ROOK && board[i].type != KING) { // rook's path should be clear of other pieces ok = false break } From 6fc99702c657cfea2f68524dd634a0c3e183bd6d Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Thu, 24 Dec 2020 13:45:24 -0500 Subject: [PATCH 4/4] Fix handling of 960 castling where rook does not move --- __tests__/chess.test.js | 14 ++++++++++++-- chess.js | 10 +++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/__tests__/chess.test.js b/__tests__/chess.test.js index 3fb003b2..dbfbff91 100644 --- a/__tests__/chess.test.js +++ b/__tests__/chess.test.js @@ -577,8 +577,8 @@ describe("Load PGN", function() { '17. Kd2 Re8 18. Na3 Na6 19. Qh5 Bf6 20. Qxh1 Bxb2 21. Qh4+ Kd7', '22. Rb1 Bxa3 23. Qa4+'], expect: true}, - /* regression test - broken PGN parser ended up here: - * fen = rnbqk2r/pp1p1ppp/4pn2/1N6/1bPN4/8/PP2PPPP/R1BQKB1R b KQkq - 2 6 */ + // regression test - broken PGN parser ended up here: + // fen = rnbqk2r/pp1p1ppp/4pn2/1N6/1bPN4/8/PP2PPPP/R1BQKB1R b KQkq - 2 6 {pgn: ['1. d4 Nf6 2. c4 e6 3. Nf3 c5 4. Nc3 cxd4 5. Nxd4 Bb4 6. Nb5'], fen: 'rnbqk2r/pp1p1ppp/4pn2/1N6/1bP5/2N5/PP2PPPP/R1BQKB1R b KQkq - 2 6', expect: true}, @@ -780,6 +780,16 @@ describe("Load PGN", function() { fen: '4r3/pk3pN1/1nR4p/8/5PPb/4P3/P6P/3R2K1 b - - 0 30', expect: true, sloppy :true + }, + // sloppy parse Chess 960 game (with castling) + {pgn: ['[Variant "chess 960"]', + '[SetUp "1"]', + '[FEN "bbqr1k1r/p2ppppB/1p6/2p5/2P2n2/1P4N1/P1QPPPPP/B2RK1R1 w GD - 3 9"]', + '', + '9. O-O-O'], + fen: 'bbqr1k1r/p2ppppB/1p6/2p5/2P2n2/1P4N1/P1QPPPPP/B1KR2R1 b - - 4 9', + expect: true, + sloppy :true } ]; diff --git a/chess.js b/chess.js index e437f16f..34d474a3 100755 --- a/chess.js +++ b/chess.js @@ -694,7 +694,7 @@ var Chess = function(fen = '', variant = 'Standard') { !attacked(them, kings[us]) && !attacked(them, castling_to) && (board[castling_to] == null || board[castling_to].type == ROOK || board[castling_to].type == KING) && - (board[castling_to - 1] == null || board[castling_to - 1].type == KING)) { + (board[castling_to - 1] == null || board[castling_to - 1].type == ROOK || board[castling_to - 1].type == KING)) { var ok = true for (var i = castling_from; i != castling_to; (castling_to >= castling_from) ? i++ : i--) { if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { // king's path should be safe and clear @@ -737,7 +737,7 @@ var Chess = function(fen = '', variant = 'Standard') { !attacked(them, kings[us]) && !attacked(them, castling_to) && (board[castling_to] == null || board[castling_to].type == ROOK || board[castling_to].type == KING) && - (board[castling_to + 1] == null || board[castling_to + 1].type == KING)) { + (board[castling_to + 1] == null || board[castling_to + 1].type == ROOK || board[castling_to + 1].type == KING)) { var ok = true for (var i = castling_from; i != castling_to; (castling_to <= castling_from) ? i-- : i++) { if ((board[i] != null && board[i].type != ROOK && board[i].type != KING) || attacked(them, i)) { // king's path should be safe and clear @@ -1030,7 +1030,7 @@ var Chess = function(fen = '', variant = 'Standard') { if (is_960) { castling_from = ROOKS[us][KSIDE_CASTLE_INDEX].square board[castling_to] = { type: ROOK, color: us } - if (board[castling_from].type != KING) { + if (castling_from != castling_to && board[castling_from].type != KING) { board[castling_from] = null } } else { @@ -1043,7 +1043,7 @@ var Chess = function(fen = '', variant = 'Standard') { if (is_960) { castling_from = ROOKS[us][QSIDE_CASTLE_INDEX].square board[castling_to] = { type: ROOK, color: us } - if (board[castling_from].type != KING) { + if (castling_from != castling_to && board[castling_from].type != KING) { board[castling_from] = null } } else { @@ -1161,7 +1161,7 @@ var Chess = function(fen = '', variant = 'Standard') { if (is_960) { board[castling_to] = { type: ROOK, color: us } - if (board[castling_from].type != KING) { + if (castling_to != castling_from && board[castling_from].type != KING) { board[castling_from] = null } } else {