File Coverage

blib/lib/Chess/Opening/Book/Polyglot.pm
Criterion Covered Total %
statement 123 123 100.0
branch 31 48 64.5
condition 3 3 100.0
subroutine 13 13 100.0
pod 1 1 100.0
total 171 188 90.9


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.6';
17 2     2   137069 use common::sense;
  2         21  
  2         15  
18              
19 2     2   131 use 5.12.0;
  2         20  
20              
21 2     2   13 use base 'Chess::Opening::Book';
  2         4  
  2         981  
22              
23 2     2   16 use Fcntl qw(:seek);
  2         5  
  2         217  
24 2     2   974 use IO::Seekable 1.20;
  2         15051  
  2         253  
25              
26 2     2   1200 use Chess::Opening::Book::Polyglot::Random64;
  2         6  
  2         1031  
27              
28             sub new {
29 1     1 1 88 my ($class, $filename) = @_;
30              
31 1 50       41 open my $fh, '<', $filename
32             or die __x("error opening '{filename}': {error}!\n",
33             filename => $filename, error => $!);
34              
35 1 50       17 $fh->sysseek(0, SEEK_END)
36             or die __x("error seeking '{filename}': {error}!\n",
37             filename => $filename, error => $!);
38            
39 1         17 my $size = $fh->sysseek(0, SEEK_CUR);
40 1 50       12 die __x("error getting position in '{filename}': {error}!\n",
41             filename => $filename, error => $!)
42             if $size < 0;
43            
44 1 50       4 die __x("error: {filename}: file size {size} is not a multiple of 16!\n",
45             filename => $filename, size => $size, error => $!)
46             if $size & 0xf;
47 1         2 my $num_entries = $size >> 4;
48 1         10 bless {
49             __fh => $fh,
50             __filename => $filename,
51             __num_entries => $num_entries,
52             }, $class;
53             }
54              
55             # Do a binary search in the file for the requested position.
56             # Using variations of the binary search like interpolation search or the
57             # newer adaptive search or hybrid search
58             # (https://arxiv.org/ftp/arxiv/papers/1708/1708.00964.pdf) is less performant
59             # because it involves significantly more disk access.
60             # This method returns a range of matching records.
61             sub _findKey {
62 56     56   123 my ($self, $key) = @_;
63              
64 56 50       139 return if !$self->{__num_entries};
65              
66 56         82 my $left = 0;
67 56         77 my $right = $self->{__num_entries};
68              
69 56         86 my $found = '';
70 56         75 my $mid;
71 56         112 while ($left < $right) {
72 286         441 $mid = $left + (($right - $left) >> 1);
73 286         596 $found = $self->__getEntryKey($mid);
74 286 100       747 if ($found gt $key) {
    100          
75 107 50       264 $right = $right == $mid ? $mid - 1 : $mid;
76             } elsif ($found lt $key) {
77 123 50       287 $left = $left == $mid ? $mid + 1 : $mid;
78             } else {
79 56         86 last;
80             }
81             }
82              
83             # Found?
84 56 50       135 return if $key ne $found;
85              
86 56         85 my $first = $mid;
87 56         71 my $last = $mid;
88 56         132 while ($first - 1 >= 0) {
89 92         206 $found = $self->__getEntryKey($first - 1);
90 92 100       246 last if $found ne $key;
91 37         75 --$first;
92             }
93 56         141 while ($last + 1 < $self->{__num_entries}) {
94 95         221 $found = $self->__getEntryKey($last + 1);
95 95 100       241 last if $found ne $key;
96 40         99 ++$last;
97             }
98              
99 56         205 return ($first, $last);
100             }
101              
102             sub _getKey {
103 65     65   4964 my ($whatever, $fen) = @_;
104              
105 2     2   1118 use integer;
  2         30  
  2         12  
106              
107 65         124 my $key = "\x00" x 8;
108              
109             # 32-bit safe xor routine.
110             my $xor = sub {
111 2358     2358   3984 my ($left, $right) = @_;
112              
113 2358         4373 my @llongs = unpack 'NN', $left;
114 2358         3748 my @rlongs = unpack 'NN', $right;
115 2358         3160 $llongs[0] ^= $rlongs[0];
116 2358         2981 $llongs[1] ^= $rlongs[1];
117              
118 2358         6034 return pack 'NN', @llongs;
119 65         331 };
120              
121 65         118 my $random64 = Chess::Opening::Book::Polyglot::Random64::DATA();
122              
123 65 50       185 my %pos = $whatever->_parseFEN($fen) or return;
124 65         259 my %pieces = $whatever->_pieces;
125 65         146 foreach my $spec (@{$pos{pieces}}) {
  65         142  
126 2079         5282 my ($file, $rank) = split //, $spec->{field};
127 2079         3224 $file = (ord $file) - (ord 'a');
128 2079         3042 $rank = (ord $rank) - (ord '1');
129 2079         3178 my $piece = $pieces{$spec->{piece}};
130 2079         2997 my $offset = ($piece << 6) | ($rank << 3) | $file;
131 2079         3439 $key = $xor->($key, $random64->[$offset]);
132             }
133              
134 65         194 my %castling_offsets = (
135             K => 768 + 0,
136             Q => 768 + 1,
137             k => 768 + 2,
138             q => 768 + 3,
139             );
140              
141 65         93 foreach my $char (keys %{$pos{castling}}) {
  65         232  
142 253         377 my $offset = $castling_offsets{$char};
143 253         409 $key = $xor->($key, $random64->[$offset]);
144             }
145              
146 65 50       183 if ($pos{ep}) {
147 65         173 my ($ep_file, $ep_rank) = split //, $pos{ep};
148 65         124 my $ep_char = ord $ep_file;
149             # This may produce invalid coordinates for the a and h rank but this
150             # is harmless.
151 65         113 my @pawns;
152             my $pawn;
153            
154 65 100       149 if ('w' eq $pos{on_move}) {
155 24         84 @pawns = (
156             chr($ep_char - 1) . '5',
157             chr($ep_char + 1) . '5',
158             );
159 24         45 $pawn = 'P';
160             } else {
161 41         139 @pawns = (
162             chr($ep_char - 1) . '4',
163             chr($ep_char + 1) . '4',
164             );
165 41         74 $pawn = 'p';
166             }
167              
168 65         98 SPEC: foreach my $spec(@{$pos{pieces}}) {
  65         128  
169 2048         2805 foreach my $field (@pawns) {
170 4094 100 100     8565 if ($spec->{field} eq $field && $spec->{piece} eq $pawn) {
171 2         4 my $offset = 772 + $ep_char - ord 'a';
172 2         5 $key = $xor->($key, $random64->[$offset]);
173 2         9 last SPEC;
174             }
175             }
176             }
177             }
178              
179 65 100       148 if ('w' eq $pos{on_move}) {
180 24         45 $key = $xor->($key, $random64->[780]);
181             }
182              
183 65         1075 return $key;
184             }
185              
186             sub __getEntryKey {
187 473     473   828 my ($self, $number) = @_;
188              
189 473         644 my $offset = $number << 4;
190              
191             $self->{__fh}->sysseek($offset, SEEK_SET)
192             or die __x("error seeking '{filename}': {error}!\n",
193 473 50       1273 filename => $self->{__filename}, error => $!);
194            
195 473         4679 my $key;
196 473         1472 my $bytes_read = $self->{__fh}->sysread($key, 8);
197             die __x("error reading from '{filename}': {error}!\n",
198 473 50       5588 filename => $self->{__filename}, error => $!)
199             if $bytes_read <= 0;
200             die __x("unexpected end-of-file reading from '{filename}'\n",
201 473 50       970 filename => $self->{__filename}, error => $!)
202             if 8 != $bytes_read;
203              
204 473         952 return $key;
205             }
206              
207             sub _getEntry {
208 133     133   254 my ($self, $number) = @_;
209              
210 133         209 my $offset = $number << 4;
211              
212             $self->{__fh}->sysseek($offset, SEEK_SET)
213             or die __x("error seeking '{filename}': {error}!\n",
214 133 50       360 filename => $self->{__filename}, error => $!);
215            
216 133         1465 my $buf;
217 133         402 my $bytes_read = $self->{__fh}->sysread($buf, 16);
218             die __x("error reading from '{filename}': {error}!\n",
219 133 50       1559 filename => $self->{__filename}, error => $!)
220             if $bytes_read <= 0;
221             die __x("unexpected end-of-file reading from '{filename}'\n",
222 133 50       248 filename => $self->{__filename}, error => $!)
223             if 16 != $bytes_read;
224              
225 133         275 my $key = substr $buf, 0, 8;
226            
227 133         488 my ($move, $count, $learn) = unpack 'n2N', substr $buf, 8;
228              
229 133         244 my $to_file = $move & 0x7;
230 133         208 my $to_rank = ($move >> 3) & 0x7;
231 133         186 my $from_file = ($move >> 6) & 0x7;
232 133         179 my $from_rank = ($move >> 9) & 0x7;
233 133         184 my $promote = ($move >> 12) & 0x7;
234 133         304 my @promotion_pieces = (
235             '', 'k', 'b', 'r', 'q'
236             );
237              
238 133         471 my $move = chr($from_file + ord 'a')
239             . chr($from_rank + ord '1')
240             . chr($to_file + ord 'a')
241             . chr($to_rank + ord '1')
242             . $promotion_pieces[$promote];
243             die __x("error: '{filename}' is corrupted\n",
244             filename => $self->{__filename})
245 133 50       620 if $move !~ /^[a-h][1-8][a-h][1-8][kbrq]?$/;
246            
247             return (
248 133         625 move => $move,
249             count => $count,
250             learn => $learn,
251             );
252             }
253              
254             1;