]> git.sesse.net Git - remoteglot/blobdiff - www/js/remoteglot.js
Revert "Fix a null pointer exception in the frontend."
[remoteglot] / www / js / remoteglot.js
index b6076f4006e9d87034d553c6e7556031802fcff7..b049c32ea81bc52c63b1cbb19172d7c769778b21 100644 (file)
@@ -7,7 +7,7 @@
  * @type {Number}
  * @const
  * @private */
-var SCRIPT_VERSION = 2016032200;
+var SCRIPT_VERSION = 2016091401;
 
 /**
  * The current backend URL.
@@ -47,8 +47,10 @@ var displayed_analysis_data = null;
  * @type {?Array.<{
  *      name: string,
  *      url: string,
+ *      hashurl: string,
  *      id: string,
- *      score: Object
+ *      score: Object=,
+ *      result: string=,
  * }>}
  * @private
  */
@@ -156,10 +158,10 @@ var display_fen = null;
 
 /** @typedef {{
  *    start_fen: string,
- *    pretty_pv: Array.<string>,
+ *    pv: Array.<string>,
  *    move_num: number,
  *    toplay: string,
- *    score: string,
+ *    scores: Array<{first_move: number, score: Object}>,
  *    start_display_move_num: number
  * }} DisplayLine
  *
@@ -275,13 +277,31 @@ var request_update = function() {
                        location.reload(true);
                }
 
-               possibly_play_sound(current_analysis_data, new_data);
-               current_analysis_data = new_data;
-               update_board();
-               update_num_viewers(num_viewers);
+               // Verify that the PV makes sense.
+               var valid = true;
+               if (new_data['pv']) {
+                       var hiddenboard = new Chess(new_data['position']['fen']);
+                       for (var i = 0; i < new_data['pv'].length; ++i) {
+                               if (hiddenboard.move(new_data['pv'][i]) === null) {
+                                       valid = false;
+                                       break;
+                               }
+                       }
+               }
+
+               var timeout = 100;
+               if (valid) {
+                       possibly_play_sound(current_analysis_data, new_data);
+                       current_analysis_data = new_data;
+                       update_board();
+                       update_num_viewers(num_viewers);
+               } else {
+                       console.log("Received invalid update, waiting five seconds and trying again.");
+                       location.reload(true);
+               }
 
                // Next update.
-               current_analysis_request_timer = setTimeout(function() { request_update(); }, 100);
+               current_analysis_request_timer = setTimeout(function() { request_update(); }, timeout);
        }).fail(function(jqXHR, textStatus, errorThrown) {
                if (textStatus === "abort") {
                        // Aborted because we are switching backends. Abandon and don't retry,
@@ -550,17 +570,17 @@ var create_arrow = function(from_square, to_square, fg_color, line_width, arrow_
 }
 
 // Note: invert is ignored.
-var compare_by_sort_key = function(refutation_lines, invert, a, b) {
-       var ska = refutation_lines[a]['sort_key'];
-       var skb = refutation_lines[b]['sort_key'];
+var compare_by_name = function(refutation_lines, invert, a, b) {
+       var ska = refutation_lines[a]['move'];
+       var skb = refutation_lines[b]['move'];
        if (ska < skb) return -1;
        if (ska > skb) return 1;
        return 0;
 };
 
 var compare_by_score = function(refutation_lines, invert, a, b) {
-       var sa = compute_score_sort_key(refutation_lines[b]['score'], invert);
-       var sb = compute_score_sort_key(refutation_lines[a]['score'], invert);
+       var sa = compute_score_sort_key(refutation_lines[b]['score'], refutation_lines[b]['depth'], invert);
+       var sb = compute_score_sort_key(refutation_lines[a]['score'], refutation_lines[a]['depth'], invert);
        return sa - sb;
 }
 
@@ -571,8 +591,8 @@ var compare_by_score = function(refutation_lines, invert, a, b) {
  * @param {!Object} data
  * @param {number} margin The maximum number of centipawns worse than the
  *     best move can be and still be included.
- * @param {boolean} margin Whether black is to play.
- * @return {Array.<string>} The UCI representation (e.g. e1g1) of all
+ * @param {boolean} invert Whether black is to play.
+ * @return {Array.<string>} The FEN representation (e.g. Ne4) of all
  *     moves, in score order.
  */
 var find_nonstupid_moves = function(data, margin, invert) {
@@ -582,14 +602,15 @@ var find_nonstupid_moves = function(data, margin, invert) {
        var best_score = undefined;
        var pv_score = undefined;
        for (var move in data['refutation_lines']) {
-               var score = compute_score_sort_key(data['refutation_lines'][move]['score'], invert);
-               if (move == data['pv_uci'][0]) {
+               var line = data['refutation_lines'][move];
+               var score = compute_score_sort_key(line['score'], line['depth'], invert, false);
+               if (move == data['pv'][0]) {
                        pv_score = score;
                }
                if (best_score === undefined || score > best_score) {
                        best_score = score;
                }
-               if (!(data['refutation_lines'][move]['depth'] >= 8)) {
+               if (line['depth'] < 8) {
                        return [];
                }
        }
@@ -602,13 +623,14 @@ var find_nonstupid_moves = function(data, margin, invert) {
        // The PV move will always be first.
        var moves = [];
        for (var move in data['refutation_lines']) {
-               var score = compute_score_sort_key(data['refutation_lines'][move]['score'], invert);
-               if (move != data['pv_uci'][0] && best_score - score <= margin) {
+               var line = data['refutation_lines'][move];
+               var score = compute_score_sort_key(line['score'], line['depth'], invert);
+               if (move != data['pv'][0] && best_score - score <= margin) {
                        moves.push(move);
                }
        }
        moves = moves.sort(function(a, b) { return compare_by_score(data['refutation_lines'], data['position']['toplay'] === 'B', a, b) });
-       moves.unshift(data['pv_uci'][0]);
+       moves.unshift(data['pv'][0]);
 
        return moves;
 }
@@ -623,21 +645,21 @@ var thousands = function(x) {
 
 /**
  * @param {!string} start_fen
- * @param {Array.<string>} pretty_pv
+ * @param {Array.<string>} pv
  * @param {number} move_num
  * @param {!string} toplay
- * @param {!string} score
+ * @param {Array<{ first_move: integer, score: Object }>} scores
  * @param {number} start_display_move_num
  * @param {number=} opt_limit
  * @param {boolean=} opt_showlast
  */
-var add_pv = function(start_fen, pretty_pv, move_num, toplay, score, start_display_move_num, opt_limit, opt_showlast) {
+var add_pv = function(start_fen, pv, move_num, toplay, scores, start_display_move_num, opt_limit, opt_showlast) {
        display_lines.push({
                start_fen: start_fen,
-               pretty_pv: pretty_pv,
+               pv: pv,
                move_num: parseInt(move_num),
                toplay: toplay,
-               score: score,
+               scores: scores,
                start_display_move_num: start_display_move_num
        });
        return print_pv(display_lines.length - 1, opt_limit, opt_showlast);
@@ -650,14 +672,14 @@ var add_pv = function(start_fen, pretty_pv, move_num, toplay, score, start_displ
  */
 var print_pv = function(line_num, opt_limit, opt_showlast) {
        var display_line = display_lines[line_num];
-       var pretty_pv = display_line.pretty_pv;
+       var pv = display_line.pv;
        var move_num = display_line.move_num;
        var toplay = display_line.toplay;
 
        // Truncate PV at the start if needed.
        var start_display_move_num = display_line.start_display_move_num;
        if (start_display_move_num > 0) {
-               pretty_pv = pretty_pv.slice(start_display_move_num);
+               pv = pv.slice(start_display_move_num);
                var to_add = start_display_move_num;
                if (toplay === 'B') {
                        ++move_num;
@@ -671,45 +693,45 @@ var print_pv = function(line_num, opt_limit, opt_showlast) {
                move_num += to_add / 2;
        }
 
-       var pv = '';
+       var ret = '';
        var i = 0;
-       if (opt_limit && opt_showlast && pretty_pv.length > opt_limit) {
+       if (opt_limit && opt_showlast && pv.length > opt_limit) {
                // Truncate the PV at the beginning (instead of at the end).
                // We assume here that toplay is 'W'. We also assume that if
                // opt_showlast is set, then it is the history, and thus,
                // the UI should be to expand the history.
-               pv = '(<a class="move" href="javascript:collapse_history(false)">…</a>) ';
-               i = pretty_pv.length - opt_limit;
+               ret = '(<a class="move" href="javascript:collapse_history(false)">…</a>) ';
+               i = pv.length - opt_limit;
                if (i % 2 == 1) {
                        ++i;
                }
                move_num += i / 2;
-       } else if (toplay == 'B' && pretty_pv.length > 0) {
-               var move = "<a class=\"move\" id=\"automove" + line_num + "-0\" href=\"javascript:show_line(" + line_num + ", " + 0 + ");\">" + pretty_pv[0] + "</a>";
-               pv = move_num + '. … ' + move;
+       } else if (toplay == 'B' && pv.length > 0) {
+               var move = "<a class=\"move\" id=\"automove" + line_num + "-0\" href=\"javascript:show_line(" + line_num + ", " + 0 + ");\">" + pv[0] + "</a>";
+               ret = move_num + '. … ' + move;
                toplay = 'W';
                ++i;
                ++move_num;
        }
-       for ( ; i < pretty_pv.length; ++i) {
-               var move = "<a class=\"move\" id=\"automove" + line_num + "-" + i + "\" href=\"javascript:show_line(" + line_num + ", " + i + ");\">" + pretty_pv[i] + "</a>";
+       for ( ; i < pv.length; ++i) {
+               var move = "<a class=\"move\" id=\"automove" + line_num + "-" + i + "\" href=\"javascript:show_line(" + line_num + ", " + i + ");\">" + pv[i] + "</a>";
 
                if (toplay == 'W') {
                        if (i > opt_limit && !opt_showlast) {
-                               return pv + ' (…)';
+                               return ret + ' (…)';
                        }
-                       if (pv != '') {
-                               pv += ' ';
+                       if (ret != '') {
+                               ret += ' ';
                        }
-                       pv += move_num + '. ' + move;
+                       ret += move_num + '. ' + move;
                        ++move_num;
                        toplay = 'B';
                } else {
-                       pv += ' ' + move;
+                       ret += ' ' + move;
                        toplay = 'W';
                }
        }
-       return pv;
+       return ret;
 }
 
 /** Update the highlighted to/from squares on the board.
@@ -725,7 +747,7 @@ var update_board_highlight = function() {
 }
 
 var update_history = function() {
-       if (display_lines[0] === null || display_lines[0].pretty_pv.length == 0) {
+       if (display_lines[0] === null || display_lines[0].pv.length == 0) {
                $("#history").html("No history");
        } else if (truncate_display_history) {
                $("#history").html(print_pv(0, 8, true));
@@ -762,9 +784,11 @@ var update_refutation_lines = function() {
 
        // Find out where the lines start from.
        var base_line = [];
+       var base_scores = display_lines[1].scores;
        var start_display_move_num = 0;
        if (hash_refutation_lines) {
-               base_line = current_display_line.pretty_pv.slice(0, current_display_move + 1);
+               base_line = current_display_line.pv.slice(0, current_display_move + 1);
+               base_scores = current_display_line.scores;
                start_display_move_num = base_line.length;
        }
 
@@ -777,7 +801,7 @@ var update_refutation_lines = function() {
        if (current_display_line && current_display_move % 2 == 0) {
                invert = !invert;
        }
-       var compare = sort_refutation_lines_by_score ? compare_by_score : compare_by_sort_key;
+       var compare = sort_refutation_lines_by_score ? compare_by_score : compare_by_name;
        moves = moves.sort(function(a, b) { return compare(refutation_lines, invert, a, b) });
        for (var i = 0; i < moves.length; ++i) {
                var line = refutation_lines[moves[i]];
@@ -788,9 +812,11 @@ var update_refutation_lines = function() {
                tr.appendChild(move_td);
                $(move_td).addClass("move");
 
-               if (line['pv_pretty'].length == 0) {
+               var scores = base_scores.concat([{ first_move: start_display_move_num, score: line['score'] }]);
+
+               if (line['pv'].length == 0) {
                        // Not found, so just make a one-move PV.
-                       var move = "<a class=\"move\" href=\"javascript:show_line(" + display_lines.length + ", " + 0 + ");\">" + line['pretty_move'] + "</a>";
+                       var move = "<a class=\"move\" href=\"javascript:show_line(" + display_lines.length + ", " + 0 + ");\">" + line['move'] + "</a>";
                        $(move_td).html(move);
                        var score_td = document.createElement("td");
 
@@ -806,13 +832,13 @@ var update_refutation_lines = function() {
                        var pv_td = document.createElement("td");
                        tr.appendChild(pv_td);
                        $(pv_td).addClass("pv");
-                       $(pv_td).html(add_pv(base_fen, base_line.concat([ line['pretty_move'] ]), move_num, toplay, line['score'], start_display_move_num));
+                       $(pv_td).html(add_pv(base_fen, base_line.concat([ line['move'] ]), move_num, toplay, scores, start_display_move_num));
 
                        tbl.append(tr);
                        continue;
                }
 
-               var move = "<a class=\"move\" href=\"javascript:show_line(" + display_lines.length + ", " + 0 + ");\">" + line['pretty_move'] + "</a>";
+               var move = "<a class=\"move\" href=\"javascript:show_line(" + display_lines.length + ", " + 0 + ");\">" + line['move'] + "</a>";
                $(move_td).html(move);
 
                var score_td = document.createElement("td");
@@ -832,7 +858,7 @@ var update_refutation_lines = function() {
                var pv_td = document.createElement("td");
                tr.appendChild(pv_td);
                $(pv_td).addClass("pv");
-               $(pv_td).html(add_pv(base_fen, base_line.concat(line['pv_pretty']), move_num, toplay, line['score'], start_display_move_num, 10));
+               $(pv_td).html(add_pv(base_fen, base_line.concat(line['pv']), move_num, toplay, scores, start_display_move_num, 10));
 
                tbl.append(tr);
        }
@@ -889,16 +915,33 @@ var update_game_list = function(games) {
 
                var game_name = document.createTextNode(game['name']);
                if (game['url'] === backend_url) {
+                       // This game.
                        game_span.appendChild(game_name);
+
+                       if (current_analysis_data && current_analysis_data['position']) {
+                               var score;
+                               if (current_analysis_data['position']['result']) {
+                                       score = " (" + current_analysis_data['position']['result'] + ")";
+                               } else {
+                                       score = " (" + format_short_score(current_analysis_data['score']) + ")";
+                               }
+                               game_span.appendChild(document.createTextNode(score));
+                       }
                } else {
+                       // Some other game.
                        var game_a = document.createElement("a");
                        game_a.setAttribute("href", "#" + game['id']);
                        game_a.appendChild(game_name);
                        game_span.appendChild(game_a);
-               }
 
-               var score = " (" + format_short_score(game['score']) + ")";
-               game_span.appendChild(document.createTextNode(score));
+                       var score;
+                       if (game['result']) {
+                               score = " (" + game['result'] + ")";
+                       } else {
+                               score = " (" + format_short_score(game['score']) + ")";
+                       }
+                       game_span.appendChild(document.createTextNode(score));
+               }
 
                games_div.appendChild(game_span);
        }
@@ -917,7 +960,7 @@ var possibly_switch_game_from_hash = function() {
        for (var i = 0; i < current_games.length; ++i) {
                if (current_games[i]['id'] === hash) {
                        if (backend_url !== current_games[i]['url']) {
-                               switch_backend(current_games[i]['url'], current_games[i]['hashurl']);
+                               switch_backend(current_games[i]);
                        }
                        return;
                }
@@ -935,8 +978,8 @@ var update_board = function() {
        // Print the history. This is pretty much the only thing that's
        // unconditionally taken from current_data (we're not interested in
        // historic history).
-       if (current_data['position']['pretty_history']) {
-               add_pv('start', current_data['position']['pretty_history'], 1, 'W', null, 0, 8, true);
+       if (current_data['position']['history']) {
+               add_pv('start', current_data['position']['history'], 1, 'W', null, 0, 8, true);
        } else {
                display_lines.push(null);
        }
@@ -946,11 +989,10 @@ var update_board = function() {
        if (current_data['games']) {
                current_games = current_data['games'];
                possibly_switch_game_from_hash();
-               update_game_list(current_data['games']);
        } else {
                current_games = null;
-               update_game_list(null);
        }
+       update_game_list(current_games);
 
        // The headline. Names are always fetched from current_data;
        // the rest can depend a bit.
@@ -1012,7 +1054,7 @@ var update_board = function() {
                // Displaying some non-current position, pick out the last move
                // from the history. This will work even if the fetch failed.
                last_move = format_halfmove_with_number(
-                       current_display_line.pretty_pv[current_display_move],
+                       current_display_line.pv[current_display_move],
                        current_display_move + 1);
                headline += ' after ' + last_move;
        } else if (data['position']['last_move'] !== 'none') {
@@ -1028,8 +1070,10 @@ var update_board = function() {
 
        // The <title> contains a very brief headline.
        var title_elems = [];
-       if (data['score']) {
-               title_elems.push(format_short_score(data['score']).replace(/^ /, ""));
+       if (data['position'] && data['position']['result']) {
+               title_elems.push(data['position']['result']);
+       } else if (data['score']) {
+               title_elems.push(format_short_score(data['score']));
        }
        if (last_move !== null) {
                title_elems.push(last_move);
@@ -1045,13 +1089,13 @@ var update_board = function() {
        if (data['position'] && data['position']['last_move_uci']) {
                highlight_from = data['position']['last_move_uci'].substr(0, 2);
                highlight_to = data['position']['last_move_uci'].substr(2, 2);
-       } else if (current_display_line_is_history && current_display_move >= 0) {
+       } else if (current_display_line_is_history && current_display_line && current_display_move >= 0) {
                // We don't have historic analysis for this position, but we
                // can reconstruct what the last move was by just replaying
                // from the start.
-               var hiddenboard = chess_from(null, current_display_line.pretty_pv, current_display_move);
+               var hiddenboard = chess_from(null, current_display_line.pv, current_display_move);
                var moves = hiddenboard.history({ verbose: true });
-               var last_move = moves.pop();
+               last_move = moves.pop();
                highlight_from = last_move.from;
                highlight_to = last_move.to;
        } else {
@@ -1078,11 +1122,20 @@ var update_board = function() {
        update_clock();
 
        // The score.
-       if (current_display_line) {
-               if (current_display_line.score) {
-                       $("#score").text(format_long_score(current_display_line.score));
+       if (current_display_line && !current_display_line_is_history) {
+               var score;
+               if (current_display_line.scores && current_display_line.scores.length > 0) {
+                       for (var i = 0; i < current_display_line.scores.length; ++i) {
+                               if (current_display_move < current_display_line.scores[i].first_move) {
+                                       break;
+                               }
+                               score = current_display_line.scores[i].score;
+                       }
+               }
+               if (score) {
+                       $("#score").text(format_long_score(score));
                } else {
-                       $("#score").text("No score for this move");
+                       $("#score").text("No score for this line");
                }
        } else if (data['score']) {
                $("#score").text(format_long_score(data['score']));
@@ -1117,48 +1170,63 @@ var update_board = function() {
 
        // Print the PV.
        $("#pvtitle").text("PV:");
-       $("#pv").html(add_pv(data['position']['fen'], data['pv_pretty'], data['position']['move_num'], data['position']['toplay'], data['score'], 0));
+
+       var scores = [{ first_move: -1, score: data['score'] }];
+       $("#pv").html(add_pv(data['position']['fen'], data['pv'], data['position']['move_num'], data['position']['toplay'], scores, 0));
 
        // Update the PV arrow.
        clear_arrows();
-       if (data['pv_uci'].length >= 1) {
+       if (data['pv'].length >= 1) {
+               var hiddenboard = new Chess(base_fen);
+
                // draw a continuation arrow as long as it's the same piece
-               for (var i = 0; i < data['pv_uci'].length; i += 2) {
-                       var from = data['pv_uci'][i].substr(0, 2);
-                       var to = data['pv_uci'][i].substr(2,4);
-                       if ((i >= 2 && from != data['pv_uci'][i - 2].substr(2, 2)) ||
-                            interfering_arrow(from, to)) {
+               var last_to;
+               for (var i = 0; i < data['pv'].length; i += 2) {
+                       var move = hiddenboard.move(data['pv'][i]);
+                       if ((i >= 2 && move.from != last_to) ||
+                            interfering_arrow(move.from, move.to)) {
                                break;
                        }
-                       create_arrow(from, to, '#f66', 6, 20);
+                       create_arrow(move.from, move.to, '#f66', 6, 20);
+                       last_to = move.from;
+                       hiddenboard.move(data['pv'][i + 1]);  // To keep continuity.
                }
 
                var alt_moves = find_nonstupid_moves(data, 30, data['position']['toplay'] === 'B');
                for (var i = 1; i < alt_moves.length && i < 3; ++i) {
-                       create_arrow(alt_moves[i].substr(0, 2),
-                                    alt_moves[i].substr(2, 2), '#f66', 1, 10);
+                       hiddenboard = new Chess(base_fen);
+                       var move = hiddenboard.move(alt_moves[i]);
+                       create_arrow(move.from, move.to, '#f66', 1, 10);
                }
        }
 
        // See if all semi-reasonable moves have only one possible response.
-       if (data['pv_uci'].length >= 2) {
+       if (data['pv'].length >= 2) {
                var nonstupid_moves = find_nonstupid_moves(data, 300, data['position']['toplay'] === 'B');
-               var response = data['pv_uci'][1];
+               var response;
+               {
+                       var hiddenboard = new Chess(base_fen);
+                       hiddenboard.move(data['pv'][0]);
+                       response = hiddenboard.move(data['pv'][1]);
+               }
                for (var i = 0; i < nonstupid_moves.length; ++i) {
-                       if (nonstupid_moves[i] == data['pv_uci'][0]) {
+                       if (nonstupid_moves[i] == data['pv'][0]) {
                                // ignore the PV move for refutation lines.
                                continue;
                        }
                        if (!data['refutation_lines'] ||
                            !data['refutation_lines'][nonstupid_moves[i]] ||
-                           !data['refutation_lines'][nonstupid_moves[i]]['pv_uci'] ||
-                           data['refutation_lines'][nonstupid_moves[i]]['pv_uci'].length < 1) {
+                           !data['refutation_lines'][nonstupid_moves[i]]['pv'] ||
+                           data['refutation_lines'][nonstupid_moves[i]]['pv'].length < 1) {
                                // Incomplete PV, abort.
                                response = undefined;
                                break;
                        }
-                       var this_response = data['refutation_lines'][nonstupid_moves[i]]['pv_uci'][1];
-                       if (response !== this_response) {
+                       var line = data['refutation_lines'][nonstupid_moves[i]];
+                       hiddenboard = new Chess(base_fen);
+                       hiddenboard.move(line['pv'][0]);
+                       var this_response = hiddenboard.move(line['pv'][1]);
+                       if (response.from !== this_response.from || response.to !== this_response.to) {
                                // Different response depending on lines, abort.
                                response = undefined;
                                break;
@@ -1166,8 +1234,7 @@ var update_board = function() {
                }
 
                if (nonstupid_moves.length > 0 && response !== undefined) {
-                       create_arrow(response.substr(0, 2),
-                                    response.substr(2, 2), '#66f', 6, 20);
+                       create_arrow(response.from, response.to, '#66f', 6, 20);
                }
        }
 
@@ -1259,6 +1326,8 @@ var update_clock = function() {
        clearTimeout(clock_timer);
 
        var data = displayed_analysis_data || current_analysis_data;
+       if (!data) return;
+
        if (data['position']) {
                var result = data['position']['result'];
                if (result === '1-0') {
@@ -1286,7 +1355,6 @@ var update_clock = function() {
 
        var white_clock_ms = null;
        var black_clock_ms = null;
-       var show_seconds = false;
 
        // Static clocks.
        if (data['position'] &&
@@ -1415,14 +1483,14 @@ var format_halfmove_with_number = function(move, halfmove_num) {
  */
 var format_tooltip = function(data, halfmove_num) {
        if (data['score_history'][halfmove_num] ||
-           halfmove_num === data['position']['pretty_history'].length) {
+           halfmove_num === data['position']['history'].length) {
                var move;
                var short_score;
-               if (halfmove_num === data['position']['pretty_history'].length) {
+               if (halfmove_num === data['position']['history'].length) {
                        move = data['position']['last_move'];
                        short_score = format_short_score(data['score']);
                } else {
-                       move = data['position']['pretty_history'][halfmove_num];
+                       move = data['position']['history'][halfmove_num];
                        short_score = format_short_score(data['score_history'][halfmove_num]);
                }
                var move_with_number = format_halfmove_with_number(move, halfmove_num);
@@ -1431,7 +1499,7 @@ var format_tooltip = function(data, halfmove_num) {
        } else {
                for (var i = halfmove_num; i --> 0; ) {
                        if (data['score_history'][i]) {
-                               var move = data['position']['pretty_history'][i];
+                               var move = data['position']['history'][i];
                                return "[Analysis kept from " + format_halfmove_with_number(move, i) + "]";
                        }
                }
@@ -1502,7 +1570,7 @@ window['prev_move'] = prev_move;
 
 var next_move = function() {
        if (current_display_line &&
-           current_display_move < current_display_line.pretty_pv.length - 1) {
+           current_display_move < current_display_line.pv.length - 1) {
                ++current_display_move;
        }
        update_historic_analysis();
@@ -1511,17 +1579,35 @@ var next_move = function() {
 }
 window['next_move'] = next_move;
 
+var next_game = function() {
+       if (current_games === null) {
+               return;
+       }
+
+       // Try to find the game we are currently looking at.
+       for (var game_num = 0; game_num < current_games.length; ++game_num) {
+               var game = current_games[game_num];
+               if (game['url'] === backend_url) {
+                       var next_game_num = (game_num + 1) % current_games.length;
+                       switch_backend(current_games[next_game_num]);
+                       return;
+               }
+       }
+
+       // Couldn't find it; give up.
+}
+
 var update_historic_analysis = function() {
        if (!current_display_line_is_history) {
                return;
        }
-       if (current_display_move == current_display_line.pretty_pv.length - 1) {
+       if (current_display_move == current_display_line.pv.length - 1) {
                displayed_analysis_data = null;
                update_board();
        }
 
        // Fetch old analysis for this line if it exists.
-       var hiddenboard = chess_from(null, current_display_line.pretty_pv, current_display_move);
+       var hiddenboard = chess_from(null, current_display_line.pv, current_display_move);
        var filename = "/history/move" + (current_display_move + 1) + "-" +
                hiddenboard.fen().replace(/ /g, '_').replace(/\//g, '-') + ".json";
 
@@ -1593,6 +1679,9 @@ var update_move_highlight = function() {
                        display_lines.push(current_display_line);
                        $("#pv").html(print_pv(display_lines.length - 1));
                        display_line_num = display_lines.length - 1;
+
+                       // Clear out the PV, so it's not selected by anything later.
+                       display_lines[1].pv = [];
                }
 
                highlighted_move = $("#automove" + display_line_num + "-" + (current_display_move - current_display_line.start_display_move_num));
@@ -1612,10 +1701,10 @@ var find_display_line_matching_num = function() {
                var line = display_lines[i];
                if (line.start_display_move_num > 0) continue;
                if (current_display_line.start_fen !== line.start_fen) continue;
-               if (current_display_line.pretty_pv.length !== line.pretty_pv.length) continue;
+               if (current_display_line.pv.length !== line.pv.length) continue;
                var ok = true;
-               for (var j = 0; j < line.pretty_pv.length; ++j) {
-                       if (current_display_line.pretty_pv[j] !== line.pretty_pv[j]) {
+               for (var j = 0; j < line.pv.length; ++j) {
+                       if (current_display_line.pv[j] !== line.pv[j]) {
                                ok = false;
                                break;
                        }
@@ -1627,12 +1716,17 @@ var find_display_line_matching_num = function() {
        return null;
 }
 
+/** Update the board based on the currently displayed line.
+ * 
+ * TODO: This should really be called only whenever something changes,
+ * instead of all the time.
+ */
 var update_displayed_line = function() {
        if (current_display_line === null) {
                $("#linenav").hide();
                $("#linemsg").show();
                display_fen = base_fen;
-               board.position(base_fen);
+               set_board_position(base_fen);
                update_imbalance(base_fen);
                return;
        }
@@ -1645,25 +1739,30 @@ var update_displayed_line = function() {
        } else {
                $("#prevmove").html("<a href=\"javascript:prev_move();\">Previous</a></span>");
        }
-       if (current_display_move == current_display_line.pretty_pv.length - 1) {
+       if (current_display_move == current_display_line.pv.length - 1) {
                $("#nextmove").html("Next");
        } else {
                $("#nextmove").html("<a href=\"javascript:next_move();\">Next</a></span>");
        }
 
-       var hiddenboard = chess_from(current_display_line.start_fen, current_display_line.pretty_pv, current_display_move);
+       var hiddenboard = chess_from(current_display_line.start_fen, current_display_line.pv, current_display_move);
+       set_board_position(hiddenboard.fen());
+       if (display_fen !== hiddenboard.fen() && !current_display_line_is_history) {
+               // Fire off a hash request, since we're now off the main position
+               // and it just changed.
+               explore_hash(hiddenboard.fen());
+       }
        display_fen = hiddenboard.fen();
+       update_imbalance(hiddenboard.fen());
+}
+
+var set_board_position = function(new_fen) {
        board_is_animating = true;
        var old_fen = board.fen();
-       board.position(hiddenboard.fen());
+       board.position(new_fen);
        if (board.fen() === old_fen) {
                board_is_animating = false;
-       } else if (!current_display_line_is_history) {
-               // Fire off a hash request, since we're now off the main position
-               // and it just changed.
-               explore_hash(display_fen);
        }
-       update_imbalance(hiddenboard.fen());
 }
 
 /**
@@ -1808,8 +1907,8 @@ var get_best_move = function(game, source, target, invert) {
 
        // See if we're already exploring some line.
        if (current_display_line &&
-           current_display_move < current_display_line.pretty_pv.length - 1) {
-               var first_move = current_display_line.pretty_pv[current_display_move + 1];
+           current_display_move < current_display_line.pv.length - 1) {
+               var first_move = current_display_line.pv[current_display_move + 1];
                if (move_hash[first_move]) {
                        return move_hash[first_move];
                }
@@ -1818,7 +1917,7 @@ var get_best_move = function(game, source, target, invert) {
        // History and PV take priority over the display lines.
        for (var i = 0; i < 2; ++i) {
                var line = display_lines[i];
-               var first_move = line.pretty_pv[line.start_display_move_num];
+               var first_move = line.pv[line.start_display_move_num];
                if (move_hash[first_move]) {
                        return move_hash[first_move];
                }
@@ -1832,9 +1931,9 @@ var get_best_move = function(game, source, target, invert) {
                if (!line['score']) {
                        continue;
                }
-               var first_move = line['pv_pretty'][0];
+               var first_move = line['pv'][0];
                if (move_hash[first_move]) {
-                       var score = compute_score_sort_key(line['score'], invert);
+                       var score = compute_score_sort_key(line['score'], line['depth'], invert);
                        if (best_move_score === null || score > best_move_score) {
                                best_move = move_hash[first_move];
                                best_move_score = score;
@@ -1883,8 +1982,8 @@ var onSnapEnd = function(source, target) {
        });
 
        if (current_display_line &&
-           current_display_move < current_display_line.pretty_pv.length - 1 &&
-           current_display_line.pretty_pv[current_display_move + 1] === move.san) {
+           current_display_move < current_display_line.pv.length - 1 &&
+           current_display_line.pv[current_display_move + 1] === move.san) {
                next_move();
                return;
        }
@@ -1893,8 +1992,12 @@ var onSnapEnd = function(source, target) {
        // this move, then select that. Note that this gives us a good priority
        // order (history first, then PV, then multi-PV lines).
        for (var i = 0; i < display_lines.length; ++i) {
+               if (i == 1 && current_display_line) {
+                       // Do not choose PV if not on it.
+                       continue;
+               }
                var line = display_lines[i];
-               if (line.pretty_pv[line.start_display_move_num] === move.san) {
+               if (line.pv[line.start_display_move_num] === move.san) {
                        show_line(i, 0);
                        return;
                }
@@ -1905,14 +2008,6 @@ var onSnapEnd = function(source, target) {
 }
 // End of dragging-related code.
 
-var pad = function(val, num_digits) {
-       var s = val.toString();
-       while (s.length < num_digits) {
-               s = " " + s;
-       }
-       return s;
-}
-
 var fmt_cp = function(v) {
        if (v === 0) {
                return "0.00";
@@ -1930,9 +2025,9 @@ var format_short_score = function(score) {
        }
        if (score[0] === 'm') {
                if (score[2]) {  // Is a bound.
-                       return score[2] + "\u00a0M" + pad(score[1], 3);
+                       return score[2] + "\u00a0M " + score[1];
                } else {
-                       return "M" + pad(score[1], 3);
+                       return "M " + score[1];
                }
        } else if (score[0] === 'd') {
                return "TB draw";
@@ -1940,13 +2035,16 @@ var format_short_score = function(score) {
                if (score[2]) {  // Is a bound.
                        return score[2] + "\u00a0" + fmt_cp(score[1]);
                } else {
-                       return pad(fmt_cp(score[1]), 5);
+                       return fmt_cp(score[1]);
                }
        }
        return null;
 }
 
 var format_long_score = function(score) {
+       if (!score) {
+               return "???";
+       }
        if (score[0] === 'm') {
                if (score[1] > 0) {
                        return "White mates in " + score[1];
@@ -1984,10 +2082,12 @@ var compute_plot_score = function(score) {
 
 /**
  * @param score The score digest tuple.
+ * @param {?number} depth Depth the move has been computed to, or null.
  * @param {boolean} invert Whether black is to play.
+ * @param {boolean=} depth_secondary_key
  * @return {number}
  */
-var compute_score_sort_key = function(score, invert) {
+var compute_score_sort_key = function(score, depth, invert, depth_secondary_key) {
        var s;
        if (!score) {
                return -10000000;
@@ -2000,20 +2100,27 @@ var compute_score_sort_key = function(score, invert) {
                        // Black mates (note the double negative for score[1]).
                        s = -99999 - score[1];
                }
-               if (invert) s = -s;
-               return s;
        } else if (score[0] === 'd') {
-               return 0;
+               s = 0;
        } else if (score[0] === 'cp') {
-               return invert ? -score[1] : score[1];
+               s = score[1];
+       }
+       if (s) {
+               if (invert) s = -s;
+               if (depth_secondary_key) {
+                       return s * 200 + (depth || 0);
+               } else {
+                       return s;
+               }
+       } else {
+               return null;
        }
-       return null;
 }
 
 /**
- * @param {string} new_backend_url
+ * @param {Object} game
  */
-var switch_backend = function(new_backend_url, new_backend_hash_url) {
+var switch_backend = function(game) {
        // Stop looking at historic data.
        current_display_line = null;
        current_display_move = null;
@@ -2042,8 +2149,9 @@ var switch_backend = function(new_backend_url, new_backend_hash_url) {
        }
 
        // Request an immediate fetch with the new backend.
-       backend_url = new_backend_url;
-       backend_hash_url = new_backend_hash_url;
+       backend_url = game['url'];
+       backend_hash_url = game['hashurl'];
+       window.location.hash = '#' + game['id'];
        current_analysis_data = null;
        ims = 0;
        request_update();
@@ -2085,10 +2193,17 @@ var init = function() {
                redraw_arrows();
        });
        $(window).keyup(function(event) {
-               if (event.which == 39) {
+               if (event.which == 39) {  // Left arrow.
                        next_move();
-               } else if (event.which == 37) {
+               } else if (event.which == 37) {  // Right arrow.
                        prev_move();
+               } else if (event.which >= 49 && event.which <= 57) {  // 1-9.
+                       var num = event.which - 49;
+                       if (current_games && current_games.length >= num) {
+                               switch_backend(current_games[num]);
+                       }
+               } else if (event.which == 78) {  // N.
+                       next_game();
                }
        });
        window.addEventListener('hashchange', possibly_switch_game_from_hash, false);