/** @type {window.ChessBoard} @private */
var board = null;
+/**
+ * The most recent analysis data we have from the server
+ * (about the most recent position).
+ *
+ * @type {?Object}
+ * @private */
+var current_analysis_data = null;
+
+/**
+ * If we are displaying previous analysis, this is non-null,
+ * and will override most of current_analysis_data.
+ *
+ * @type {?Object}
+ * @private
+ */
+var displayed_analysis_data = null;
+
/** @type {Array.<{
* from_col: number,
* from_row: number,
/** @type {?number} @private */
var unique = null;
+/** @type {boolean} @private */
+var enable_sound = false;
+
+/**
+ * Our best estimate of how many milliseconds we need to add to
+ * new Date() to get the true UTC time. Calibrated against the
+ * server clock.
+ *
+ * @type {?number}
+ * @private
+ */
+var client_clock_offset_ms = null;
+
+var clock_timer = null;
+
/** The current position on the board, represented as a FEN string.
* @type {?string}
* @private
/** @type {?DisplayLine} @private */
var current_display_line = null;
+/** @type {boolean} @private */
+var current_display_line_is_history = false;
+
/** @type {?number} @private */
var current_display_move = null;
$.ajax({
url: "/analysis.pl?ims=" + ims + "&unique=" + unique
}).done(function(data, textstatus, xhr) {
+ sync_server_clock(xhr.getResponseHeader('Date'));
ims = xhr.getResponseHeader('X-Remoteglot-Last-Modified');
var num_viewers = xhr.getResponseHeader('X-Remoteglot-Num-Viewers');
- update_board(data);
+ possibly_play_sound(current_analysis_data, data);
+ current_analysis_data = data;
+ update_board(current_analysis_data, displayed_analysis_data);
update_num_viewers(num_viewers);
// Next update.
});
}
+var possibly_play_sound = function(old_data, new_data) {
+ if (!enable_sound) {
+ return;
+ }
+ if (old_data === null) {
+ return;
+ }
+ var ding = document.getElementById('ding');
+ if (ding && ding.play) {
+ if (old_data['position'] && old_data['position']['fen'] &&
+ new_data['position'] && new_data['position']['fen'] &&
+ (old_data['position']['fen'] !== new_data['position']['fen'] ||
+ old_data['position']['move_num'] !== new_data['position']['move_num'])) {
+ ding.play();
+ }
+ }
+}
+
+/**
+ * @type {!string} server_date_string
+ */
+var sync_server_clock = function(server_date_string) {
+ var server_time_ms = new Date(server_date_string).getTime();
+ var client_time_ms = new Date().getTime();
+ var estimated_offset_ms = server_time_ms - client_time_ms;
+
+ // In order not to let the noise move us too much back and forth
+ // (the server only has one-second resolution anyway), we only
+ // change an existing skew if we are at least five seconds off.
+ if (client_clock_offset_ms === null ||
+ Math.abs(estimated_offset_ms - client_clock_offset_ms) > 5000) {
+ client_clock_offset_ms = estimated_offset_ms;
+ }
+}
+
var clear_arrows = function() {
for (var i = 0; i < arrows.length; ++i) {
if (arrows[i].svg) {
arrow.svg.parentElement.removeChild(arrow.svg);
delete arrow.svg;
}
- if (current_display_line !== null) {
+ if (current_display_line !== null && !current_display_line_is_history) {
return;
}
/**
* @param {!string} fen
- * @param {Array.<string>} uci_pv
+ * @param {Array.<string>} pretty_pv
* @param {number} move_num
* @param {!string} toplay
* @param {number=} opt_limit
* @param {boolean=} opt_showlast
*/
-var add_pv = function(fen, uci_pv, move_num, toplay, opt_limit, opt_showlast) {
- var hiddenboard = new Chess();
- hiddenboard.load(fen);
- for (var i = 0; i < uci_pv.length; ++i) {
- hiddenboard.move(ucimove_to_chessjs_move(uci_pv[i]));
- }
- var pretty_pv = hiddenboard.history();
-
+var add_pv = function(fen, pretty_pv, move_num, toplay, opt_limit, opt_showlast) {
display_lines.push({
start_fen: fen,
- uci_pv: uci_pv,
pretty_pv: pretty_pv,
line_number: display_lines.length
});
var update_highlight = function() {
$("#board").find('.square-55d63').removeClass('nonuglyhighlight');
- if (current_display_line === null && highlight_from !== undefined && highlight_to !== undefined) {
+ if ((current_display_line === null || current_display_line_is_history) &&
+ highlight_from !== undefined && highlight_to !== undefined) {
$("#board").find('.square-' + highlight_from).addClass('nonuglyhighlight');
$("#board").find('.square-' + highlight_to).addClass('nonuglyhighlight');
}
var move_td = document.createElement("td");
tr.appendChild(move_td);
$(move_td).addClass("move");
- if (line['pv_uci'].length == 0) {
+ if (line['pv_pretty'].length == 0) {
$(move_td).text(line['pretty_move']);
} else {
var move = "<a class=\"move\" href=\"javascript:show_line(" + display_lines.length + ", " + 0 + ");\">" + line['pretty_move'] + "</a>";
var pv_td = document.createElement("td");
tr.appendChild(pv_td);
$(pv_td).addClass("pv");
- $(pv_td).html(add_pv(fen, line['pv_uci'], move_num, toplay, 10));
+ $(pv_td).html(add_pv(fen, line['pv_pretty'], move_num, toplay, 10));
tbl.append(tr);
}
/**
* @param {Object} data
+ * @param {?Object} display_data
*/
-var update_board = function(data) {
+var update_board = function(current_data, display_data) {
+ var data = display_data || current_data;
+
display_lines = [];
- // The headline.
+ // 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', 8, true);
+ } else {
+ display_lines.push(null);
+ }
+ update_history();
+
+ // The headline. Names are always fetched from current_data;
+ // the rest can depend a bit.
var headline;
- if (data['position']['player_w'] && data['position']['player_b']) {
- headline = data['position']['player_w'] + '–' +
- data['position']['player_b'] + ', analysis';
+ if (current_data &&
+ current_data['position']['player_w'] && current_data['position']['player_b']) {
+ headline = current_data['position']['player_w'] + '–' +
+ current_data['position']['player_b'] + ', analysis';
} else {
headline = 'Analysis';
}
- var last_move;
- if (data['position']['last_move'] !== 'none') {
- if (data['position']['toplay'] == 'W') {
- last_move = (data['position']['move_num']-1) + '… ';
- } else {
- last_move = data['position']['move_num'] + '. ';
- }
- last_move += data['position']['last_move'];
+ var last_move;
+ if (display_data) {
+ // Displaying some non-current position, pick out the last move
+ // from the history. This will work even if the fetch failed.
+ last_move = format_move_with_number(
+ current_display_line.pretty_pv[current_display_move],
+ Math.floor((current_display_move + 1) / 2) + 1,
+ (current_display_move % 2 == 1));
+ headline += ' after ' + last_move;
+ } else if (data['position']['last_move'] !== 'none') {
+ last_move = format_move_with_number(
+ data['position']['last_move'],
+ data['position']['move_num'],
+ data['position']['toplay'] == 'W');
headline += ' after ' + last_move;
} else {
last_move = null;
}
-
$("#headline").text(headline);
- // The engine id.
- if (data['id'] && data['id']['name'] !== null) {
- $("#engineid").text(data['id']['name']);
- }
-
- // The score.
- if (data['score'] !== null) {
- $("#score").text(data['score']);
- }
-
+ // The <title> contains a very brief headline.
var title_elems = [];
if (data['short_score'] !== undefined && data['short_score'] !== null) {
- title_elems.push(data['short_score']);
+ title_elems.push(data['short_score'].replace(/^ /, ""));
}
if (last_move !== null) {
title_elems.push(last_move);
document.title = 'analysis.sesse.net';
}
+ // The last move (shown by highlighting the from and to squares).
+ 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) {
+ // 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 = new Chess();
+ for (var i = 0; i <= current_display_move; ++i) {
+ hiddenboard.move(current_display_line.pretty_pv[i]);
+ }
+ var moves = hiddenboard.history({ verbose: true });
+ var last_move = moves.pop();
+ highlight_from = last_move.from;
+ highlight_to = last_move.to;
+ } else {
+ highlight_from = highlight_to = undefined;
+ }
+ update_highlight();
+
+ if (data['failed']) {
+ $("#score").text("No analysis for this move");
+ $("#pv").empty();
+ $("#searchstats").html(" ");
+ $("#refutationlines").empty();
+ $("#whiteclock").empty();
+ $("#blackclock").empty();
+ refutation_lines = [];
+ update_refutation_lines();
+ clear_arrows();
+ update_displayed_line();
+ return;
+ }
+
+ update_clock();
+
+ // The engine id.
+ if (data['id'] && data['id']['name'] !== null) {
+ $("#engineid").text(data['id']['name']);
+ }
+
+ // The score.
+ if (data['score'] !== null) {
+ $("#score").text(data['score']);
+ }
+
// The search stats.
if (data['tablebase'] == 1) {
$("#searchstats").text("Tablebase result");
fen = data['position']['fen'];
update_displayed_line();
- if (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 {
- highlight_from = highlight_to = undefined;
- }
- update_highlight();
-
- // Print the history.
- if (data['position']['history']) {
- add_pv('start', data['position']['history'], 1, 'W', 8, true);
- } else {
- display_lines.push(null);
- }
- update_history();
-
// Print the PV.
- $("#pv").html(add_pv(data['position']['fen'], data['pv_uci'], data['position']['move_num'], data['position']['toplay']));
+ $("#pv").html(add_pv(data['position']['fen'], data['pv_pretty'], data['position']['move_num'], data['position']['toplay']));
// Update the PV arrow.
clear_arrows();
}
}
+var update_clock = function() {
+ clearTimeout(clock_timer);
+
+ var data = displayed_analysis_data || current_analysis_data;
+ if (data['position']) {
+ var result = data['position']['result'];
+ if (result === '1-0') {
+ $("#whiteclock").text("1");
+ $("#blackclock").text("0");
+ $("#whiteclock").removeClass("running-clock");
+ $("#blackclock").removeClass("running-clock");
+ return;
+ }
+ if (result === '1/2-1/2') {
+ $("#whiteclock").text("1/2");
+ $("#blackclock").text("1/2");
+ $("#whiteclock").removeClass("running-clock");
+ $("#blackclock").removeClass("running-clock");
+ return;
+ }
+ if (result === '0-1') {
+ $("#whiteclock").text("0");
+ $("#blackclock").text("1");
+ $("#whiteclock").removeClass("running-clock");
+ $("#blackclock").removeClass("running-clock");
+ return;
+ }
+ }
+
+ var white_clock = "";
+ var black_clock = "";
+
+ // Static clocks.
+ if (data['position'] &&
+ data['position']['white_clock'] &&
+ data['position']['black_clock']) {
+ white_clock = data['position']['white_clock'].replace(/:[0-5][0-9]$/, "");
+ black_clock = data['position']['black_clock'].replace(/:[0-5][0-9]$/, "");
+ }
+
+ // Dynamic clock (only one, obviously).
+ var color;
+ if (data['position']['white_clock_target']) {
+ color = "white";
+ } else if (data['position']['black_clock_target']) {
+ color = "black";
+ }
+ if (color) {
+ var now = new Date().getTime() + client_clock_offset_ms;
+ var remaining_ms = data['position'][color + '_clock_target'] * 1000 - now;
+ if (color === "white") {
+ white_clock = format_clock(remaining_ms);
+ } else {
+ black_clock = format_clock(remaining_ms);
+ }
+
+ // See when the clock will change next, and update right after that.
+ var next_update_ms = remaining_ms % 60000 + 100;
+ clock_timer = setTimeout(update_clock, next_update_ms);
+ }
+
+ $("#whiteclock").text(white_clock);
+ $("#blackclock").text(black_clock);
+
+ if (toplay == 'W') {
+ $("#whiteclock").addClass("running-clock");
+ $("#blackclock").removeClass("running-clock");
+ } else {
+ $("#whiteclock").removeClass("running-clock");
+ $("#blackclock").addClass("running-clock");
+ }
+}
+
+/**
+ * @param {Number} remaining_ms
+ */
+var format_clock = function(remaining_ms) {
+ if (remaining_ms <= 0) {
+ return "00:00";
+ }
+
+ var remaining = Math.floor(remaining_ms / 1000);
+ var seconds = remaining % 60;
+ remaining = (remaining - seconds) / 60;
+ var minutes = remaining % 60;
+ remaining = (remaining - minutes) / 60;
+ var hours = remaining;
+ return format_2d(hours) + ":" + format_2d(minutes);
+}
+
+/**
+ * @param {Number} x
+ */
+var format_2d = function(x) {
+ if (x >= 10) {
+ return x;
+ } else {
+ return "0" + x;
+ }
+}
+
+/**
+ * @param {string} move
+ * @param {Number} move_num
+ * @param {boolean} white_to_play
+ */
+var format_move_with_number = function(move, move_num, white_to_play) {
+ var ret;
+ if (white_to_play) {
+ ret = (move_num - 1) + '… ';
+ } else {
+ ret = move_num + '. ';
+ }
+ ret += move;
+ return ret;
+}
+
/**
* @param {boolean} sort_by_score
*/
var resort_refutation_lines = function(sort_by_score) {
sort_refutation_lines_by_score = sort_by_score;
+ if (supports_html5_storage()) {
+ localStorage['sort_refutation_lines_by_score'] = sort_by_score ? 1 : 0;
+ }
update_refutation_lines();
}
window['resort_refutation_lines'] = resort_refutation_lines;
if (line_num == -1) {
current_display_line = null;
current_display_move = null;
+ if (displayed_analysis_data) {
+ // TODO: Support exiting to history position if we are in an
+ // analysis line of a history position.
+ displayed_analysis_data = null;
+ update_board(current_analysis_data, displayed_analysis_data);
+ }
} else {
current_display_line = display_lines[line_num];
current_display_move = move_num;
}
+ current_display_line_is_history = (line_num == 0);
+
+ update_historic_analysis();
update_displayed_line();
update_highlight();
redraw_arrows();
if (current_display_move > -1) {
--current_display_move;
}
+ update_historic_analysis();
update_displayed_line();
}
window['prev_move'] = prev_move;
if (current_display_line && current_display_move < current_display_line.pretty_pv.length - 1) {
++current_display_move;
}
+ update_historic_analysis();
update_displayed_line();
}
window['next_move'] = next_move;
+var update_historic_analysis = function() {
+ if (!current_display_line_is_history) {
+ return;
+ }
+ if (current_display_move == current_display_line.pretty_pv.length - 1) {
+ displayed_analysis_data = null;
+ update_board(current_analysis_data, displayed_analysis_data);
+ }
+
+ // Fetch old analysis for this line if it exists.
+ var hiddenboard = new Chess();
+ for (var i = 0; i <= current_display_move; ++i) {
+ hiddenboard.move(current_display_line.pretty_pv[i]);
+ }
+ var filename = "/history/move" + (current_display_move + 1) + "-" +
+ hiddenboard.fen().replace(/ /g, '_').replace(/\//g, '-') + ".json";
+
+ $.ajax({
+ url: filename
+ }).done(function(data, textstatus, xhr) {
+ displayed_analysis_data = data;
+ update_board(current_analysis_data, displayed_analysis_data);
+ }).fail(function() {
+ displayed_analysis_data = {'failed': true};
+ update_board(current_analysis_data, displayed_analysis_data);
+ });
+}
+
var update_displayed_line = function() {
if (highlighted_move !== null) {
highlighted_move.removeClass('highlight');
} else {
$("#prevmove").html("<a href=\"javascript:prev_move();\">Previous</a></span>");
}
- if (current_display_move == current_display_line.uci_pv.length - 1) {
+ if (current_display_move == current_display_line.pretty_pv.length - 1) {
$("#nextmove").html("Next");
} else {
$("#nextmove").html("<a href=\"javascript:next_move();\">Next</a></span>");
var hiddenboard = new Chess();
hiddenboard.load(current_display_line.start_fen);
for (var i = 0; i <= current_display_move; ++i) {
- hiddenboard.move(ucimove_to_chessjs_move(current_display_line.uci_pv[i]));
+ hiddenboard.move(current_display_line.pretty_pv[i]);
}
highlighted_move = $("#automove" + current_display_line.line_number + "-" + current_display_move);
board.position(hiddenboard.fen());
}
-var ucimove_to_chessjs_move = function(move) {
- var source = move.substr(0, 2);
- var target = move.substr(2, 2);
- var promo = move.substr(4, 1);
-
- if (promo === '') {
- return { from: source, to: target };
+/**
+ * @param {boolean} param_enable_sound
+ */
+var set_sound = function(param_enable_sound) {
+ enable_sound = param_enable_sound;
+ if (enable_sound) {
+ $("#soundon").html("<strong>On</strong>");
+ $("#soundoff").html("<a href=\"javascript:set_sound(false)\">Off</a>");
+
+ // Seemingly at least Firefox prefers MP3 over Opus; tell it otherwise,
+ // and also preload the file since the user has selected audio.
+ var ding = document.getElementById('ding');
+ if (ding && ding.canPlayType && ding.canPlayType('audio/ogg; codecs="opus"') === 'probably') {
+ ding.src = 'ding.opus';
+ ding.load();
+ }
} else {
- return { from: source, to: target, promotion: promo };
+ $("#soundon").html("<a href=\"javascript:set_sound(true)\">On</a>");
+ $("#soundoff").html("<strong>Off</strong>");
+ }
+ if (supports_html5_storage()) {
+ localStorage['enable_sound'] = enable_sound ? 1 : 0;
}
}
+window['set_sound'] = set_sound;
var init = function() {
unique = get_unique();
+ // Load settings from HTML5 local storage if available.
+ if (supports_html5_storage() && localStorage['enable_sound']) {
+ set_sound(parseInt(localStorage['enable_sound']));
+ } else {
+ set_sound(false);
+ }
+ if (supports_html5_storage() && localStorage['sort_refutation_lines_by_score']) {
+ sort_refutation_lines_by_score = parseInt(localStorage['sort_refutation_lines_by_score']);
+ } else {
+ sort_refutation_lines_by_score = true;
+ }
+
// Create board.
board = new window.ChessBoard('board', 'start');