Lately, I've been playing around with computer generated analysis of chess games. I have two programs that I use for this purpose, one a commercial chess program called 'Fritz' and the other, an open source effort by Bob Hyatt called 'Crafty'1. In particular, I've been looking at the output of Crafty and how I could convert the raw data generated into graphs for a more useful look at the information created.
Without going too deeply into the details, I generate that source file for this exploration this way2:
- First I break out the game I want to analyze. For instance I've collected all of the games from the 1997 Cutthroat Classic into a file called ctcl97.pgn. So I extract the first game into ctcl97.1.pgn as a temporary file.
- Next I create a command file called cmd.txt. It contains the line
annotate ctcl99.1.pgn bw 1-999 -1.000 30
- Then I feed this to Crafty with a command line like:
C:\crafty < cmd.txt
As a rough guide to what you are looking at:[Event "Cutthroat Classic 97"] [Site "Sun Valley (ID)"] [Date "1997.05.17"] [Round "01"] [White "Richardson Tom (ID)"] [WhiteElo ""] [Black "Myers Hugh S (ID)"] [BlackElo ""] [Result "0-1"] [Annotator "Crafty v18.5"] {annotating both black and white moves.} {using a scoring margin of -1.00 pawns.} {search time limit is 30.00} 1. e4 d5 2. exd5 Nf6 3. Nc3 Nxd5 4. d3 ({12:-0.35} 4. d3 Nxc3 5. bxc3 e6 6. Nf3 Bd6 7. Be2 Q +f6 8. Bd2 O-O 9. O-O Nc6 $15) ({0:+0.00} 4. Bc4 Nb6 $10) 4. ... e5 ({12:-0.11} 4. ... e5 5. Qh5 Nc6 6. Bg5 Qd6 7. Nge2 N +xc3 8. Nxc3 Qb4 9. Rb1 Bg4 10. Qh4 $10) ({12:-0.19} 4. ... Nxc3 5. bxc3 e5 6. Be2 Bc5 7. Nf3 +Nc6 8. Bg5 f6 9. Be3 $10) . . .
- The stuff in brackets either comes from the source file or was generated by Crafty. In any event, it is all .pgn header information.
- The first 3 moves have no analysis---this is because they are all still in 'book', i.e. they conform to a particular well known opening, in this case the "Center Counter" also known as the "Scandinavian" or the "Scandinavian Gambit".
- Move 4. for white begins the analysis.
- Next is the first line of analytical output. Roughly speaking, this says that 'd3' looses the equivalent of 35 hundredths of a pawn, i.e. the move is bad! The '12' before the colon says that this position was examined to a depth of 12 plys (where ply is a half move) or 6 moves ahead.
- The second line, says that Crafty thinks that a better response would have been the book move, 'Bc4'. The 0 for look ahead, represents a table lookup, i.e. Crafty read this move out of his opening book. Here the +0.00 given as the positional score indicates both sides even, no advantage to either.
So where is the code you ask? Well, that's easy, it's here:
In short, given a Crafty .can file as input, parse it into three arrays.#!/perl/bin/perl # # craftysvg.pl -- script to generate .svg file from Crafty .pgn analy +sis. use strict; use warnings; use diagnostics; my @scores; my @allscores; my @bestblack; my @bestwhite; my $black; my $level; my $score; my $value; my $previous_level = 0; while (<>) { if (/^\s/) { if (/^\s+(\d+)\./) { $level = 1; $black = ( /\.\.\./ ? '1' : '0' ); s/^\s+//; my @temp = split (/\s+/); unless ( scalar(@temp) == 2 ) { unless ($black) { push ( @allscores, '0,1,0:+0.00' ); push ( @allscores, '0,2,0:+0.00' ); push ( @allscores, '1,1,0:+0.00' ); push ( @allscores, '1,2,0:+0.00' ); } } } elsif (/^\s+\(\{/) { /{(.*?)}/; push ( @allscores, "$black,$level,$1" ); $level++; } } } foreach (@allscores) { ( $black, $level, $score ) = split /,/; $score =~ /:([-+]\d+\.\d+)/; $value = $1; if ( $level == 1 ) { if ( $previous_level == 1 ) { push ( @bestblack, undef ); push ( @bestwhite, undef ); } if ($value) { push ( @scores, $value ); } else { push ( @scores, undef ); } } else { if ($black) { push ( @bestblack, $value ); push ( @bestwhite, undef ); } else { push ( @bestblack, undef ); push ( @bestwhite, $value ); } } $previous_level = $level; }
- @scores Craftie's positional analysis for each side's move.
- @bestwhite Craftie's best short at improving white's move.
- @bestblack Craftie's best short at improving black's move.
So at this point we have the data, what about the pretty pictures? Well the first approach I used was to add use GD::Graph::mixed; to the top of my script and then this at the bottom:
Of course this isn't immediately useful, but a trival web page with a single image reference quickly gets you something you can hot-key to!my @data = ( [0..scalar(@scores) - 1], [@scores[0..scalar(@scores) - 1]], [@bestwhite[0..scalar(@scores) - 1]], [@bestblack[0..scalar(@scores) - 1]], ); my $graph = GD::Graph::mixed->new(800, 700); $graph->set( x_label => 'Performance By Half-move', y_label => 'Scoring By Centipawn', title => 'Game Performance Graph', y_max_value => 15, y_min_value => -15, y_tick_number => 30, y_label_skip => 2, zero_axis => 1, dclrs => [qw(lgray red green)], types => [qw(bars linespoints linespoints)], ); my $gd = $graph->plot(\@data); open(IMAGE,'>image.png') or die "Couldn't open image.png:$!\n"; binmode IMAGE; print IMAGE $gd->png(); close IMAGE;
The second approach uses the same code to generate the arrays, but instead of GD as the graphics engine, I thought I'd investigate SVG instead. The necessary difference looks like this:
Yes, it is quite a bit longer, but it is also more interesting---at least to me!openSVG( 450, 450 ); openG( transform => 'translate(10,85) scale(1,-1)' ); openG( transform => 'scale(5)' ); foreach ( -15 .. 15 ) { hline( 0, 80, $_ ); } foreach ( 0 .. 80 ) { vline( -15, 15, $_ ); } path( d => 'M -1 0 H 81', style => 'stroke: red; stroke-opacity: .25; stroke-width: .1' ); graphline( 'black', 0.1, @scores ); graphline( 'green', 0.1, @bestblack ); graphline( 'red', 0.1, @bestwhite ); closeG(); closeG(); closeSVG(); sub path { openTAG( 'path', @_ ); closeTAG(); } sub openTAG { my $tag = shift; my %attributes = @_; print "<$tag"; foreach ( keys %attributes ) { print " $_=\"$attributes{$_}\""; } } sub closeTAG { my $s = shift; if ($s) { print "</$s>\n"; } else { print "/>\n"; } } sub openG { openTAG( 'g', @_ ); print ">\n"; } sub closeG { closeTAG('g'); } sub openSVG { my $height = shift; my $width = shift; print "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\" +?>\n"; print "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\"\n"; print " \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dt +d\">\n"; print "<svg height=\"$height\" width=\"$width\" viewbox=\"0,0,$width,$height +\">\n"; } sub closeSVG { closeTAG('svg'); } sub hline { my ( $x1, $x2, $y1, $color ) = @_; if ($color) { path( d => "M $x1,$y1 H $x2", style => "stroke: $color;" ); } else { path( d => "M $x1,$y1 H $x2" ); } } sub vline { my ( $y1, $y2, $x1, $color ) = @_; if ($color) { path( d => "M $x1,$y1 V $y2", style => "stroke: $color;" ); } else { path( d => "M $x1,$y1 V $y2" ); } } sub graphline { my $color = shift; my $width = shift; my @values = @_; my @list; my $y = 0; foreach (@values) { if ($_) { push ( @list, $y ); push ( @list, $_ ); } $y++; } openTAG( 'polyline', points => join ( ',', @list ), style => "stroke: $color; stroke-width: $width; fill: none;", ); closeTAG(); }
2 Actually, I don't do it this way, I use a perl script that does all of the work for me in good 'lazy' programmer fashion!
--hsm
"Never try to teach a pig to sing...it wastes your time and it annoys the pig."
|
---|