File Coverage

blib/lib/Chess/Opening/Book/Polyglot.pm
Criterion Covered Total %
statement 118 118 100.0
branch 31 48 64.5
condition 3 3 100.0
subroutine 11 11 100.0
pod 1 1 100.0
total 164 181 90.6


line stmt bran cond sub pod time code
1             #! /bin/false
2              
3             # Copyright (C) 2019 Guido Flohr ,
4             # all rights reserved.
5              
6             # This program is free software. It comes without any warranty, to
7             # the extent permitted by applicable law. You can redistribute it
8             # and/or modify it under the terms of the Do What the Fuck You Want
9             # to Public License, Version 2, as published by Sam Hocevar. See
10             # http://www.wtfpl.net/ for more details.
11              
12             # Make Dist::Zilla happy.
13             # ABSTRACT: Read chess opening books in polyglot format
14              
15             package Chess::Opening::Book::Polyglot;
16             $Chess::Opening::Book::Polyglot::VERSION = '0.3';
17 2     2   137644 use common::sense;
  2         23  
  2         17  
18              
19 2     2   128 use base 'Chess::Opening::Book';
  2         4  
  2         1020  
20              
21 2     2   16 use Fcntl qw(:seek);
  2         4  
  2         221  
22              
23 2     2   1132 use Chess::Opening::Book::Polyglot::Random64;
  2         6  
  2         993  
24              
25             sub new {
26 1     1 1 90 my ($class, $filename) = @_;
27              
28 1 50       46 open my $fh, '<', $filename
29             or die __x("error opening '{filename}': {error}!\n",
30             filename => $filename, error => $!);
31              
32 1 50       74 $fh->sysseek(0, SEEK_END)
33             or die __x("error seeking '{filename}': {error}!\n",
34             filename => $filename, error => $!);
35            
36 1         8816 my $size = $fh->sysseek(0, SEEK_CUR);
37 1 50       14 die __x("error getting position in '{filename}': {error}!\n",
38             filename => $filename, error => $!)
39             if $size < 0;
40            
41 1 50       5 die __x("error: {filename}: file size {size} is not a multiple of 16!\n",
42             filename => $filename, size => $size, error => $!)
43             if $size & 0xf;
44 1         4 my $num_entries = $size >> 4;
45 1         12 bless {
46             __fh => $fh,
47             __filename => $filename,
48             __num_entries => $num_entries,
49             }, $class;
50             }
51              
52             # Do a binary search in the file for the requested position.
53             # Using variations of the binary search like interpolation search or the
54             # newer adaptive search or hybrid search
55             # (https://arxiv.org/ftp/arxiv/papers/1708/1708.00964.pdf) is less performant
56             # because it involves significantly more disk access.
57             # This method returns a range of matching records.
58             sub _findKey {
59 56     56   112 my ($self, $key) = @_;
60              
61 56 50       138 return if !$self->{__num_entries};
62              
63 56         76 my $left = 0;
64 56         87 my $right = $self->{__num_entries};
65              
66 56         79 my $found = '';
67 56         65 my $mid;
68 56         111 while ($left < $right) {
69 286         479 $mid = $left + (($right - $left) >> 1);
70 286         625 $found = $self->__getEntryKey($mid);
71 286 100       749 if ($found gt $key) {
    100          
72 107 50       253 $right = $right == $mid ? $mid - 1 : $mid;
73             } elsif ($found lt $key) {
74 123 50       286 $left = $left == $mid ? $mid + 1 : $mid;
75             } else {
76 56         135 last;
77             }
78             }
79              
80             # Found?
81 56 50       124 return if $key ne $found;
82              
83 56         87 my $first = $mid;
84 56         72 my $last = $mid;
85 56         115 while ($first - 1 >= 0) {
86 92         200 $found = $self->__getEntryKey($first - 1);
87 92 100       235 last if $found ne $key;
88 37         80 --$first;
89             }
90 56         168 while ($last + 1 < $self->{__num_entries}) {
91 95         225 $found = $self->__getEntryKey($last + 1);
92 95 100       235 last if $found ne $key;
93 40         90 ++$last;
94             }
95              
96 56         204 return ($first, $last);
97             }
98              
99             sub _getKey {
100 65     65   5118 my ($whatever, $fen) = @_;
101              
102 2     2   1403 use integer;
  2         31  
  2         17  
103              
104 65         126 my $key = "\x00" x 8;
105              
106             # 32-bit safe xor routine.
107             my $xor = sub {
108 2358     2358   3709 my ($left, $right) = @_;
109              
110 2358         4269 my @llongs = unpack 'NN', $left;
111 2358         3593 my @rlongs = unpack 'NN', $right;
112 2358         3173 $llongs[0] ^= $rlongs[0];
113 2358         2944 $llongs[1] ^= $rlongs[1];
114              
115 2358         5909 return pack 'NN', @llongs;
116 65         336 };
117              
118 65         119 my $random64 = Chess::Opening::Book::Polyglot::Random64::DATA();
119              
120 65 50       194 my %pos = $whatever->_parseFEN($fen) or return;
121 65         235 my %pieces = $whatever->_pieces;
122 65         137 foreach my $spec (@{$pos{pieces}}) {
  65         143  
123 2079         5191 my ($file, $rank) = split //, $spec->{field};
124 2079         3214 $file = (ord $file) - (ord 'a');
125 2079         2601 $rank = (ord $rank) - (ord '1');
126 2079         3251 my $piece = $pieces{$spec->{piece}};
127 2079         3011 my $offset = ($piece << 6) | ($rank << 3) | $file;
128 2079         3281 $key = $xor->($key, $random64->[$offset]);
129             }
130              
131 65         195 my %castling_offsets = (
132             K => 768 + 0,
133             Q => 768 + 1,
134             k => 768 + 2,
135             q => 768 + 3,
136             );
137              
138 65         95 foreach my $char (keys %{$pos{castling}}) {
  65         225  
139 253         374 my $offset = $castling_offsets{$char};
140 253         405 $key = $xor->($key, $random64->[$offset]);
141             }
142              
143 65 50       171 if ($pos{ep}) {
144 65         189 my ($ep_file, $ep_rank) = split //, $pos{ep};
145 65         110 my $ep_char = ord $ep_file;
146             # This may produce invalid coordinates for the a and h rank but this
147             # is harmless.
148 65         101 my @pawns;
149             my $pawn;
150            
151 65 100       161 if ('w' eq $pos{on_move}) {
152 24         83 @pawns = (
153             chr($ep_char - 1) . '5',
154             chr($ep_char + 1) . '5',
155             );
156 24         45 $pawn = 'P';
157             } else {
158 41         136 @pawns = (
159             chr($ep_char - 1) . '4',
160             chr($ep_char + 1) . '4',
161             );
162 41         69 $pawn = 'p';
163             }
164              
165 65         96 SPEC: foreach my $spec(@{$pos{pieces}}) {
  65         133  
166 2048         2770 foreach my $field (@pawns) {
167 4094 100 100     7815 if ($spec->{field} eq $field && $spec->{piece} eq $pawn) {
168 2         6 my $offset = 772 + $ep_char - ord 'a';
169 2         7 $key = $xor->($key, $random64->[$offset]);
170 2         10 last SPEC;
171             }
172             }
173             }
174             }
175              
176 65 100       142 if ('w' eq $pos{on_move}) {
177 24         61 $key = $xor->($key, $random64->[780]);
178             }
179              
180 65         1063 return $key;
181             }
182              
183             sub __getEntryKey {
184 473     473   786 my ($self, $number) = @_;
185              
186 473         667 my $offset = $number << 4;
187              
188             $self->{__fh}->sysseek($offset, SEEK_SET)
189             or die __x("error seeking '{filename}': {error}!\n",
190 473 50       1162 filename => $self->{__filename}, error => $!);
191            
192 473         5020 my $key;
193 473         1282 my $bytes_read = $self->{__fh}->sysread($key, 8);
194             die __x("error reading from '{filename}': {error}!\n",
195 473 50       5788 filename => $self->{__filename}, error => $!)
196             if $bytes_read <= 0;
197             die __x("unexpected end-of-file reading from '{filename}'\n",
198 473 50       915 filename => $self->{__filename}, error => $!)
199             if 8 != $bytes_read;
200              
201 473         999 return $key;
202             }
203              
204             sub _getEntry {
205 133     133   227 my ($self, $number) = @_;
206              
207 133         219 my $offset = $number << 4;
208              
209             $self->{__fh}->sysseek($offset, SEEK_SET)
210             or die __x("error seeking '{filename}': {error}!\n",
211 133 50       365 filename => $self->{__filename}, error => $!);
212            
213 133         1535 my $buf;
214 133         381 my $bytes_read = $self->{__fh}->sysread($buf, 16);
215             die __x("error reading from '{filename}': {error}!\n",
216 133 50       1674 filename => $self->{__filename}, error => $!)
217             if $bytes_read <= 0;
218             die __x("unexpected end-of-file reading from '{filename}'\n",
219 133 50       263 filename => $self->{__filename}, error => $!)
220             if 16 != $bytes_read;
221              
222 133         285 my $key = substr $buf, 0, 8;
223            
224 133         450 my ($move, $count, $learn) = unpack 'n2N', substr $buf, 8;
225              
226 133         242 my $to_file = $move & 0x7;
227 133         194 my $to_rank = ($move >> 3) & 0x7;
228 133         171 my $from_file = ($move >> 6) & 0x7;
229 133         206 my $from_rank = ($move >> 9) & 0x7;
230 133         180 my $promote = ($move >> 12) & 0x7;
231 133         323 my @promotion_pieces = (
232             '', 'k', 'b', 'r', 'q'
233             );
234              
235 133         467 my $move = chr($from_file + ord 'a')
236             . chr($from_rank + ord '1')
237             . chr($to_file + ord 'a')
238             . chr($to_rank + ord '1')
239             . $promotion_pieces[$promote];
240             die __x("error: '{filename}' is corrupted\n",
241             filename => $self->{__filename})
242 133 50       675 if $move !~ /^[a-h][1-8][a-h][1-8][kbrq]?$/;
243            
244             return (
245 133         623 move => $move,
246             count => $count,
247             learn => $learn,
248             );
249             }
250              
251             1;