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.5';
17 2     2   138614 use common::sense;
  2         60  
  2         17  
18              
19 2     2   143 use 5.12.0;
  2         8  
20              
21 2     2   12 use base 'Chess::Opening::Book';
  2         5  
  2         1041  
22              
23 2     2   15 use Fcntl qw(:seek);
  2         6  
  2         228  
24 2     2   940 use IO::Seekable 1.20;
  2         14265  
  2         215  
25              
26 2     2   1158 use Chess::Opening::Book::Polyglot::Random64;
  2         7  
  2         991  
27              
28             sub new {
29 1     1 1 102 my ($class, $filename) = @_;
30              
31 1 50       46 open my $fh, '<', $filename
32             or die __x("error opening '{filename}': {error}!\n",
33             filename => $filename, error => $!);
34              
35 1 50       19 $fh->sysseek(0, SEEK_END)
36             or die __x("error seeking '{filename}': {error}!\n",
37             filename => $filename, error => $!);
38            
39 1         19 my $size = $fh->sysseek(0, SEEK_CUR);
40 1 50       14 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         5 my $num_entries = $size >> 4;
48 1         13 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   115 my ($self, $key) = @_;
63              
64 56 50       124 return if !$self->{__num_entries};
65              
66 56         133 my $left = 0;
67 56         87 my $right = $self->{__num_entries};
68              
69 56         93 my $found = '';
70 56         73 my $mid;
71 56         123 while ($left < $right) {
72 286         447 $mid = $left + (($right - $left) >> 1);
73 286         629 $found = $self->__getEntryKey($mid);
74 286 100       764 if ($found gt $key) {
    100          
75 107 50       245 $right = $right == $mid ? $mid - 1 : $mid;
76             } elsif ($found lt $key) {
77 123 50       320 $left = $left == $mid ? $mid + 1 : $mid;
78             } else {
79 56         450 last;
80             }
81             }
82              
83             # Found?
84 56 50       154 return if $key ne $found;
85              
86 56         81 my $first = $mid;
87 56         80 my $last = $mid;
88 56         114 while ($first - 1 >= 0) {
89 92         198 $found = $self->__getEntryKey($first - 1);
90 92 100       247 last if $found ne $key;
91 37         76 --$first;
92             }
93 56         154 while ($last + 1 < $self->{__num_entries}) {
94 95         218 $found = $self->__getEntryKey($last + 1);
95 95 100       247 last if $found ne $key;
96 40         86 ++$last;
97             }
98              
99 56         224 return ($first, $last);
100             }
101              
102             sub _getKey {
103 65     65   5184 my ($whatever, $fen) = @_;
104              
105 2     2   1136 use integer;
  2         30  
  2         14  
106              
107 65         120 my $key = "\x00" x 8;
108              
109             # 32-bit safe xor routine.
110             my $xor = sub {
111 2358     2358   3953 my ($left, $right) = @_;
112              
113 2358         4371 my @llongs = unpack 'NN', $left;
114 2358         3843 my @rlongs = unpack 'NN', $right;
115 2358         3200 $llongs[0] ^= $rlongs[0];
116 2358         3066 $llongs[1] ^= $rlongs[1];
117              
118 2358         6300 return pack 'NN', @llongs;
119 65         328 };
120              
121 65         111 my $random64 = Chess::Opening::Book::Polyglot::Random64::DATA();
122              
123 65 50       187 my %pos = $whatever->_parseFEN($fen) or return;
124 65         237 my %pieces = $whatever->_pieces;
125 65         132 foreach my $spec (@{$pos{pieces}}) {
  65         146  
126 2079         5299 my ($file, $rank) = split //, $spec->{field};
127 2079         3230 $file = (ord $file) - (ord 'a');
128 2079         2764 $rank = (ord $rank) - (ord '1');
129 2079         3165 my $piece = $pieces{$spec->{piece}};
130 2079         3101 my $offset = ($piece << 6) | ($rank << 3) | $file;
131 2079         3456 $key = $xor->($key, $random64->[$offset]);
132             }
133              
134 65         239 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         273  
142 253         402 my $offset = $castling_offsets{$char};
143 253         421 $key = $xor->($key, $random64->[$offset]);
144             }
145              
146 65 50       172 if ($pos{ep}) {
147 65         182 my ($ep_file, $ep_rank) = split //, $pos{ep};
148 65         118 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       162 if ('w' eq $pos{on_move}) {
155 24         88 @pawns = (
156             chr($ep_char - 1) . '5',
157             chr($ep_char + 1) . '5',
158             );
159 24         40 $pawn = 'P';
160             } else {
161 41         137 @pawns = (
162             chr($ep_char - 1) . '4',
163             chr($ep_char + 1) . '4',
164             );
165 41         66 $pawn = 'p';
166             }
167              
168 65         124 SPEC: foreach my $spec(@{$pos{pieces}}) {
  65         126  
169 2048         2830 foreach my $field (@pawns) {
170 4094 100 100     8099 if ($spec->{field} eq $field && $spec->{piece} eq $pawn) {
171 2         3 my $offset = 772 + $ep_char - ord 'a';
172 2         6 $key = $xor->($key, $random64->[$offset]);
173 2         9 last SPEC;
174             }
175             }
176             }
177             }
178              
179 65 100       171 if ('w' eq $pos{on_move}) {
180 24         55 $key = $xor->($key, $random64->[780]);
181             }
182              
183 65         1090 return $key;
184             }
185              
186             sub __getEntryKey {
187 473     473   813 my ($self, $number) = @_;
188              
189 473         685 my $offset = $number << 4;
190              
191             $self->{__fh}->sysseek($offset, SEEK_SET)
192             or die __x("error seeking '{filename}': {error}!\n",
193 473 50       1270 filename => $self->{__filename}, error => $!);
194            
195 473         4983 my $key;
196 473         1247 my $bytes_read = $self->{__fh}->sysread($key, 8);
197             die __x("error reading from '{filename}': {error}!\n",
198 473 50       5813 filename => $self->{__filename}, error => $!)
199             if $bytes_read <= 0;
200             die __x("unexpected end-of-file reading from '{filename}'\n",
201 473 50       889 filename => $self->{__filename}, error => $!)
202             if 8 != $bytes_read;
203              
204 473         996 return $key;
205             }
206              
207             sub _getEntry {
208 133     133   238 my ($self, $number) = @_;
209              
210 133         216 my $offset = $number << 4;
211              
212             $self->{__fh}->sysseek($offset, SEEK_SET)
213             or die __x("error seeking '{filename}': {error}!\n",
214 133 50       345 filename => $self->{__filename}, error => $!);
215            
216 133         1526 my $buf;
217 133         391 my $bytes_read = $self->{__fh}->sysread($buf, 16);
218             die __x("error reading from '{filename}': {error}!\n",
219 133 50       1665 filename => $self->{__filename}, error => $!)
220             if $bytes_read <= 0;
221             die __x("unexpected end-of-file reading from '{filename}'\n",
222 133 50       244 filename => $self->{__filename}, error => $!)
223             if 16 != $bytes_read;
224              
225 133         276 my $key = substr $buf, 0, 8;
226            
227 133         476 my ($move, $count, $learn) = unpack 'n2N', substr $buf, 8;
228              
229 133         237 my $to_file = $move & 0x7;
230 133         219 my $to_rank = ($move >> 3) & 0x7;
231 133         220 my $from_file = ($move >> 6) & 0x7;
232 133         197 my $from_rank = ($move >> 9) & 0x7;
233 133         215 my $promote = ($move >> 12) & 0x7;
234 133         308 my @promotion_pieces = (
235             '', 'k', 'b', 'r', 'q'
236             );
237              
238 133         490 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       666 if $move !~ /^[a-h][1-8][a-h][1-8][kbrq]?$/;
246            
247             return (
248 133         635 move => $move,
249             count => $count,
250             learn => $learn,
251             );
252             }
253              
254             1;