X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=www%2Fjs%2Fbook.js;h=dce293b2a8e70392d083b271b68fac7b5fa63059;hb=652025e5e5b191447bae787e351bb97c126ec7a5;hp=1f2ec371b7d2647635edc7c2036da09cc6dc4098;hpb=faadf5a106a0fddaa4ae27b67a20af0ea94adaaa;p=remoteglot-book diff --git a/www/js/book.js b/www/js/book.js index 1f2ec37..dce293b 100644 --- a/www/js/book.js +++ b/www/js/book.js @@ -1,8 +1,15 @@ (function() { var board = null; -var history = []; +var game = new Chess(); +var fens = []; var move_override = 0; +var includetransp = true; +var stockfish = new Worker('/js/stockfish.js'); +var engine_running = false; +var engine_replacement_callback = null; +var recommended_move = null; +var reverse_dragging_from = null; var entity_map = { "&": "&", @@ -18,38 +25,57 @@ function escape_html(string) { }); } -var get_game = function() { - var game = new Chess(); - for (var i = 0; i < move_override; ++i) { - game.move(history[i]); +var current_display_fen = function() { + if (move_override == 0) { + return 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; + } else { + return fens[move_override - 1]; } - return game; } var update = function() { var text = ""; + var history = game.history({ verbose: true }); for (var i = 0; i < history.length; ++i) { if (i % 2 == 0) { text += (i/2 + 1) + ". "; } - if (i == move_override) { - text += '' + history[i] + ''; + if (i + 1 == move_override) { + text += '' + history[i].san + ''; } else { - text += history[i]; + text += '' + history[i].san + ''; } text += " "; } $('#gamehistory').html(text); - var game = get_game(); - board.position(game.fen()); + if (board.fen() != current_display_fen()) { + board.position(current_display_fen()); + } + + $("#board").find('.square-55d63').removeClass('nonuglyhighlight'); + if (move_override > 0) { + var last_move = history[move_override - 1]; + var highlight_from = last_move.from; + var highlight_to = last_move.to; + $("#board").find('.square-' + highlight_from).addClass('nonuglyhighlight'); + $("#board").find('.square-' + highlight_to).addClass('nonuglyhighlight'); + } + fetch_analysis(); } +var get_history_url = function() { + var history = game.history({ verbose: true }).map(function(x) { return x.san; }); + history.length = move_override; + return '/?' + history.join(','); +} + var fetch_analysis = function() { - var game = get_game(); + var fen = current_display_fen(); $.ajax({ - url: "/opening-stats.pl?fen=" + encodeURIComponent(game.fen()) + url: "/opening-stats.pl?fen=" + encodeURIComponent(fen) + + ";includetransp=" + (includetransp ? 1 : 0) }).done(function(data, textstatus, xhr) { show_lines(data, game); }); @@ -71,6 +97,8 @@ var headings = [ [ "Move", TYPE_MOVE ], [ "Games", TYPE_INTEGER ], [ "%", TYPE_RATIO ], + [ "CGames", TYPE_INTEGER ], + [ "Hum", TYPE_RATIO ], [ "Win%", TYPE_RATIO ], [ "WWin", TYPE_INTEGER ], [ "%WW", TYPE_RATIO ], @@ -138,6 +166,7 @@ var show_lines = function(data, game) { } var lines = []; + var transpose_only = []; for (var i = 0; i < moves.length; ++i) { var move = moves[i]; var line = []; @@ -145,15 +174,22 @@ var show_lines = function(data, game) { var white = parseInt(move['white']); var draw = parseInt(move['draw']); var black = parseInt(move['black']); + var computer = parseInt(move['computer']); line.push(move['move']); // Move. + transpose_only.push(move['transpose_only']); var num = white + draw + black; line.push(num); // N. line.push(num / total_num); // %. + line.push(computer); // CGames. + + // Adjust so that the human index is 50% overall. + var exp = Math.log(0.5) / Math.log(data['computer_games'] / data['total_games']); + line.push(1.0 - Math.pow(computer / num, exp)); // Hum. // Win%. var white_win_ratio = (white + 0.5 * draw) / num; - var win_ratio = (game.turn() == 'w') ? white_win_ratio : 1.0 - white_win_ratio; + var win_ratio = (move_override % 2 == 0) ? white_win_ratio : 1.0 - white_win_ratio; line.push(win_ratio); line.push(white); // WWin. @@ -173,7 +209,7 @@ var show_lines = function(data, game) { var win_elo = -400.0 * Math.log(1.0 / white_win_ratio - 1.0) / Math.LN10; win_elo -= (move['white_avg_elo'] - move['black_avg_elo']); white_win_ratio = 1.0 / (1.0 + Math.pow(10, win_elo / -400.0)); - win_ratio = (game.turn() == 'w') ? white_win_ratio : 1.0 - white_win_ratio; + win_ratio = (move_override % 2 == 0) ? white_win_ratio : 1.0 - white_win_ratio; line.push(win_ratio); } else { line.push(null); @@ -195,6 +231,8 @@ var show_lines = function(data, game) { if (line[0] === undefined) { $(tr).addClass("totals"); + } else if (transpose_only[i]) { + $(tr).addClass("transponly"); } for (var j = 0; j < line.length; ++j) { @@ -217,11 +255,19 @@ var show_lines = function(data, game) { td.appendChild(move_a); $(move_a).text(line[j]); } else if (headings[j][1] == TYPE_INTEGER) { - add_td(tr, line[j]); + add_td(tr, line[j] || 0); } else if (headings[j][1] == TYPE_FLOAT) { - add_td(tr, line[j].toFixed(1)); + if (isNaN(line[j]) || !isFinite(line[j])) { + add_td(tr, ''); + } else { + add_td(tr, line[j].toFixed(1)); + } } else { - add_td(tr, (100.0 * line[j]).toFixed(1) + "%"); + if (isNaN(line[j]) || !isFinite(line[j])) { + add_td(tr, ''); + } else { + add_td(tr, (100.0 * line[j]).toFixed(1) + "%"); + } } } @@ -229,16 +275,36 @@ var show_lines = function(data, game) { } } -var make_move = function(move) { - if (move_override < history.length && history[move_override] == move) { +var set_includetransp = function(value) { + includetransp = value; + update(); +} +window['set_includetransp'] = set_includetransp; + +var make_move = function(move, do_update) { + var history = game.history({ verbose: true }); + if (move_override < history.length && history[move_override].san == move) { // User effectively only moved forward in history. ++move_override; } else { - history.length = move_override; - history.push(move); - move_override = history.length; + var moves = game.history(); + // Truncate the history if needed. + if (move_override < moves.length) { + game = new Chess(); + for (var i = 0; i < move_override; ++i) { + game.move(moves[i]); + } + fens.length = move_override; + } + game.move(move); + fens.push(game.fen()); + ++move_override; + } + + if (do_update !== false) { + update(); + window.history.pushState(null, null, get_history_url()); } - update(); } window['make_move'] = make_move; @@ -246,32 +312,158 @@ var prev_move = function() { if (move_override > 0) { --move_override; update(); + window.history.replaceState(null, null, get_history_url()); } } window['prev_move'] = prev_move; var next_move = function() { - if (move_override < history.length) { + if (move_override < game.history().length) { ++move_override; update(); + window.history.replaceState(null, null, get_history_url()); } } window['next_move'] = next_move; +var set_move = function(n, do_update) { + move_override = n; + if (do_update !== false) { + update(); + window.history.replaceState(null, null, get_history_url()); + } +} +window['set_move'] = set_move; + // almost all of this stuff comes from the chessboard.js example page var onDragStart = function(source, piece, position, orientation) { - var game = get_game(); - if (game.game_over() === true || - (game.turn() === 'w' && piece.search(/^b/) !== -1) || - (game.turn() === 'b' && piece.search(/^w/) !== -1)) { + var pseudogame = new Chess(current_display_fen()); + if (pseudogame.game_over() === true || + (pseudogame.turn() === 'w' && piece.search(/^b/) !== -1) || + (pseudogame.turn() === 'b' && piece.search(/^w/) !== -1)) { return false; } + + recommended_move = null; + get_best_dest(pseudogame, source, null, function(src, dest) { + $("#board").find('.square-55d63').removeClass('nonuglyhighlight'); + if (dest !== null) { + var squareEl = $('#board .square-' + dest); + squareEl.addClass('highlight1-32417'); + recommended_move = [src, dest]; + } + }); +} + +var mousedownSquare = function(e) { + reverse_dragging_from = null; + var square = $(this).attr('data-square'); + + var pseudogame = new Chess(current_display_fen()); + if (pseudogame.game_over() === true) { + return; + } + + // If the square is empty, or has a piece of the side not to move, + // we handle it. If not, normal piece dragging will take it. + var position = board.position(); + if (!position.hasOwnProperty(square) || + (pseudogame.turn() === 'w' && position[square].search(/^b/) !== -1) || + (pseudogame.turn() === 'b' && position[square].search(/^w/) !== -1)) { + reverse_dragging_from = square; + get_best_dest(pseudogame, null, square, function(src, dest) { + if (src !== null) { + var squareEl = $('#board .square-' + src); + squareEl.addClass('highlight1-32417'); + squareEl = $('#board .square-' + dest); + squareEl.addClass('highlight1-32417'); + recommended_move = [src, dest]; + } + }); + } else { + recommended_src = null; + } +} + +var mouseupSquare = function(e) { + if (reverse_dragging_from === null) { + return; + } + var source = $(this).attr('data-square'); + var target = reverse_dragging_from; + reverse_dragging_from = null; + if (onDrop(source, target) !== 'snapback') { + onSnapEnd(source, target); + } + $("#board").find('.square-55d63').removeClass('highlight1-32417'); +} + +var get_best_dest = function(game, source, target, cb) { + var moves = game.moves({ verbose: true }); + if (source !== null) { + moves = moves.filter(function(move) { return move.from == source; }); + } + if (target !== null) { + moves = moves.filter(function(move) { return move.to == target; }); + } + if (moves.length == 0) { + cb(null, null); + return; + } + if (moves.length == 1) { + cb(moves[0].from, moves[0].to); + return; + } + + // More than one move. Ask the engine to disambiguate. + var uci_moves = moves.map(function(m) { return m.from + m.to; }); + var when_engine_is_ready = function() { + engine_running = true; + stockfish.onmessage = function(event) { + var res = event.data.match(/^bestmove (\S\S)(\S\S)/); + if (res !== null) { + engine_running = false; + if (engine_replacement_callback !== null) { + // We are no longer interested in this query, + // so just discard it and call this other callback. + engine_replacement_callback(); + engine_replacement_callback = null; + } else { + cb(res[1], res[2]); + } + } + }; + stockfish.postMessage("position fen " + game.fen()); + stockfish.postMessage("go depth 6 searchmoves " + uci_moves.join(" ")); + }; + if (engine_running) { + engine_replacement_callback = when_engine_is_ready; + } else { + when_engine_is_ready(); + } } var onDrop = function(source, target) { + if (engine_running) { + // Snap end before the engine came back. + // Discard the result when it does. + engine_replacement_callback = function() {}; + } + if (source == target) { + if (recommended_move === null) { + return 'snapback'; + } else { + // Accept the move. It will be changed in onSnapEnd. + return; + } + } else { + // Suggestion not asked for. + recommended_move = null; + } + // see if the move is legal - var game = get_game(); - var move = game.move({ + var pseudogame = new Chess(current_display_fen()); + var move = pseudogame.move({ from: source, to: target, promotion: 'q' // NOTE: always promote to a queen for example simplicity @@ -279,23 +471,46 @@ var onDrop = function(source, target) { // illegal move if (move === null) return 'snapback'; +} - var new_history = game.history({ verbose: true }); - history = []; - for (var i = 0; i < new_history.length; ++i) { - history.push(new_history[i].san); +var onSnapEnd = function(source, target) { + if (source == target && recommended_move !== null) { + source = recommended_move[0]; + target = recommended_move[1]; } - move_override = history.length; - update(); -}; + recommended_move = null; + var pseudogame = new Chess(current_display_fen()); + var move = pseudogame.move({ + from: source, + to: target, + promotion: 'q' // NOTE: always promote to a queen for example simplicity + }); -// update the board position after the piece snap -// for castling, en passant, pawn promotion -var onSnapEnd = function() { - var game = get_game(); - board.position(game.fen()); - fetch_analysis(); -}; + make_move(pseudogame.history({ verbose: true }).pop().san); +} + +var onpopstate = function() { + var old_moves = game.history({ verbose: true }).map(function(x) { return x.san; }); + var new_moves = document.location.search.replace(/^\?/, "").split(","); + + if (new_moves.length == 1 && new_moves[0] == "") { + new_moves = []; + } + + var num_shared_moves; + for (num_shared_moves = 0; num_shared_moves < Math.min(old_moves.length, new_moves.length); ++num_shared_moves) { + if (old_moves[i] != new_moves[i]) { + break; + } + } + + set_move(num_shared_moves, false); + for (var i = num_shared_moves; i < new_moves.length; ++i) { + make_move(new_moves[i], false); + } + update(); + window.history.replaceState(null, null, get_history_url()); +} var init = function() { // Create board. @@ -306,7 +521,11 @@ var init = function() { onDrop: onDrop, onSnapEnd: onSnapEnd }); - update(); + $("#board").on('mousedown', '.square-55d63', mousedownSquare); + $("#board").on('mouseup', '.square-55d63', mouseupSquare); + + window.onpopstate = onpopstate; + onpopstate(); $(window).keyup(function(event) { if (event.which == 39) {