Upgrade Chess.js to a version that supports Chess960 castling.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 14 Jan 2018 10:38:04 +0000 (11:38 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 14 Jan 2018 10:38:04 +0000 (11:38 +0100)
www/index.html
www/js/chess.js

index 9631994..2c113e7 100644 (file)
@@ -78,7 +78,7 @@
 <!-- For faster development -->
 <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
 <script type="text/javascript" src="js/chessboard-0.3.0.min.js"></script>
-<script type="text/javascript" src="js/chess.min.js"></script>
+<script type="text/javascript" src="js/chess.js"></script>
 <script type="text/javascript" src="js/json_delta.js"></script>
 <script type="text/javascript" src="js/jquery.sparkline.js"></script>
 <script type="text/javascript" src="js/remoteglot.js"></script>
index 079d7d7..569d7c8 100644 (file)
@@ -1,6 +1,5 @@
-'use strict';
 /*
- * Copyright (c) 2015, 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
@@ -29,7 +28,7 @@
 /* minified license below  */
 
 /* @license
- * Copyright (c) 2015, Jeff Hlywa (jhlywa@gmail.com)
+ * Copyright (c) 2017, Jeff Hlywa (jhlywa@gmail.com)
  * Released under the BSD license
  * https://github.com/jhlywa/chess.js/blob/master/LICENSE
  */
@@ -147,15 +146,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;
@@ -173,7 +166,11 @@ var Chess = function(fen) {
     load(fen);
   }
 
-  function clear() {
+  function clear(keep_headers) {
+    if (typeof keep_headers === 'undefined') {
+      keep_headers = false;
+    }
+
     board = new Array(128);
     kings = {w: EMPTY, b: EMPTY};
     turn = WHITE;
@@ -182,7 +179,7 @@ var Chess = function(fen) {
     half_moves = 0;
     move_number = 1;
     history = [];
-    header = {};
+    if (!keep_headers) header = {};
     update_setup(generate_fen());
   }
 
@@ -190,7 +187,11 @@ var Chess = function(fen) {
     load(DEFAULT_POSITION);
   }
 
-  function load(fen) {
+  function load(fen, keep_headers) {
+    if (typeof keep_headers === 'undefined') {
+      keep_headers = false;
+    }
+
     var tokens = fen.split(/\s+/);
     var position = tokens[0];
     var square = 0;
@@ -199,7 +200,7 @@ var Chess = function(fen) {
       return false;
     }
 
-    clear();
+    clear(keep_headers);
 
     for (var i = 0; i < position.length; i++) {
       var piece = position.charAt(i);
@@ -217,17 +218,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[0].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.a1 + (black_frc_columns[0].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]];
@@ -239,6 +286,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.',
@@ -252,6 +304,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? */
@@ -276,7 +329,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]};
     }
 
@@ -317,6 +371,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]};
   }
@@ -355,10 +414,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 || '-';
@@ -446,7 +534,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,
@@ -460,7 +548,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;
@@ -469,7 +559,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)) {
@@ -478,10 +568,35 @@ 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 = [];
     var us = turn;
     var them = swap_color(us);
@@ -570,32 +685,25 @@ 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);
         }
       }
     }
@@ -620,10 +728,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) {
@@ -631,7 +781,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;
@@ -664,6 +814,11 @@ 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 */
@@ -813,6 +968,7 @@ var Chess = function(fen) {
   function make_move(move) {
     var us = turn;
     var them = swap_color(us);
+    var old_to = board[move.to];
     push(move);
 
     board[move.to] = board[move.from];
@@ -839,14 +995,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] = old_to===null ? board[castling_from] : old_to;
+        if(castling_from !== move.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] = old_to===null ? board[castling_from] : old_to;
+        if(castling_from !== move.to)
+          board[castling_from] = null;
       }
 
       /* turn off castling */
@@ -855,10 +1013,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;
         }
       }
@@ -866,10 +1024,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;
         }
       }
@@ -916,6 +1074,8 @@ var Chess = function(fen) {
     var us = turn;
     var them = swap_color(turn);
 
+    var old_from = board[move.from];
+
     board[move.from] = board[move.to];
     board[move.from].type = move.piece;  // to undo any promotions
     board[move.to] = null;
@@ -936,23 +1096,24 @@ 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] = old_from===null ? board[castling_from] : old_from;
+      if(castling_from !== move.from)
+        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 moves = generate_moves({legal: !sloppy});
 
     var from = move.from;
     var to = move.to;
@@ -1035,6 +1196,45 @@ 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];
+      }
+    }
+
+    var 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
    ****************************************************************************/
@@ -1062,7 +1262,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);
 
@@ -1178,7 +1378,7 @@ 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));
         }
       }
 
@@ -1228,6 +1428,26 @@ var Chess = function(fen) {
       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 />" })
@@ -1262,26 +1482,23 @@ var Chess = function(fen) {
 
       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++;
+        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 = pgn_move_number + '.';
-          pgn_move_number++;
+          move_string = move_number + '.';
         }
 
-        move_string = move_string + ' ' + move_to_san(move);
+        move_string = move_string + ' ' + move_to_san(move, false);
         make_move(move);
       }
 
@@ -1327,36 +1544,20 @@ var Chess = function(fen) {
     },
 
     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, '\\');
       }
 
-      /* convert a move from Standard Algebraic Notation (SAN) to 0x88
-       * coordinates
-      */
-      function move_from_san(move) {
-        /* strip off any move decorations: e.g Nf3+?! */
-        var move_replaced = move.replace(/=/,'').replace(/[+#]?[?!]*$/,'');
-        var moves = generate_moves();
-        for (var i = 0, len = moves.length; i < len; i++) {
-          if (move_replaced ===
-              move_to_san(moves[i]).replace(/=/,'').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 true;
         }
-        return has_keys;
+        return false;
       }
 
       function parse_pgn_header(header, options) {
@@ -1382,9 +1583,9 @@ var Chess = function(fen) {
       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');
+      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');
@@ -1394,7 +1595,7 @@ var Chess = function(fen) {
         header_string = '';
       }
 
-     reset();
+      reset();
 
       /* parse PGN header */
       var headers = parse_pgn_header(header_string, options);
@@ -1405,7 +1606,7 @@ var Chess = function(fen) {
       /* load the starting position indicated by [Setup '1'] and
       * [FEN position] */
       if (headers['SetUp'] === '1') {
-          if (!(('FEN' in headers) && load(headers['FEN']))) {
+          if (!(('FEN' in headers) && load(headers['FEN'], true ))) { // second argument to load: don't clear the headers
             return false;
           }
       }
@@ -1423,11 +1624,14 @@ var Chess = function(fen) {
       }
 
       /* delete move numbers */
-      ms = ms.replace(/\d+\./g, '');
+      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+/));
 
@@ -1436,7 +1640,7 @@ var Chess = function(fen) {
       var move = '';
 
       for (var half_move = 0; half_move < moves.length - 1; half_move++) {
-        move = get_move_obj(moves[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)
@@ -1456,7 +1660,7 @@ var Chess = function(fen) {
         }
       }
       else {
-        move = get_move_obj(move);
+        move = move_from_san(move, sloppy);
         if (move == null) {
           return false;
         } else {
@@ -1478,7 +1682,7 @@ var Chess = function(fen) {
       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
@@ -1488,21 +1692,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 */
-        /* strip off any move decorations: e.g Nf3+?! */
-        var move_replaced = move.replace(/=/,'').replace(/[+#]?[?!]*$/,'');
-        for (var i = 0, len = moves.length; i < len; i++) {
-          if (move_replaced ===
-              move_to_san(moves[i]).replace(/=/,'').replace(/[+#]?[?!]*$/,'')) {
-            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) &&