]> git.sesse.net Git - remoteglot/blobdiff - www/js/chess.js
Handle streaming PGNs, like from Lichess (although this might break non-streaming...
[remoteglot] / www / js / chess.js
index 91f6c1dda7af308f342e08e05095ca26238566c7..4598758205ed2173b1bf75fe59129ed2b572fed1 100644 (file)
@@ -128,11 +128,7 @@ var Chess = function(fen) {
     load(fen);
   }
 
-  function clear(keep_headers) {
-    if (typeof keep_headers === 'undefined') {
-      keep_headers = false;
-    }
-
+  function clear() {
     board = new Array(128);
     kings = {w: EMPTY, b: EMPTY};
     turn = WHITE;
@@ -141,7 +137,7 @@ var Chess = function(fen) {
     half_moves = 0;
     move_number = 1;
     history = [];
-    if (!keep_headers) header = {};
+    header = {};
     update_setup(generate_fen());
   }
 
@@ -149,11 +145,7 @@ var Chess = function(fen) {
     load(DEFAULT_POSITION);
   }
 
-  function load(fen, keep_headers) {
-    if (typeof keep_headers === 'undefined') {
-      keep_headers = false;
-    }
-
+  function load(fen) {
     var tokens = fen.split(/\s+/);
     var position = tokens[0];
     var square = 0;
@@ -162,7 +154,7 @@ var Chess = function(fen) {
       return false;
     }
 
-    clear(keep_headers);
+    clear();
 
     for (var i = 0; i < position.length; i++) {
       var piece = position.charAt(i);
@@ -1176,10 +1168,10 @@ var Chess = function(fen) {
     return '';
   }
 
-  // Find all pseudolegal moves featuring the given piece attacking
+  // 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 kings
-  // or pawns. Assumes there's // not already a piece of our own color
+  // 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 = [];
@@ -1195,7 +1187,7 @@ var Chess = function(fen) {
         if (square & 0x88) break;
 
         if (board[square] != null) {
-          if (board[square].color !== us) break;
+          if (board[square].color !== us || board[square].type !== piece) break;
           if (board[to] == null) {
             add_move(board, moves, square, to, BITS.NORMAL);
           } else {
@@ -1204,8 +1196,8 @@ var Chess = function(fen) {
           break;
         }
 
-        /* break if knight */
-        if (piece === 'n') break;
+        /* break if knight or king */
+        if (piece === 'n' || piece === 'k') break;
       }
     }
 
@@ -1259,7 +1251,23 @@ var Chess = function(fen) {
       }
     }
 
-    var moves = generate_moves();
+    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
@@ -1401,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
@@ -1431,32 +1439,32 @@ var Chess = function(fen) {
       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 ||
@@ -1466,263 +1474,263 @@ 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();
     },
 
-    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: "<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 = '';
-
-      /* 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();
-    },
+    // 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: "<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 = '';
+
+    //   /* 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;
@@ -1787,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 = [];