diff --git a/__tests__/chess.test.js b/__tests__/chess.test.js index d68d6a1a..dbfbff91 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"]', @@ -551,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}, @@ -741,11 +767,36 @@ 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 + }, + // 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 + } ]; 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 +829,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 +1194,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 +1357,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..34d474a3 100755 --- a/chess.js +++ b/chess.js @@ -25,7 +25,7 @@ * *----------------------------------------------------------------------------*/ -var Chess = function(fen) { +var Chess = function(fen = '', variant = 'Standard') { 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 = variant.indexOf('960') >= 0 /* 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,27 @@ 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 == 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 + 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 + } + } + if (ok) { + add_move(board, moves, kings[us], castling_to, BITS.KSIDE_CASTLE) + } } } @@ -641,7 +719,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 +733,27 @@ 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 == 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 + 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 + } + } + if (ok) { + add_move(board, moves, kings[us], castling_to, BITS.QSIDE_CASTLE) + } } } } @@ -897,7 +1001,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 +1027,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 (castling_from != castling_to && 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 (castling_from != castling_to && 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 +1127,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 +1147,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 (castling_to != castling_from && board[castling_from].type != KING) { + board[castling_from] = null + } + } else { + board[castling_to] = board[castling_from] + board[castling_from] = null + } } return move @@ -1179,6 +1316,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 +1371,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 +1483,14 @@ var Chess = function(fen) { })(), FLAGS: FLAGS, + is_960: function() { + return is_960 + }, + + set_960: function(v) { + is_960 = v + }, + /*************************************************************************** * PUBLIC API **************************************************************************/ @@ -1615,6 +1827,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