X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=www%2Fjs%2Fbook.js;h=aacd98030c516bef45fa7bd0d380884e16f90674;hb=b6effd41313938e70b383f9d6fa61ff53806cf56;hp=8300be61da3391a1d4c517ab07f2d41c7a7524e1;hpb=b00d64218b75a44b86f2fbc8529765d0cd8cc9d0;p=remoteglot-book diff --git a/www/js/book.js b/www/js/book.js index 8300be6..aacd980 100644 --- a/www/js/book.js +++ b/www/js/book.js @@ -2,9 +2,21 @@ var board = null; var game = new Chess(); -var fens = []; +var fens = []; // Position after each. 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 practice_mode = false; +var practice_side = 'W'; + +// TODO: Make this configurable. +var practice_top_moves_limit = 5; +var practice_minimum_move_fraction_start = 0.05; +var practice_minimum_move_fraction_move5 = 0.30; var entity_map = { "&": "&", @@ -21,10 +33,14 @@ function escape_html(string) { } var current_display_fen = function() { - if (move_override == 0) { + return fen_before_move(move_override); +} + +var fen_before_move = function(move_num) { + if (move_num == 0) { return 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; } else { - return fens[move_override - 1]; + return fens[move_num - 1]; } } @@ -48,15 +64,24 @@ var update = function() { 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-55d63').removeClass('nonuglyhighlight'); $("#board").find('.square-' + highlight_from).addClass('nonuglyhighlight'); $("#board").find('.square-' + highlight_to).addClass('nonuglyhighlight'); } + if (practice_mode) { + find_last_move_score(); + var side_to_move = (move_override % 2 == 0) ? 'W' : 'B'; + if (side_to_move !== practice_side) { + find_computer_move(); + } + // Fall through to get the line name and such. + } + fetch_analysis(); } @@ -76,6 +101,142 @@ var fetch_analysis = function() { }); } +var find_last_move_score = function() { + var history = game.history({ verbose: true }); + var side_to_move = (move_override % 2 == 0) ? 'W' : 'B'; + var move_num = (side_to_move === practice_side) ? (move_override - 2) : (move_override - 1); + if (move_num < 0) { + $("#yourmove").text("(none)"); + $("#yourfraction").text("N/A"); + $("#yourrank").text("N/A"); + $("#yourawin").text("??.?%"); + $("#yourawindiff").text("+?.?%"); + return; + } + + var chosen_move = history[move_num].san; + $("#yourmove").text(chosen_move); + $.ajax({ + url: "/opening-stats.pl?fen=" + encodeURIComponent(fen_before_move(move_num)) + + ";includetransp=0" + }).done(function(data, textstatus, xhr) { + var moves = data['moves']; + var root_move = sort_move_by_frequency(moves, data); + var your_move, your_index; + + for (var i = 0; i < moves.length; ++i) { + var move = moves[i]; + if (move['move'] === chosen_move) { + your_move = move; + your_index = i; + } + } + + if (your_move) { + $("#yourfraction").text(format_fraction(your_move['fraction'])); + $("#yourawin").text(format_fraction(your_move['corrected_win_ratio'])); + $("#yourrank").text(format_ordinal(your_index + 1) + ", based on " + root_move['num'] + " games"); + var diff = your_move['corrected_win_ratio'] - root_move['corrected_win_ratio']; + $("#yourawindiff").css("color", "black"); + if (diff === 0) { + $("#yourawindiff").text("0.0%"); + } else if (diff > 0) { + $("#yourawindiff").text("+" + format_fraction(diff)); + $("#yourawindiff").css("color", "green"); + } else { + $("#yourawindiff").text(format_fraction(diff)); + if (diff < -0.02) { + $("#yourawindiff").css("color", "red"); + } + } + } else { + $("#yourfraction").text("??.?%"); + $("#yourrank").text("?th"); + $("#yourawin").text("??.?%"); + $("#yourawindiff").text("+?.?%"); + } + }); +} + +var candidate_moves = []; +var chosen_index = null; + +var find_computer_move = function() { + var fen = current_display_fen(); + $.ajax({ + url: "/opening-stats.pl?fen=" + encodeURIComponent(fen) + ";includetransp=0" + }).done(function(data, textstatus, xhr) { + candidate_moves = []; + + var moves = data['moves']; + var root_move = sort_move_by_frequency(moves, data); + + var practice_minimum_move_fraction; + if (move_override > 20) { + practice_minimum_move_fraction = practice_minimum_move_fraction_move5; + } else { + practice_minimum_move_fraction = practice_minimum_move_fraction_start + + ((move_override-1)/10.0) * (practice_minimum_move_fraction_move5 - practice_minimum_move_fraction_start); + } + console.log(practice_minimum_move_fraction); + + for (var i = 0; i < Math.min(moves.length, practice_top_moves_limit); ++i) { + var move = moves[i]; + if (i == 0 || move['fraction'] >= practice_minimum_move_fraction) { + candidate_moves.push(move); + } + } + + // Pick one at random. + choose_move(Math.floor(Math.random() * candidate_moves.length)); + }); +} + +var choose_move = function(idx) { + chosen_index = idx; + var chosen = candidate_moves[chosen_index]; + $("#compmove").text(chosen['move']); + $("#compfraction").text((100.0 * chosen['fraction']).toFixed(1) + "%"); + if (candidate_moves.length == 1) { + $("#comprank").text("only candidate move"); + } else { + $("#comprank").text(format_ordinal(chosen_index + 1) + " out of " + candidate_moves.length + " candidate moves, j/k to switch"); + } + make_move(chosen['move']); +} + +var prev_variant = function() { + if (chosen_index !== null) { + --move_override; + choose_move((chosen_index + candidate_moves.length - 1) % candidate_moves.length); + } +} + +var next_variant = function() { + if (chosen_index !== null) { + --move_override; + choose_move((chosen_index + 1) % candidate_moves.length); + } +} + +// Add deried data and then sort moves to get the most common ones (in-place). +// Remove the root mode and return it. Currently usable for practice mode only! +var sort_move_by_frequency = function(moves, data) +{ + var total_num = find_total_games(moves); + var root_move; + for (var i = 0; i < moves.length; ++i) { + var move = moves[i]; + calc_move_derived_data(move, total_num, data, (practice_side === 'W')); + if (!move['move']) { + root_move = (moves.splice(i, 1))[0]; + --i; + } + } + moves.sort(function(a, b) { return b['num'] - a['num'] }); + return root_move; +} + var add_td = function(tr, value) { var td = document.createElement("td"); tr.appendChild(td); @@ -83,6 +244,23 @@ var add_td = function(tr, value) { $(td).text(value); } +var format_ordinal = function(x) { + var tens = Math.floor(x / 10) % 10; + if (tens == 1) { + return x + "th"; + } else { + var ones = x % 10; + if (ones == 1) return x + "st"; + if (ones == 2) return x + "nd"; + if (ones == 3) return x + "rd"; + return x + "th"; + } +} + +var format_fraction = function(x) { + return (100.0 * x).toFixed(1) + '%'; +} + var TYPE_MOVE = 0; var TYPE_INTEGER = 1; var TYPE_FLOAT = 2; @@ -92,6 +270,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 ], @@ -129,15 +309,7 @@ var show_lines = function(data, game) { $('#gamesummary').html(text); } - var total_num = 0; - for (var i = 0; i < moves.length; ++i) { - var move = moves[i]; - if (move['move']) { - total_num += parseInt(move['white']); - total_num += parseInt(move['draw']); - total_num += parseInt(move['black']); - } - } + var total_num = find_total_games(moves); var headings_tr = $("#headings"); headings_tr.empty(); @@ -164,46 +336,40 @@ var show_lines = function(data, game) { var move = moves[i]; var line = []; - var white = parseInt(move['white']); - var draw = parseInt(move['draw']); - var black = parseInt(move['black']); + calc_move_derived_data(move, total_num, data, (move_override % 2 == 0)); + + var white = move['white']; + var draw = move['draw']; + var black = move['black']; + var computer = 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); // %. - - // Win%. - var white_win_ratio = (white + 0.5 * draw) / num; - var win_ratio = (move_override % 2 == 0) ? white_win_ratio : 1.0 - white_win_ratio; - line.push(win_ratio); - - line.push(white); // WWin. - line.push(white / num); // %WW. - line.push(black); // BWin. - line.push(black / num); // %BW. - line.push(draw); // Draw. - line.push(draw / num); // %Draw. + line.push(move['num']); // N. + line.push(move['fraction']); // %. + line.push(computer); // CGames. + line.push(move['human_index']); // Hum. + line.push(move['win_ratio']); // Win%. + + line.push(white); // WWin. + line.push(white / move['num']); // %WW. + line.push(black); // BWin. + line.push(black / move['num']); // %BW. + line.push(draw); // Draw. + line.push(draw / move['num']); // %Draw. if (move['num_elo'] >= 10) { // Elo. line.push(move['white_avg_elo']); line.push(move['black_avg_elo']); line.push(move['white_avg_elo'] - move['black_avg_elo']); - - // Win% corrected for Elo. - 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 = (move_override % 2 == 0) ? white_win_ratio : 1.0 - white_win_ratio; - line.push(win_ratio); } else { line.push(null); line.push(null); line.push(null); - line.push(null); } + + line.push(move['corrected_win_ratio'] || null); lines.push(line); } @@ -237,10 +403,14 @@ var show_lines = function(data, game) { } } - var move_a = document.createElement("a"); - move_a.href = "javascript:make_move('" + line[j] + "')"; - td.appendChild(move_a); - $(move_a).text(line[j]); + if (line[j] === '1-0' || line[j] === '1/2-1/2' || line[j] === '0-1') { + $(td).text($(td).text() + line[j]); + } else { + var move_a = document.createElement("a"); + move_a.href = "javascript:make_move('" + line[j] + "')"; + td.appendChild(move_a); + $(move_a).text(line[j]); + } } else if (headings[j][1] == TYPE_INTEGER) { add_td(tr, line[j] || 0); } else if (headings[j][1] == TYPE_FLOAT) { @@ -253,7 +423,7 @@ var show_lines = function(data, game) { if (isNaN(line[j]) || !isFinite(line[j])) { add_td(tr, ''); } else { - add_td(tr, (100.0 * line[j]).toFixed(1) + "%"); + add_td(tr, format_fraction(line[j])); } } } @@ -262,12 +432,76 @@ var show_lines = function(data, game) { } } +var find_total_games = function(moves) { + var total_num = 0; + for (var i = 0; i < moves.length; ++i) { + var move = moves[i]; + if (move['move']) { + total_num += move['white']; + total_num += move['draw']; + total_num += move['black']; + } + } + return total_num; +} + +var calc_move_derived_data = function(move, total_num, data, is_white) { + var white = move['white']; + var draw = move['draw']; + var black = move['black']; + var computer = move['computer']; + + var num = white + draw + black; + move['num'] = num; + move['fraction'] = num / total_num; + + // Adjust so that the human index is 50% overall. + var exp = Math.log(0.5) / Math.log(data['computer_games'] / data['total_games']); + move['human_index'] = 1.0 - Math.pow(computer / num, exp); + + // Win%. + var white_win_ratio = (white + 0.5 * draw) / num; + var win_ratio = is_white ? white_win_ratio : 1.0 - white_win_ratio; + move['win_ratio'] = win_ratio; + + if (move['num_elo'] >= 10) { + // Win% corrected for Elo. + 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 = is_white ? white_win_ratio : 1.0 - white_win_ratio; + move['corrected_win_ratio'] = win_ratio; + } +}; + var set_includetransp = function(value) { includetransp = value; update(); } window['set_includetransp'] = set_includetransp; +var set_flipboard = function(value) { + board.orientation(value ? 'black' : 'white'); +} +window['set_flipboard'] = set_flipboard; + +var set_practice = function(value) { + practice_mode = value; + if (practice_mode) { + practice_side = (move_override % 2 == 0) ? 'W' : 'B'; + find_last_move_score(); + $("#stats").hide(); + $("#practiceoutput").show(); + document.getElementById("includetransp").checked = false; + set_includetransp(false); + } else { + $("#stats").show(); + $("#practiceoutput").hide(); + } + update(); +} +window['set_practice'] = set_practice; + var make_move = function(move, do_update) { var history = game.history({ verbose: true }); if (move_override < history.length && history[move_override].san == move) { @@ -296,8 +530,9 @@ var make_move = function(move, do_update) { window['make_move'] = make_move; var prev_move = function() { - if (move_override > 0) { - --move_override; + var moves_to_skip = practice_mode ? 2 : 1; + if (move_override >= moves_to_skip) { + move_override -= moves_to_skip; update(); window.history.replaceState(null, null, get_history_url()); } @@ -330,9 +565,124 @@ var onDragStart = function(source, piece, position, orientation) { (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 pseudogame = new Chess(current_display_fen()); var move = pseudogame.move({ @@ -346,6 +696,11 @@ var onDrop = function(source, target) { } var onSnapEnd = function(source, target) { + if (source == target && recommended_move !== null) { + source = recommended_move[0]; + target = recommended_move[1]; + } + recommended_move = null; var pseudogame = new Chess(current_display_fen()); var move = pseudogame.move({ from: source, @@ -388,19 +743,28 @@ var init = function() { onDrop: onDrop, onSnapEnd: onSnapEnd }); + $("#board").on('mousedown', '.square-55d63', mousedownSquare); + $("#board").on('mouseup', '.square-55d63', mouseupSquare); window.onpopstate = onpopstate; onpopstate(); + set_practice(false); $(window).keyup(function(event) { if (event.which == 39) { next_move(); } else if (event.which == 37) { prev_move(); + } else if (event.which == 74) { // j + if (practice_mode) next_variant(); + } else if (event.which == 75) { // k + if (practice_mode) prev_variant(); } }); -} + // Seemingly the web worker is not started before we send it a message. + stockfish.postMessage("uci"); +} $(document).ready(init);