]> git.sesse.net Git - remoteglot-book/blobdiff - www/js/book.js
Add a training mode.
[remoteglot-book] / www / js / book.js
index 95f423abf944dd34aa1226a2046c87f3740d22e0..5234eff7a426da8ee4250c629d358b5627e6143a 100644 (file)
@@ -2,7 +2,7 @@
 
 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');
@@ -10,6 +10,13 @@ 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_move10 = 0.30;
 
 var entity_map = {
        "&": "&",
@@ -26,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];
        }
 }
 
@@ -62,6 +73,15 @@ var update = function() {
                $("#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();
 }
 
@@ -81,6 +101,122 @@ 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_moves_by_common(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;
+                       }
+               }
+               moves.sort(function(a, b) { return b['num'] - a['num'] });
+
+               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 find_computer_move = function() {
+       var fen = current_display_fen();
+       $.ajax({
+               url: "/opening-stats.pl?fen=" + encodeURIComponent(fen) + ";includetransp=0"
+       }).done(function(data, textstatus, xhr) {
+               var candidate_moves = [];
+
+               var moves = data['moves'];
+               var root_move = sort_moves_by_common(moves, data);
+
+               var practice_minimum_move_fraction;
+               if (move_override > 20) {
+                       practice_minimum_move_fraction = practice_minimum_move_fraction_move10;
+               } else {
+                       practice_minimum_move_fraction = practice_minimum_move_fraction_start +
+                               ((move_override-1)/20.0) * (practice_minimum_move_fraction_move10 - 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.
+               var chosen_index = Math.floor(Math.random() * candidate_moves.length);
+               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");
+               }
+               make_move(chosen['move']);
+       });
+}
+
+// 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_moves_by_common = 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);
@@ -88,6 +224,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;
@@ -250,7 +403,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]));
                                }
                        }
                }
@@ -312,6 +465,22 @@ var set_flipboard = function(value) {
 }
 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;
+       } 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) {
@@ -340,8 +509,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());
        }
@@ -557,6 +727,7 @@ var init = function() {
 
        window.onpopstate = onpopstate;
        onpopstate();
+       set_practice(false);
 
        $(window).keyup(function(event) {
                if (event.which == 39) {