From 7dfa8135cabec7261a2a255e7b5edd679e75da0b Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Tue, 9 Dec 2014 02:00:36 +0100 Subject: [PATCH 1/1] Add some opening book stuff that is still under development, and really should be a different project eventually. --- .gitignore | 6 ++ ECO.pm | 82 ++++++++++++++ book/binloader.cpp | 113 ++++++++++++++++++++ book/binlookup.cpp | 41 +++++++ book/count.h | 9 ++ book/eco-list.pl | 8 ++ book/opening-stats.pl | 17 +++ book/parallel-parse-pgn.sh | 6 ++ book/parse-pgn.pl | 66 ++++++++++++ www/book.html | 52 +++++++++ www/css/remoteglot.css | 35 ++++++ www/js/book.js | 213 +++++++++++++++++++++++++++++++++++++ www/opening-stats.pl | 45 ++++++++ 13 files changed, 693 insertions(+) create mode 100755 ECO.pm create mode 100644 book/binloader.cpp create mode 100644 book/binlookup.cpp create mode 100644 book/count.h create mode 100644 book/eco-list.pl create mode 100755 book/opening-stats.pl create mode 100755 book/parallel-parse-pgn.sh create mode 100755 book/parse-pgn.pl create mode 100644 www/book.html create mode 100644 www/js/book.js create mode 100755 www/opening-stats.pl diff --git a/.gitignore b/.gitignore index c6b42d3..537e739 100644 --- a/.gitignore +++ b/.gitignore @@ -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 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 index 0000000..e4b40a6 --- /dev/null +++ b/book/binloader.cpp @@ -0,0 +1,113 @@ +//#define _GLIBCXX_PARALLEL +#include +#include +#include +#include +#include +#include +#include +#include +#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 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 index 0000000..5ed372d --- /dev/null +++ b/book/binlookup.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#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 index 0000000..1d8043f --- /dev/null +++ b/book/count.h @@ -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 index 0000000..52e3546 --- /dev/null +++ b/book/eco-list.pl @@ -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 index 0000000..963b8d6 --- /dev/null +++ b/book/opening-stats.pl @@ -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 index 0000000..79fa440 --- /dev/null +++ b/book/parallel-parse-pgn.sh @@ -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 index 0000000..1668b1e --- /dev/null +++ b/book/parse-pgn.pl @@ -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 index 0000000..8dcc6e4 --- /dev/null +++ b/www/book.html @@ -0,0 +1,52 @@ + + + + + + analysis.sesse.net + + + + + +

Openings

+
+
+
+ +

<<<

+

>>>

+

+
+
+
+ + + + + + + + + + + + + + + +
MoveGames%Win%WEloBEloAWin%
+
+ + + + + + + diff --git a/www/css/remoteglot.css b/www/css/remoteglot.css index da0a3a6..32402b2 100644 --- a/www/css/remoteglot.css +++ b/www/css/remoteglot.css @@ -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 index 0000000..bed05c1 --- /dev/null +++ b/www/js/book.js @@ -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 index 0000000..3b2b2ff --- /dev/null +++ b/www/opening-stats.pl @@ -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'}; +} -- 2.39.2