Add some opening book stuff that is still under development, and really should be...
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Tue, 9 Dec 2014 01:00:36 +0000 (02:00 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Tue, 9 Dec 2014 01:02:02 +0000 (02:02 +0100)
13 files changed:
.gitignore
ECO.pm [new file with mode: 0755]
book/binloader.cpp [new file with mode: 0644]
book/binlookup.cpp [new file with mode: 0644]
book/count.h [new file with mode: 0644]
book/eco-list.pl [new file with mode: 0644]
book/opening-stats.pl [new file with mode: 0755]
book/parallel-parse-pgn.sh [new file with mode: 0755]
book/parse-pgn.pl [new file with mode: 0755]
www/book.html [new file with mode: 0644]
www/css/remoteglot.css
www/js/book.js [new file with mode: 0644]
www/opening-stats.pl [new file with mode: 0755]

index c6b42d3..537e739 100644 (file)
@@ -8,3 +8,9 @@ ficslog.txt
 openings.txt
 www/history/
 closure/
+book/part-*.bin
+book/ficsgame*.pgn
+book/binloader
+book/binlookup
+book/eco.pgn
+book/open.mtbl
diff --git a/ECO.pm b/ECO.pm
new file mode 100755 (executable)
index 0000000..c1a3833
--- /dev/null
+++ b/ECO.pm
@@ -0,0 +1,82 @@
+#! /usr/bin/perl
+#
+# Get eco.pgn from ftp://ftp.cs.kent.ac.uk/pub/djb/pgn-extract/eco.pgn,
+# or any other opening database you might want to use as a base.
+#
+use strict;
+use warnings;
+use Chess::PGN::Parse;
+
+require 'Position.pm';
+
+package ECO;
+
+our %fen_to_opening = ();
+our @openings = ();
+
+sub init {
+       {
+               my $pos = Position->start_pos("white", "black");
+               my $key = _key_for_pos($pos);
+               push @openings, { eco => 'A00', name => 'Start position' };
+               $fen_to_opening{$key} = $#openings;
+       }
+
+       my $pgn = Chess::PGN::Parse->new("eco.pgn")
+               or die "can't open eco.pgn\n";
+       while ($pgn->read_game()) {
+               my $tags = $pgn->tags();
+               $pgn->quick_parse_game;
+               my $pos = Position->start_pos("white", "black");
+               my $moves = $pgn->moves // [];
+               my $eco = $pgn->eco;
+               next if (!defined($eco));
+               my $name = $tags->{'Opening'};
+               if (exists($tags->{'Variation'}) && $tags->{'Variation'} ne '') {
+                       $name .= ": " . $tags->{'Variation'};
+               }
+               for (my $i = 0; $i < scalar @$moves; ++$i) {
+                       my ($from_row, $from_col, $to_row, $to_col, $promo) = $pos->parse_pretty_move($moves->[$i]);
+                       $pos = $pos->make_move($from_row, $from_col, $to_row, $to_col, $promo, $moves->[$i]);
+               }
+               my $key = _key_for_pos($pos);
+               push @openings, { eco => $pgn->eco(), name => $name };
+               $fen_to_opening{$key} = $#openings;
+       }
+}
+
+sub persist {
+       my $filename = shift;
+       open my $fh, ">", $filename
+               or die "openings.txt: $!";
+       for my $opening (@openings) {
+               print $fh $opening->{'eco'}, " ", $opening->{'name'}, "\n";
+       }
+       close $fh;
+}
+
+sub unpersist {
+       my $filename = shift;
+       open my $fh, "<", $filename
+               or die "openings.txt: $!";
+       while (<$fh>) {
+               chomp;
+               push @openings, $_;
+       }
+       close $fh;
+}
+
+sub get_opening_num {  # May return undef.
+       my $pos = shift;
+       return $fen_to_opening{_key_for_pos($pos)};
+}
+
+sub _key_for_pos {
+       my $pos = shift;
+       my $key = $pos->fen;
+       # Remove the move clocks.
+       $key =~ s/ \d+ \d+$//;
+       return $key;
+}
+
+1;
diff --git a/book/binloader.cpp b/book/binloader.cpp
new file mode 100644 (file)
index 0000000..e4b40a6
--- /dev/null
@@ -0,0 +1,113 @@
+//#define _GLIBCXX_PARALLEL
+#include <stdio.h>
+#include <vector>
+#include <mtbl.h>
+#include <algorithm>
+#include <utility>
+#include <memory>
+#include <string>
+#include <string.h>
+#include "count.h"
+
+using namespace std;
+
+enum Result { WHITE = 0, DRAW, BLACK };
+struct Element {
+       string bpfen_and_move;
+       Result result;
+       int opening_num, white_elo, black_elo;
+
+       bool operator< (const Element& other) const {
+               return bpfen_and_move < other.bpfen_and_move;
+       }
+};
+
+int main(int argc, char **argv)
+{
+       vector<Element> elems;
+
+       for (int i = 1; i < argc; ++i) {
+               FILE *fp = fopen(argv[i], "rb");
+               if (fp == NULL) {
+                       perror(argv[i]);
+                       exit(1);
+               }
+               for ( ;; ) {
+                       int l = getc(fp);
+                       if (l == -1) {
+                               break;
+                       }
+               
+                       string bpfen_and_move;
+                       bpfen_and_move.resize(l);
+                       if (fread(&bpfen_and_move[0], l, 1, fp) != 1) {
+                               perror("fread()");
+               //              exit(1);
+                               break;
+                       }
+
+                       int r = getc(fp);
+                       if (r == -1) {
+                               perror("getc()");
+                               //exit(1);
+                               break;
+                       }
+
+                       int opening_num, white_elo, black_elo;
+                       if (fread(&white_elo, sizeof(white_elo), 1, fp) != 1) {
+                               perror("fread()");
+                               //exit(1);
+                               break;
+                       }
+                       if (fread(&black_elo, sizeof(black_elo), 1, fp) != 1) {
+                               perror("fread()");
+                               //exit(1);
+                               break;
+                       }
+                       if (fread(&opening_num, sizeof(opening_num), 1, fp) != 1) {
+                               perror("fread()");
+                               //exit(1);
+                               break;
+                       }
+                       elems.emplace_back(Element {move(bpfen_and_move), Result(r), opening_num, white_elo, black_elo});
+               }
+               fclose(fp);
+
+               printf("Read %ld elems\n", elems.size());
+       }
+
+       printf("Sorting...\n");
+       sort(elems.begin(), elems.end());
+
+       printf("Writing SSTable...\n");
+       mtbl_writer* mtbl = mtbl_writer_init("open.mtbl", NULL);
+       Count c;
+       int num_elo = 0;
+       double sum_white_elo = 0.0, sum_black_elo = 0.0;
+       for (int i = 0; i < elems.size(); ++i) {
+               if (elems[i].result == WHITE) {
+                       ++c.white;
+               } else if (elems[i].result == DRAW) {
+                       ++c.draw;
+               } else if (elems[i].result == BLACK) {
+                       ++c.black;
+               }
+               c.opening_num = elems[i].opening_num;
+               if (elems[i].white_elo >= 100 && elems[i].black_elo >= 100) {
+                       sum_white_elo += elems[i].white_elo;
+                       sum_black_elo += elems[i].black_elo;
+                       ++num_elo;
+               }
+               if (i == elems.size() - 1 || elems[i].bpfen_and_move != elems[i + 1].bpfen_and_move) {
+                       c.avg_white_elo = sum_white_elo / num_elo;
+                       c.avg_black_elo = sum_black_elo / num_elo;
+                       mtbl_writer_add(mtbl,
+                               (const uint8_t *)elems[i].bpfen_and_move.data(), elems[i].bpfen_and_move.size(),
+                               (const uint8_t *)&c, sizeof(c));
+                       c = Count();
+                       num_elo = 0;
+                       sum_white_elo = sum_black_elo = 0.0;
+               }
+       }
+       mtbl_writer_destroy(&mtbl);
+}
diff --git a/book/binlookup.cpp b/book/binlookup.cpp
new file mode 100644 (file)
index 0000000..5ed372d
--- /dev/null
@@ -0,0 +1,41 @@
+#include <stdio.h>
+#include <vector>
+#include <mtbl.h>
+#include <algorithm>
+#include <utility>
+#include <memory>
+#include <string>
+#include <string.h>
+#include "count.h"
+
+using namespace std;
+
+int main(int argc, char **argv)
+{
+       const char *hex_prefix = argv[2];
+       const int prefix_len = strlen(hex_prefix) / 2;
+       uint8_t *prefix = new uint8_t[prefix_len];
+
+       for (int i = 0; i < prefix_len; ++i) {
+               char x[3];
+               x[0] = hex_prefix[i * 2 + 0];
+               x[1] = hex_prefix[i * 2 + 1];
+               x[2] = 0;
+               int k;
+               sscanf(x, "%02x", &k);
+               prefix[i] = k;
+       }
+
+       mtbl_reader* mtbl = mtbl_reader_init(argv[1], NULL);
+       const mtbl_source *src = mtbl_reader_source(mtbl);
+               mtbl_iter *it = mtbl_source_get_prefix(src, prefix, prefix_len);
+
+       const uint8_t *key, *val;
+       size_t len_key, len_val;
+
+       while (mtbl_iter_next(it, &key, &len_key, &val, &len_val)) {
+               string move((char *)(key + prefix_len), len_key - prefix_len);
+               const Count* c = (Count *)val;
+               printf("%s %d %d %d %d %f %f\n", move.c_str(), c->white, c->draw, c->black, c->opening_num, c->avg_white_elo, c->avg_black_elo);
+       }
+}
diff --git a/book/count.h b/book/count.h
new file mode 100644 (file)
index 0000000..1d8043f
--- /dev/null
@@ -0,0 +1,9 @@
+
+struct Count {
+       int white = 0;
+       int draw = 0;
+       int black = 0;
+       int opening_num = -1;
+       float avg_white_elo = 0.0;
+       float avg_black_elo = 0.0;
+};
diff --git a/book/eco-list.pl b/book/eco-list.pl
new file mode 100644 (file)
index 0000000..52e3546
--- /dev/null
@@ -0,0 +1,8 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+require 'ECO.pm';
+
+ECO::init();
+ECO::persist();
+
diff --git a/book/opening-stats.pl b/book/opening-stats.pl
new file mode 100755 (executable)
index 0000000..963b8d6
--- /dev/null
@@ -0,0 +1,17 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use CGI;
+use JSON::XS;
+use lib '.';
+use Position;
+require 'ECO.pm';
+
+#ECO::unpersist();
+
+my $cgi = CGI->new;
+my $fen = $ARGV[0];
+my $pos = Position->from_fen($fen);
+my $hex = unpack('H*', $pos->bitpacked_fen);
+system("./binlookup", "./open.mtbl", $hex);
+
diff --git a/book/parallel-parse-pgn.sh b/book/parallel-parse-pgn.sh
new file mode 100755 (executable)
index 0000000..79fa440
--- /dev/null
@@ -0,0 +1,6 @@
+#! /bin/sh
+FILE=$1
+for X in $( seq 0 39 ); do
+       ( ./parse-pgn.pl $FILE $X 40 >> part-$X.bin ) &
+done
+wait
diff --git a/book/parse-pgn.pl b/book/parse-pgn.pl
new file mode 100755 (executable)
index 0000000..1668b1e
--- /dev/null
@@ -0,0 +1,66 @@
+#! /usr/bin/perl
+use Chess::PGN::Parse;
+use Data::Dumper;
+use strict;
+use warnings;
+use DBI;
+use DBD::Pg;
+require 'Position.pm';
+require 'Engine.pm';
+require 'ECO.pm';
+
+my $DRYRUN = 1;
+my $TEXTOUT = 0;
+my $BINOUT = 1;
+
+ECO::init();
+
+my $dbh = DBI->connect("dbi:Pg:dbname=ficsopening", "sesse", undef);
+$dbh->do("COPY opening FROM STDIN") unless $DRYRUN;
+
+my ($filename, $my_num, $tot_num) = @ARGV;
+
+my $pgn = Chess::PGN::Parse->new($filename)
+       or die "can't open $filename\n";
+my $game_num = 0;
+while ($pgn->read_game()) {
+       next unless ($game_num++ % $tot_num == $my_num);
+       my $tags = $pgn->tags();
+#      next unless $tags->{'WhiteElo'} >= 2000;
+#      next unless $tags->{'BlackElo'} >= 2000;
+       $pgn->quick_parse_game;
+       my $pos = Position->start_pos($pgn->white, $pgn->black);
+       my $result = $pgn->result;
+       my $binresult;
+       if ($result eq '1-0') {
+               $binresult = chr(0);
+       } elsif ($result eq '1/2-1/2') {
+               $binresult = chr(1);
+       } elsif ($result eq '0-1') {
+               $binresult = chr(2);
+       } else {
+               die "Unknown result $result";
+       }
+       my $binwhiteelo = pack('l', $tags->{'WhiteElo'});
+       my $binblackelo = pack('l', $tags->{'BlackElo'});
+       my $moves = $pgn->moves;
+       my $opening = ECO::get_opening_num($pos);
+#      print STDERR $pgn->white, " ", $pgn->black, "\n";
+       for (my $i = 0; $i + 1 < scalar @$moves; ++$i) {
+               my ($from_row, $from_col, $to_row, $to_col, $promo) = $pos->parse_pretty_move($moves->[$i]);
+               my $next_move = $moves->[$i];
+               my $bpfen = $pos->bitpacked_fen;
+               my $bpfen_q = $dbh->quote($bpfen, { pg_type => DBD::Pg::PG_BYTEA });
+               my $fen = $pos->fen;
+               $opening = ECO::get_opening_num($pos) // $opening;
+               print "$fen $next_move $result $opening\n" if $TEXTOUT;
+               if ($BINOUT) {
+                       print chr(length($bpfen) + length($next_move)) . $bpfen . $next_move;
+                       print $binresult . $binwhiteelo . $binblackelo;
+                       print pack('l', $opening);
+               }
+               $dbh->pg_putcopydata("$bpfen_q\t$next_move\t$result\n") unless $DRYRUN;
+               $pos = $pos->make_move($from_row, $from_col, $to_row, $to_col, $promo, $moves->[$i]);
+       }
+}
+$dbh->pg_putcopyend unless $DRYRUN;
diff --git a/www/book.html b/www/book.html
new file mode 100644 (file)
index 0000000..8dcc6e4
--- /dev/null
@@ -0,0 +1,52 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <title>analysis.sesse.net</title>
+
+  <link rel="stylesheet" href="css/chessboard-0.3.0.min.css" />
+  <link rel="stylesheet" href="css/remoteglot.css" />
+</head>
+<body>
+<h1 id="headline">Openings</h1>
+<div id="boardcontainer">
+  <div id="board"></div>
+  <div id="bottompanel">
+    <!-- CSS abuse... -->
+    <p id="whiteclock"><a href="javascript:prev_move()">&lt;&lt;&lt;</a></p>
+    <p id="blackclock"><a href="javascript:next_move()">&gt;&gt;&gt;</a></p>
+    <p id="numviewers"></p>
+  </div>
+</div>
+<div id="analysis">
+  <table>
+    <thead>
+      <tr>
+        <th>Move</th>
+        <th>Games</th>
+        <th>%</th>
+        <th>Win%</th>
+        <th>WElo</th>
+        <th>BElo</th>
+        <th>AWin%</th>
+        <!--<th class="winbars">
+          <table><tr>
+             <td class="white" style="width: 35%;">White</td>
+             <td class="draw" style="width: 30%;">Draw</td>
+             <td class="black" style="width: 35%;">Black</td>
+           </tr></table>
+        </th> -->
+      </tr>
+    </thead>
+    <tbody id="lines">
+    </tbody>
+  </table>
+</div>
+<!-- For faster development -->
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
+<script type="text/javascript" src="js/chessboard-0.3.0.min.js"></script>
+<script type="text/javascript" src="js/chess.min.js"></script>
+<script type="text/javascript" src="js/book.js"></script>
+</body>
+</html>
index da0a3a6..32402b2 100644 (file)
@@ -152,3 +152,38 @@ a.move:hover {
 #linenav {
        display: none;
 }
+
+/* Opening display */
+.num {
+       padding-left: 0.5em;
+       padding-right: 0.5em;
+       text-align: right;
+}
+.winbars {
+        width: 20em;
+       font-size: small;
+       font-weight: bold;
+       text-align: center;
+}
+.winbars table {
+       border: 1px solid black;
+       width: 100%;
+       border-collapse: collapse;
+}
+.winbars table td {
+       border: 1px solid black;
+       overflow: hidden;
+       max-width: 0px;
+}
+.winbars table td.white {
+       background-color: white;
+       color: black;
+}
+.winbars table td.draw {
+       background-color: gray;
+       color: white;
+}
+.winbars table td.black {
+       background-color: black;
+       color: white;
+}
diff --git a/www/js/book.js b/www/js/book.js
new file mode 100644 (file)
index 0000000..bed05c1
--- /dev/null
@@ -0,0 +1,213 @@
+(function() {
+
+var board = null;
+var moves = [];
+var move_override = 0;
+
+var get_game = function() {
+       var game = new Chess();
+       for (var i = 0; i < move_override; ++i) {
+               game.move(moves[i]);
+       }
+       return game;
+}
+
+var update = function() {
+       var game = get_game();
+       board.position(game.fen());
+       fetch_analysis();
+}
+
+var fetch_analysis = function() {
+       var game = get_game();
+       $.ajax({
+               url: "/opening-stats.pl?fen=" + encodeURIComponent(game.fen())
+       }).done(function(data, textstatus, xhr) {
+               show_lines(data, game);
+       });
+}
+
+var add_td = function(tr, value) {
+       var td = document.createElement("td");
+       tr.appendChild(td);
+       $(td).addClass("num");
+       $(td).text(value);
+}
+
+var show_lines = function(data, game) {
+       var moves = data['moves'];
+       $('#numviewers').text(data['opening']);
+       var total_num = 0;
+       for (var i = 0; i < moves.length; ++i) {
+               var move = moves[i];
+               total_num += parseInt(move['white']);
+               total_num += parseInt(move['draw']);
+               total_num += parseInt(move['black']);
+       }       
+
+       var tbl = $("#lines");
+       tbl.empty();
+
+       for (var i = 0; i < moves.length; ++i) {
+               var move = moves[i];
+               var tr = document.createElement("tr");
+
+               var white = parseInt(move['white']);
+               var draw = parseInt(move['draw']);
+               var black = parseInt(move['black']);
+
+               // Move.
+               var move_td = document.createElement("td");
+               tr.appendChild(move_td);
+               $(move_td).addClass("move");
+
+               var move_a = document.createElement("a");
+               move_a.href = "javascript:make_move('" + move['move'] + "')";
+               move_td.appendChild(move_a);
+               $(move_a).text(move['move']);
+
+               // #.
+               var num = white + draw + black;
+               add_td(tr, num);
+
+               // %.
+               add_td(tr, (100.0 * num / total_num).toFixed(1) + "%");
+
+               // Win%.
+               var white_win_ratio = (white + 0.5 * draw) / num;
+               var win_ratio = (game.turn() == 'w') ? white_win_ratio : 1.0 - white_win_ratio;
+               add_td(tr, ((100.0 * win_ratio).toFixed(1) + "%"));
+
+               // Elo.
+               add_td(tr, move['white_avg_elo'].toFixed(1));
+               add_td(tr, move['black_avg_elo'].toFixed(1));
+
+               // 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 = (game.turn() == 'w') ? white_win_ratio : 1.0 - white_win_ratio;
+               add_td(tr, ((100.0 * win_ratio).toFixed(1) + "%"));
+
+               if (false) {
+                       // Win bars (W/D/B).
+                       var winbar_td = document.createElement("td");
+                       $(winbar_td).addClass("winbars");
+                       tr.appendChild(winbar_td);
+                       var winbar_table = document.createElement("table");
+                       winbar_td.appendChild(winbar_table);
+                       var winbar_tr = document.createElement("tr");
+                       winbar_table.appendChild(winbar_tr);
+
+                       if (white > 0) {
+                               var white_percent = (100.0 * white / num).toFixed(0) + "%";
+                               var white_td = document.createElement("td");
+                               winbar_tr.appendChild(white_td);
+                               $(white_td).addClass("white");
+                               white_td.style.width = white_percent;
+                               $(white_td).text(white_percent);
+                       }
+                       if (draw > 0) {
+                               var draw_percent = (100.0 * draw / num).toFixed(0) + "%";
+                               var draw_td = document.createElement("td");
+                               winbar_tr.appendChild(draw_td);
+                               $(draw_td).addClass("draw");
+                               draw_td.style.width = draw_percent;
+                               $(draw_td).text(draw_percent);
+                       }
+                       if (black > 0) {
+                               var black_percent = (100.0 * black / num).toFixed(0) + "%";
+                               var black_td = document.createElement("td");
+                               winbar_tr.appendChild(black_td);
+                               $(black_td).addClass("black");
+                               black_td.style.width = black_percent;
+                               $(black_td).text(black_percent);
+                       }
+               }
+
+               tbl.append(tr);
+       }
+}
+
+var make_move = function(move) {
+       moves.length = move_override;
+       moves.push(move);
+       move_override = moves.length;
+       update();
+}
+window['make_move'] = make_move;
+
+var prev_move = function() {
+       if (move_override > 0) {
+               --move_override;
+               update();
+       }
+}
+window['prev_move'] = prev_move;
+
+var next_move = function() {
+       if (move_override < moves.length) {
+               ++move_override;
+               update();
+       }
+}
+window['next_move'] = next_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)) {
+               return false;
+       }
+}
+
+var onDrop = function(source, target) {
+       // see if the move is legal
+       var game = get_game();
+       var move = game.move({
+               from: source,
+               to: target,
+               promotion: 'q' // NOTE: always promote to a queen for example simplicity
+       });
+
+       // illegal move
+       if (move === null) return 'snapback';
+
+       moves = game.history({ verbose: true });
+       move_override = moves.length;
+};
+
+// 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();
+};
+
+var init = function() {
+       // Create board.
+       board = new window.ChessBoard('board', {
+               draggable: true,
+               position: 'start',
+               onDragStart: onDragStart,
+               onDrop: onDrop,
+               onSnapEnd: onSnapEnd
+       });
+       update();
+
+       $(window).keyup(function(event) {
+               if (event.which == 39) {
+                       next_move();
+               } else if (event.which == 37) {
+                       prev_move();
+               }
+       });
+}
+
+
+$(document).ready(init);
+
+})();
diff --git a/www/opening-stats.pl b/www/opening-stats.pl
new file mode 100755 (executable)
index 0000000..3b2b2ff
--- /dev/null
@@ -0,0 +1,45 @@
+#! /usr/bin/perl
+use strict;
+use warnings;
+use CGI;
+use JSON::XS;
+use lib '..';
+use Position;
+use ECO;
+
+ECO::unpersist("../book/openings.txt");
+
+my $cgi = CGI->new;
+my $fen = $cgi->param('fen');
+my $pos = Position->from_fen($fen);
+my $hex = unpack('H*', $pos->bitpacked_fen);
+open my $fh, "-|", "../book/binlookup", "../book/open.mtbl", $hex
+       or die "../book/binlookup: $!";
+
+my $opening;
+
+my @moves = ();
+while (<$fh>) {
+       chomp;
+       my ($move, $white, $draw, $black, $opening_num, $white_avg_elo, $black_avg_elo) = split;
+       push @moves, {
+               move => $move,
+               white => $white * 1,
+               draw => $draw * 1,
+               black => $black * 1,
+               white_avg_elo => $white_avg_elo * 1,
+               black_avg_elo => $black_avg_elo * 1
+       };
+       $opening = $ECO::openings[$opening_num];
+}
+close $fh;
+
+@moves = sort { num($b) <=> num($a) } @moves;
+
+print $cgi->header(-type=>'application/json');
+print JSON::XS::encode_json({ moves => \@moves, opening => $opening });
+
+sub num {
+       my $x = shift;
+       return $x->{'white'} + $x->{'draw'} + $x->{'black'};
+}