* Version of this script. If the server returns a version larger than
* this, it is a sign we should reload to upgrade ourselves.
*
- * @type {Number}
+ * @type {number}
* @const
* @private */
-let SCRIPT_VERSION = 2021021300;
+let SCRIPT_VERSION = 2023122700;
/**
* The current backend URL.
/** Currently displayed refutation lines (on-screen).
* Can either come from the current_analysis_data, displayed_analysis_data,
- * or hash_refutation_lines.
+ * or hash_refutation_lines (choose_displayed_refutation_lines() chooses which one).
+ *
+ * @typedef {{
+ * score: Array,
+ * depth: string,
+ * pv: Array.<string>,
+ * move: string
+ * }}
+ * @private
*/
-let refutation_lines = [];
+var RefutationLine;
/** Refutation lines from current hash probe.
- *
* If non-null, will override refutation lines from the base position.
- * Note that these are relative to display_fen, not base_fen.
+ *
+ * @type {Array.<RefutationLine>}
*/
let hash_refutation_lines = null;
-/** @type {!number} @private */
-let move_num = 1;
-
-/** @type {!string} @private */
-let toplay = 'W';
+/**
+ * What FEN hash_refutation_lines is relative to.
+ */
+let hash_refutation_lines_base_fen = null;
/** @type {number} @private */
let ims = 0;
/** @type {?number} @private */
let unique = null;
+/** @type {?string} @private */
+let admin_password = null;
+
/** @type {boolean} @private */
let enable_sound = false;
/** @typedef {{
* start_fen: string,
* pv: Array.<string>,
- * move_num: number,
- * toplay: string,
* scores: Array<{first_move: number, score: Object}>,
* start_display_move_num: number
- * }} DisplayLine
+ * }}
*
* "start_display_move_num" is the (half-)move number to start displaying the PV at.
- * "score" is also evaluated at this point.
+ * "score" is also evaluated at this point. scores can be empty and is frequently
+ * sparse; it's generally only really useful for history (we obviously don't know
+ * much about how the score will * move during e.g. a PV, except that a mate/TB
+ * counter might go down).
*/
+var DisplayLine;
/** All PVs that we currently know of.
*
* The current timer to fire off a request to get main analysis (not history),
* if any, so that we can abort it.
*
- * @type {?Number}
+ * @type {?number}
* @private
*/
let current_analysis_request_timer = null;
* The current timer to display hash probe information (it could be waiting on the
* board to stop animating), if any, so that we can abort it.
*
- * @type {?Number}
+ * @type {?number}
* @private
*/
let current_hash_display_timer = null;
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'])) {
+ old_data['position']['fen'] !== new_data['position']['fen']) {
ding.play();
}
}
}
}
+/** @param {!string} fen
+ * @return {!string}
+ *
+ * Return whose side it is to play (w or b), given a FEN.
+ */
+function find_toplay(fen) {
+ return fen.split(' ')[1];
+}
+
+/** @param {!string} fen
+ * @return {!number}
+ *
+ * Return the move clock, starting from 1. See also find_toplay().
+ */
+function find_move_num(fen) {
+ return parseInt(fen.split(' ')[5]);
+}
+
+/** @param {!string} fen
+ * @return {!number}
+ *
+ * Return the half-move clock, starting from 0 (and never resetting).
+ */
+function find_halfmove_num(fen) {
+ let move_num = find_move_num(fen);
+ let toplay = find_toplay(fen);
+ return (move_num - 1) * 2 + (toplay === 'w' ? 0 : 1);
+}
+
/** @param {!number} x
* @return {!number}
*/
moves.push(move);
}
}
- moves = moves.sort(function(a, b) { return compare_by_score(data['refutation_lines'], data['position']['toplay'] === 'B', a, b) });
+ let toplay = find_toplay(data['position']['fen']);
+ moves = moves.sort(function(a, b) { return compare_by_score(data['refutation_lines'], toplay, a, b) });
moves.unshift(data['pv'][0]);
return moves;
/**
* @param {!string} start_fen
* @param {Array.<string>} pv
- * @param {number} move_num
- * @param {!string} toplay
- * @param {Array<{ first_move: integer, score: Object }>} scores
+ * @param {Array<{ first_move: number, score: Object }>} scores
* @param {number} start_display_move_num
* @param {number=} opt_limit
* @param {boolean=} opt_showlast
*/
-function add_pv(start_fen, pv, move_num, toplay, scores, start_display_move_num, opt_limit, opt_showlast) {
+function add_pv(start_fen, pv, scores, start_display_move_num, opt_limit, opt_showlast) {
display_lines.push({
start_fen: start_fen,
pv: pv,
- move_num: parseInt(move_num),
- toplay: toplay,
scores: scores,
start_display_move_num: start_display_move_num
});
function print_pv(line_num, splicepos, opt_limit, opt_showlast) {
let display_line = display_lines[line_num];
let pv = display_line.pv;
- let move_num = display_line.move_num;
- let toplay = display_line.toplay;
-
- // Truncate PV at the start if needed.
- let start_display_move_num = display_line.start_display_move_num;
- if (start_display_move_num > 0) {
- pv = pv.slice(start_display_move_num);
- let to_add = start_display_move_num;
- if (toplay === 'B') {
- ++move_num;
- toplay = 'W';
- --to_add;
- }
- if (to_add % 2 == 1) {
- toplay = 'B';
- --to_add;
- }
- move_num += to_add / 2;
- if (splicepos !== null && splicepos > 0) {
- --splicepos;
- }
- }
+ let halfmove_num = find_halfmove_num(display_line.start_fen) + 2; // From two, for simplicity.
let ret = document.createDocumentFragment();
- let in_tb = false;
- let i = 0;
- 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.
+
+ // Truncate PV at the start if needed.
+ let to_skip = display_line.start_display_move_num;
+ if (opt_limit && opt_showlast && pv.length - to_skip > opt_limit) {
+ // Explicit (UI-visible) truncation from the start, for the history.
ret.appendChild(document.createTextNode('('));
let link = document.createElement('a');
link.className = 'move';
link.textContent = '…';
ret.appendChild(link);
ret.appendChild(document.createTextNode(') '));
- i = pv.length - opt_limit;
- if (i % 2 == 1) {
- ++i;
+ to_skip = pv.length - opt_limit;
+ to_skip += to_skip % 2; // Make sure it starts on a white move.
+ }
+ if (to_skip > 0) {
+ pv = pv.slice(to_skip);
+ halfmove_num += to_skip;
+ if (splicepos !== null) {
+ splicepos -= to_skip;
+ if (splicepos < 0) {
+ splicepos = 0;
+ }
}
- move_num += i / 2;
- } else if (toplay == 'B' && pv.length > 0) {
- ret.appendChild(document.createTextNode(move_num + '. … '));
}
- for (; i < pv.length; ++i) {
- let link = document.createElement('a');
- link.className = 'move';
- link.setAttribute('id', 'automove' + line_num + '-' + i);
- link.textContent = pv[i];
- link.href = 'javascript:show_line(' + line_num + ', ' + i + ');';
+
+ // The initial move number needs to go before any (TB: …) marker.
+ if (halfmove_num % 2 == 1) {
+ // Black move.
+ ret.appendChild(document.createTextNode((halfmove_num - 1) / 2 + '. … '));
+ } else {
+ // White move.
+ ret.appendChild(document.createTextNode(halfmove_num / 2 + '. '));
+ }
+ let in_tb = false;
+ for (let i = 0; i < pv.length; ++i, ++halfmove_num) {
+ let prefix = '';
if (splicepos === i) {
- ret.appendChild(document.createTextNode('(TB: '));
+ prefix = '(TB:';
in_tb = true;
}
- if (toplay == 'B' && i == 0) {
- ++move_num;
- toplay = 'W';
- } else if (toplay == 'W') {
+ if (halfmove_num % 2 == 0 && i != 0) {
if (i > opt_limit && !opt_showlast) {
if (in_tb) {
- ret.appendChild(document.createTextNode(')'));
+ prefix += ')';
}
- ret.appendChild(document.createTextNode(' (…)'));
+ ret.appendChild(document.createTextNode(prefix + ' (…)'));
return ret;
}
- ret.appendChild(document.createTextNode(' ' + move_num + '. '));
- ++move_num;
- toplay = 'B';
+ prefix += ' ' + (halfmove_num / 2) + '. ';
} else {
- ret.appendChild(document.createTextNode(' '));
- toplay = 'W';
+ prefix += ' ';
}
+ ret.appendChild(document.createTextNode(prefix));
+
+ let link = document.createElement('a');
+ link.className = 'move';
+ link.setAttribute('id', 'automove' + line_num + '-' + i);
+ link.textContent = pv[i];
+ link.href = 'javascript:show_line(' + line_num + ', ' + i + ');';
ret.appendChild(link);
}
if (in_tb) {
}
window['collapse_history'] = collapse_history;
-/** Update the HTML display of multi-PV from the global "refutation_lines".
+function choose_displayed_refutation_lines() {
+ if (hash_refutation_lines) {
+ // If we're in hash exploration, that takes precedence.
+ return [hash_refutation_lines, hash_refutation_lines_base_fen];
+ } else {
+ let data = displayed_analysis_data || current_analysis_data;
+ return [data['refutation_lines'], data['position']['fen']];
+ }
+}
+
+/** Update the HTML display of multi-PV.
*
* Also recreates the global "display_lines".
*/
function update_refutation_lines() {
- if (base_fen === null) {
+ const [refutation_lines, refutation_lines_base_fen] = choose_displayed_refutation_lines();
+ if (!refutation_lines) {
return;
}
if (display_lines.length > 2) {
moves.push(move);
}
- let invert = (toplay === 'B');
+ let invert = (find_toplay(refutation_lines_base_fen) === 'b');
if (current_display_line && current_display_move % 2 == 0 && !current_display_line_is_history) {
invert = !invert;
}
let pv_td = document.createElement("td");
tr.appendChild(pv_td);
pv_td.classList.add("pv");
- pv_td.append(add_pv(base_fen, base_line.concat([ line['move'] ]), move_num, toplay, scores, start_display_move_num));
+ pv_td.append(add_pv(refutation_lines_base_fen, base_line.concat([ line['move'] ]), scores, start_display_move_num));
tbl.append(tr);
continue;
let pv_td = document.createElement("td");
tr.appendChild(pv_td);
pv_td.classList.add("pv");
- pv_td.append(add_pv(base_fen, base_line.concat(line['pv']), move_num, toplay, scores, start_display_move_num, 10));
+ pv_td.append(add_pv(refutation_lines_base_fen, base_line.concat(line['pv']), scores, start_display_move_num, 10));
tbl.append(tr);
}
// unconditionally taken from current_data (we're not interested in
// historic history).
if (current_data['position']['history']) {
- let start = (current_data['position'] && current_data['position']['start_fen']) ? current_data['position']['start_fen'] : 'start';
- add_pv(start, current_data['position']['history'], 1, 'W', null, 0, 8, true);
+ let start = (current_data['position'] && current_data['position']['start_fen']) ? current_data['position']['start_fen'] : 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
+ add_pv(start, current_data['position']['history'], null, 0, 8, true);
} else {
display_lines.push(null);
}
headline = 'Analysis';
}
- // Credits, where applicable. Note that we don't want the footer to change a lot
- // when e.g. viewing history, so if any of these changed during the game,
- // use the current one still.
- if (current_data['using_lomonosov']) {
- document.getElementById("lomonosov").style.display = null;
- } else {
- document.getElementById("lomonosov").style.display = 'none';
- }
-
// Credits: The engine name/version.
if (current_data['engine'] && current_data['engine']['name'] !== null) {
document.getElementById("engineid").textContent = current_data['engine']['name'];
} else if (data['position']['last_move'] !== 'none') {
// Find the previous move.
let previous_move_num, previous_toplay;
- if (data['position']['toplay'] == 'B') {
- previous_move_num = data['position']['move_num'];
- previous_toplay = 'W';
+ let fen = data['position']['fen'];
+ if (find_toplay(fen) === 'b') {
+ previous_move_num = find_move_num(fen);
+ previous_toplay = 'w';
} else {
- previous_move_num = data['position']['move_num'] - 1;
- previous_toplay = 'B';
+ previous_move_num = find_move_num(fen) - 1;
+ previous_toplay = 'b';
}
last_move = format_move_with_number(
data['position']['last_move'],
previous_move_num,
- previous_toplay == 'W');
+ previous_toplay == 'w');
headline += ' after ' + last_move;
} else {
last_move = null;
document.getElementById("score").textContent = "No analysis for this move";
document.getElementById("pvtitle").textContent = "PV:";
document.getElementById("pv").replaceChildren();
- document.getElementById("searchstats").textContent = " ";
+ document.getElementById("searchstats").innerHTML = " ";
document.getElementById("refutationlines").replaceChildren();
document.getElementById("whiteclock").replaceChildren();
document.getElementById("blackclock").replaceChildren();
- refutation_lines = [];
update_refutation_lines();
clear_arrows();
update_displayed_line();
// The search stats.
if (data['searchstats']) {
document.getElementById("searchstats").textContent = data['searchstats'];
- } else if (data['tablebase'] == 1) {
- document.getElementById("searchstats").textContent = "Tablebase result";
} else if (data['nodes'] && data['nps'] && data['depth']) {
let stats = thousands(data['nodes']) + ' nodes, ' + thousands(data['nps']) + ' nodes/sec, depth ' + data['depth'] + ' ply';
if (data['seldepth']) {
} else {
document.getElementById("searchstats").textContent = "";
}
+ if (admin_password !== null) {
+ document.getElementById("searchstats").innerHTML += " | <span style=\"color: red;\">ADMIN MODE (if password is right)</span>";
+ }
// Update the board itself.
base_fen = data['position']['fen'];
document.getElementById("pvtitle").textContent = "PV:";
let scores = [{ first_move: -1, score: data['score'] }];
- document.getElementById("pv").replaceChildren(add_pv(data['position']['fen'], data['pv'], data['position']['move_num'], data['position']['toplay'], scores, 0));
+ document.getElementById("pv").replaceChildren(add_pv(data['position']['fen'], data['pv'], scores, 0));
// Update the PV arrow.
clear_arrows();
+ let toplay = find_toplay(data['position']['fen']);
if (data['pv'].length >= 1) {
let hiddenboard = new Chess(base_fen);
hiddenboard.move(data['pv'][i + 1]); // To keep continuity.
}
- let alt_moves = find_nonstupid_moves(data, 30, data['position']['toplay'] === 'B');
+ let alt_moves = find_nonstupid_moves(data, 30, toplay === 'b');
for (let i = 1; i < alt_moves.length && i < 3; ++i) {
hiddenboard = new Chess(base_fen);
let move = patch_move(hiddenboard.move(alt_moves[i]));
// See if all semi-reasonable moves have only one possible response.
if (data['pv'].length >= 2) {
- let nonstupid_moves = find_nonstupid_moves(data, 300, data['position']['toplay'] === 'B');
+ let nonstupid_moves = find_nonstupid_moves(data, 300, toplay === 'b');
let hiddenboard = new Chess(base_fen);
hiddenboard.move(data['pv'][0]);
let response = hiddenboard.move(data['pv'][1]);
}
}
- // Update the refutation lines.
base_fen = data['position']['fen'];
- move_num = parseInt(data['position']['move_num']);
- toplay = data['position']['toplay'];
- refutation_lines = hash_refutation_lines || data['refutation_lines'];
update_refutation_lines();
// Update the sparkline last, since its size depends on how everything else reflowed.
}
}
if (first_move_num !== undefined) {
- let last_move_num = data['position']['move_num'] * 2 - 3;
- if (data['position']['toplay'] === 'B') {
+ let fen = data['position']['fen'];
+ let last_move_num = find_move_num(fen) * 2 - 3;
+ if (find_toplay(fen) === 'b') {
++last_move_num;
}
// This matches what DGT clocks do.
let show_seconds = (white_clock_ms < 60 * 20 * 1000 || black_clock_ms < 60 * 20 * 1000);
- if (color) {
+ if (color && remaining_ms > 0) {
// See when the clock will change next, and update right after that.
let next_update_ms;
if (show_seconds) {
}
/**
- * @param {Number} remaining_ms
+ * @param {number} remaining_ms
* @param {boolean} show_seconds
*/
function format_clock(remaining_ms, show_seconds) {
}
/**
- * @param {Number} x
+ * @param {number} x
*/
function format_2d(x) {
if (x >= 10) {
/**
* @param {string} move
- * @param {Number} move_num Move number of this move.
+ * @param {number} move_num Move number of this move.
* @param {boolean} white_to_play Whether white is to play this move.
*/
function format_move_with_number(move, move_num, white_to_play) {
/**
* @param {string} move
- * @param {Number} halfmove_num Half-move number that is to be played,
+ * @param {number} halfmove_num Half-move number that is to be played,
* starting from 0.
*/
function format_halfmove_with_number(move, halfmove_num) {
/**
* @param {Object} data
- * @param {Number} halfmove_num
+ * @param {number} halfmove_num
*/
function format_tooltip(data, halfmove_num) {
if (data['score_history'][halfmove_num + 1] ||
}
}
-/**
- * @param {boolean} truncate_history
- */
-function set_truncate_history(truncate_history) {
- truncate_display_history = truncate_history;
- update_refutation_lines();
-}
-window['set_truncate_history'] = set_truncate_history;
-
/**
* @param {number} line_num
* @param {number} move_num
document.getElementById("pvtitle").textContent = "Exploring:";
current_display_line.start_display_move_num = 0;
display_lines.push(current_display_line);
- document.getElementById("pv").append(print_pv(display_lines.length - 1, null)); // FIXME
+ document.getElementById("pv").replaceChildren(print_pv(display_lines.length - 1, null)); // FIXME
display_line_num = display_lines.length - 1;
// Clear out the PV, so it's not selected by anything later.
function set_board_position(new_fen) {
board_is_animating = true;
let old_fen = board.fen();
- board.position(new_fen);
+ let animate = old_fen !== '8/8/8/8/8/8/8/';
+ board.position(new_fen, animate);
if (board.fen() === old_fen) {
board_is_animating = false;
}
}
current_hash_display_timer = null;
hash_refutation_lines = data['lines'];
+ hash_refutation_lines_base_fen = fen;
update_board();
}
let best_move = null;
let best_move_score = null;
+ let refutation_lines = choose_displayed_refutation_lines()[0];
for (let move in refutation_lines) {
let line = refutation_lines[move];
if (!line['score']) {
if (move === null) return 'snapback';
}
+/**
+ * If we are in admin mode, send this move to the backend.
+ *
+ * @param {string} fen
+ * @param {string} move
+ */
+function send_chosen_move(fen, move) {
+ if (admin_password !== null) {
+ let history = current_analysis_data['position']['history'];
+ let url = '/manual-override.pl';
+ url += '?fen=' + encodeURIComponent(fen);
+ url += '&history=' + encodeURIComponent(JSON.stringify(history));
+ url += '&move=' + encodeURIComponent(move);
+ url += '&player_w=' + encodeURIComponent(current_analysis_data['position']['player_w']);
+ url += '&player_b=' + encodeURIComponent(current_analysis_data['position']['player_b']);
+ url += '&password=' + encodeURIComponent(admin_password);
+
+ console.log(fen, history);
+ fetch(url); // Ignore the result.
+ }
+}
+
function onSnapEnd(source, target) {
if (source === target && recommended_move !== null) {
source = recommended_move.from;
promotion: 'q' // NOTE: always promote to a queen for example simplicity
});
+ if (admin_password !== null) {
+ send_chosen_move(display_fen, move.san);
+ return;
+ }
+
// Move ahead on the line we're on -- this includes history if we've
// gone backwards.
if (current_display_line &&
set_sound(false);
}
+ let admin_match = window.location.href.match(/\?password=([a-zA-Z0-9_-]+)/);
+ if (admin_match !== null) {
+ admin_password = admin_match[1];
+ }
+
// Create board.
board = new window.ChessBoard('board', {
onMoveEnd: function() { board_is_animating = false; },
document.getElementById("board").addEventListener('mousedown', mousedownSquare);
document.getElementById("board").addEventListener('mouseup', mouseupSquare);
+ if (window['inline_json']) {
+ let j = window['inline_json'];
+ process_update_response(j['data'], { 'get': (h) => j['headers'][h] });
+ delete window['inline_json'];
+ }
request_update();
window.addEventListener('resize', function() {
board.resize();
update_board_highlight();
redraw_arrows();
});
+ new ResizeObserver(() => update_sparkline(displayed_analysis_data || current_analysis_data)).observe(document.getElementById('scoresparkcontainer'));
window.addEventListener('keyup', function(event) {
if (event.which == 39) { // Left arrow.
next_move();