File Coverage

blib/lib/Chess/Rep/Coverage.pm
Criterion Covered Total %
statement 146 184 79.3
branch 38 64 59.3
condition 23 32 71.8
subroutine 12 18 66.6
pod 3 3 100.0
total 222 301 73.7


line stmt bran cond sub pod time code
1             package Chess::Rep::Coverage;
2             BEGIN {
3 2     2   76333 $Chess::Rep::Coverage::AUTHORITY = 'cpan:GENE';
4             }
5             # ABSTRACT: Expose chess ply potential energy
6              
7 2     2   19 use strict;
  2         4  
  2         77  
8 2     2   16 use warnings;
  2         4  
  2         77  
9              
10 2     2   2159 use parent 'Chess::Rep';
  2         676  
  2         11  
11              
12 2     2   57494 use constant SIZE => 7;
  2         5  
  2         6063  
13              
14             our $VERSION = '0.1101';
15              
16              
17             sub coverage {
18 3     3 1 20751 my $self = shift;
19              
20             # What is the state of our board?
21 3         20 my $fen = $self->get_fen();
22              
23             # Return a bucket of piece coverages.
24 3         1748 my $cover = {};
25              
26             # Get the set of pieces and ids.
27 3         8 my %pieces;
28 3         4 @pieces{values %{+Chess::Rep::PIECE_TO_ID()}} = keys %{+Chess::Rep::PIECE_TO_ID()};
  3         59  
  3         20  
29              
30             # Look at each board position.
31 3         14 for my $row (0 .. SIZE) {
32 24         49 for my $col (0 .. SIZE) {
33 192         531 my $p = $self->get_piece_at($row, $col); # decimal of index
34 192 100       2317 if ($p) {
35 37         120 my $c = Chess::Rep::piece_color($p); # 0=black, 0x80=white
36 37         221 my $i = Chess::Rep::get_index($row, $col); # $row << 4 | $col
37 37         230 my $f = Chess::Rep::get_field_id($i); # A-H, 1-8
38              
39             # Set the coverage properties for the piece.
40 37         750 $cover->{$f}{occupant} = $pieces{$p};
41 37         104 $cover->{$f}{piece} = $p;
42 37         88 $cover->{$f}{color} = $c;
43 37         99 $cover->{$f}{index} = $i;
44 37         100 $cover->{$f}{protects} = [];
45 37         94 $cover->{$f}{threatens} = [];
46              
47             # Kings are special-cased.
48 37 100 100     226 if ($p & 0x04) {
    100          
49             # Collect the moves of the piece.
50 3         11 $cover->{$f}{move} = $self->_fetch_new_moves($f, $i, $c);
51              
52             # Inspect the positions surrounding the king.
53 3         29 for my $m ([$row, $col + 1], [$row + 1, $col], [$row + 1, $col + 1], [$row + 1, $col - 1],
54             [$row, $col - 1], [$row - 1, $col], [$row - 1, $col - 1], [$row - 1, $col + 1]
55             ) {
56 24         62 my $x = Chess::Rep::get_index(@$m);
57 24 100       129 next if $x & 0x88;
58 18         39 $self->_set_piece_status($cover, $f, $x, $c);
59             }
60             }
61             # Attacking pawns are special-cased.
62             elsif (($p & 0x01) && $self->to_move != $c) {
63 9 50       194 my $moves = $c == 0
64             ? [ [$row - 1, $col + 1], [$row - 1, $col - 1] ]
65             : [ [$row + 1, $col + 1], [$row + 1, $col - 1] ];
66             # Add diagonal positions unless occupied.
67 9         40 for my $m (@$moves) {
68 18 100 33     158 next if $m->[0] < 0 || $m->[0] > SIZE
      66        
      100        
69             || $m->[1] < 0 || $m->[1] > SIZE;
70 16         38 my $x = Chess::Rep::get_index(@$m);
71 16         82 $self->_set_piece_status($cover, $f, $x, $c);
72             # Collect the moves of the piece.
73 16         20 push @{ $cover->{$f}{move} }, $x;
  16         68  
74             }
75             }
76             else {
77             # Invert the FEN to compute all possible moves, threats and protections.
78 25         197 my $inverted = _invert_fen($fen, $row, $col, $c);
79 25         1198 $self->set_from_fen($inverted);
80              
81             # Collect the moves of the piece.
82 25         157233 $cover->{$f}{move} = $self->_fetch_new_moves($f, $i, $c);
83              
84             # Reset original game FEN.
85 25         113 $self->set_from_fen($fen);
86              
87             # Find the threats and protections by the piece.
88 25         207271 $self->_set_piece_status($cover, $f, $_, $c) for @{$cover->{$f}{move}};
  25         389  
89             }
90             }
91             }
92             }
93              
94             # Compute piece and position status.
95 3         49 for my $piece (keys %$cover) {
96 37   100     210 $cover->{$piece}{is_threatened_by} ||= [];
97 37   100     155 $cover->{$piece}{is_protected_by} ||= [];
98              
99             # Compute protection status of a piece.
100 37         45 for my $index (@{$cover->{$piece}{protects}}) {
  37         95  
101 40         547 my $f = Chess::Rep::get_field_id($index); # A-H, 1-8
102 40         910 push @{$cover->{$f}{is_protected_by}}, $cover->{$piece}{index};
  40         165  
103             }
104              
105             # Compute threat status of a piece.
106 37         50 for my $index (@{$cover->{$piece}{threatens}}) {
  37         91  
107 3         10 my $f = Chess::Rep::get_field_id($index); # A-H, 1-8
108 3         35 push @{$cover->{$f}{is_threatened_by}}, $cover->{$piece}{index};
  3         15  
109             }
110              
111             # Compute move status of a position.
112 37         52 for my $index (@{$cover->{$piece}{move}}) {
  37         72  
113 88         362 my $p = $self->get_piece_at($index);
114 88 100       934 if (!$p) {
115 53         427 my $f = Chess::Rep::get_field_id($index); # A-H, 1-8
116              
117 53   100     779 $cover->{$f}{white_can_move_here} ||= [];
118 53   100     211 $cover->{$f}{black_can_move_here} ||= [];
119              
120 53 100       265 my $color = $cover->{$piece}{color} ? 'white' : 'black';
121 53         64 push @{$cover->{$f}{$color . '_can_move_here'}}, $cover->{$piece}{index};
  53         465  
122             }
123             }
124             }
125              
126             # Set the object coverage attribute.
127 3         16 $self->_cover($cover);
128              
129 3         69 return $cover;
130             }
131              
132             sub _invert_fen {
133 25     25   94 my ($fen, $row, $col, $color) = @_;
134              
135             # Grab the board positions only.
136 25         47 my $suffix = '';
137 25 50       476 if ($fen =~ /^(.+?)\s(.*)$/) {
138 25         160 ($fen, $suffix) = ($1, $2);
139             }
140             # Convert pieces to all black or all white, given the piece color.
141 25 100       102 $fen = $color ? lc $fen : uc $fen;
142             # Split the FEN into rows.
143 25         196 my @fen = split /\//, $fen; # rows: 7..0, cols: 0..7
144             # The FEN sections are the rows reversed.
145 25         59 $row = SIZE - $row;
146              
147 25         80 my $position = 0;
148 25         36 my $counter = 0;
149             # Inspect each character in the row to find the position of the piece to invert.
150 25         258 for my $i (split //, $fen[$row]) {
151             # Increment the position if we are on a digit.
152 105 100       240 if ($i =~ /^\d$/) {
153 3         11 $position += $i;
154             }
155             else {
156             # Invert the piece character (to its original state) or increment the position.
157 102 100       183 if ($position == $col) {
158 25         112 substr($fen[$row], $counter, 1) = $i ^ "\x20";
159 25         61 last;
160             }
161             else {
162             # Next!
163 77         112 $position++;
164             }
165             }
166              
167             # Increment the loop counter.
168 80         437 $counter++;
169             }
170              
171 25         196 return join('/', @fen) . " $suffix";
172             }
173              
174             sub _fetch_new_moves {
175 28     28   85 my $self = shift;
176 28         62 my($field, $index, $color) = @_;
177             # Set the "next to move" color to the piece.
178 28         121 $self->to_move($color);
179             # Recompute the move status.
180 28         251 $self->compute_valid_moves;
181             # TODO Pawns can move diagonally to capture. That is a valid move in the abstract.
182             # Collect the moves of the piece.
183 28         40252 return [ map { $_->{to} } grep { $_->{from} == $index } @{ $self->status->{moves} } ];
  72         221  
  113         797  
  28         161  
184             }
185              
186             sub _set_piece_status {
187 99     99   156 my $self = shift;
188 99         170 my($cover, $field, $index, $color) = @_;
189             # Only consider positions with pieces.
190 99         311 my $p = $self->get_piece_at($index);
191 99 100       1379 return unless $p;
192             # Set the protection or threat status of the piece.
193 46 100       147 if (Chess::Rep::piece_color($p) == $color) {
194             # Any piece can be protected but a king.
195 43 100 100     428 push @{$cover->{$field}{protects}}, $index
  40         320  
196             unless $p == 4 or $p == 132;
197             }
198             else {
199             # Any piece can be threatened.
200 3         25 push @{$cover->{$field}{threatens}}, $index;
  3         24  
201             }
202             }
203              
204             sub _cover {
205 223     223   276 my $self = shift;
206 223 100       445 $self->{cover} = shift if @_;
207 223         805 return $self->{cover};
208             }
209              
210              
211             sub board {
212 2     2 1 5980 my $self = shift;
213 2         6 my %args = @_;
214              
215             # Compute coverage if has not been done yet.
216 2 50       7 $self->coverage() unless $self->_cover();
217              
218             # Start rendering the board.
219 2         7 my $board = _ascii_board('header');
220 2         6 $board .= _ascii_board('row');
221              
222             # Look at each board position.
223 2         7 for my $row (reverse(1 .. 8)) {
224             # Render the beginning of the row.
225 16         33 $board .= $row . _ascii_board('cell_pad');
226              
227 16         49 for my $col ('A' .. 'H') {
228             # Render a new cell.
229 128         252 $board .= _ascii_board('new_cell');
230              
231             # Inspect the coverage at the column and row position.
232 128 100       299 if ($self->_cover()->{$col . $row}) {
233 19 100 66     37 if (exists $self->_cover()->{$col . $row}->{is_protected_by} and
    50 33        
234             exists $self->_cover()->{$col . $row}->{is_threatened_by}
235             ) {
236             # Show threat and protection status.
237 5         12 my $protects = $self->_cover()->{$col . $row}->{is_protected_by};
238 5         13 my $threats = $self->_cover()->{$col . $row}->{is_threatened_by};
239 5         16 $board .= @$protects . '/' . @$threats;
240             # $board .= $self->_cover()->{$col . $row}->{occupant};
241             }
242             elsif (exists $self->_cover()->{$col . $row}->{white_can_move_here} and
243             exists $self->_cover()->{$col . $row}->{black_can_move_here}
244             ) {
245             # Show player movement status.
246 14         26 my $whites = $self->_cover()->{$col . $row}->{white_can_move_here};
247 14         31 my $blacks = $self->_cover()->{$col . $row}->{black_can_move_here};
248 14         36 $board .= @$whites . ':' . @$blacks;
249             # $board .= $self->_cover()->{$col . $row}->{occupant};
250             }
251             }
252             else {
253             # Render an empty cell.
254 109         227 $board .= _ascii_board('empty_cell');
255             }
256              
257             # Render the end of a cell.
258 128         266 $board .= _ascii_board('cell_pad');
259             # Render the end of a column if we have reached the last.
260 128 100       386 $board .= _ascii_board('col_edge') if $col eq 'H';
261             }
262              
263             # Render the end of a row.
264 16         34 $board .= "\n" . _ascii_board('row');
265             }
266              
267 2         17 return $board;
268             }
269              
270             sub _ascii_board {
271 417     417   527 my $section = shift;
272              
273 417         527 my ($cells, $size, $empty) = (8, 5, 3);
274              
275 417         1251 my %board = (
276             cell_pad => ' ',
277             col_edge => '|',
278             corner => '+',
279             row_edge => '-',
280             );
281 417         945 $board{edge} = $board{corner} . ($board{row_edge} x $size);
282 417         1213 $board{row} = ($board{cell_pad} x ($empty - 1)) . ($board{edge} x $cells) . $board{corner} . "\n";
283 417         803 $board{empty_cell} = $board{cell_pad} x $empty;
284 417         769 $board{new_cell} = $board{col_edge} . $board{cell_pad};
285 417         1274 $board{header} = ($board{cell_pad} x $size) . join($board{cell_pad} x $size, 'A' .. 'H') . "\n";
286              
287 417         1682 return $board{$section};
288             }
289              
290              
291             sub move_probability {
292 0     0 1   my ($moves, $threat, $threatened, $protect, $protected) = @ARGV;
293              
294             # Bail-out unless the number of moves (greater than or equal to 0).
295 0 0 0       die _usage() unless $moves and $moves >= 0;
296              
297             # Set threat penalty and protection rewards.
298 0           $threat = _set_level('threat', $threat, $moves);
299 0           $protect = _set_level('protect', $protect, $moves);
300              
301             # Create a piece that is unprotected, unthreatened and unbounded.
302 0           my $piece = [ map { 1 / $_ } ($moves) x $moves ];
  0            
303              
304             # Apply threatened and protected states to move probabilities.
305 0           $piece = _influence($threatened, $threat, $piece, 'threat');
306 0           _output_state($piece);
307 0           $piece = _influence($protected, $protect, $piece);
308 0           _output_state($piece);
309             }
310              
311             sub _influence {
312 0     0     my($influenced, $score, $piece, $state) = @_;
313              
314             # "Move along. Nothing to compute here."
315 0 0         return $piece unless $influenced;
316              
317             # Convenience variable for "size of piece" == "number of moves."
318 0           my $size = @$piece - 1;
319              
320             # Traverse the influenced moves and compute the probabilities.
321 0           for my $move (split ',', $influenced) {
322             # Move counter.
323 0           my $n = 0;
324              
325             # Re-evaluate each move given the threat value.
326 0           for my $p (@$piece) {
327 0 0         if ($n + 1 == $move) {
328             # We've found an influenced move!
329 0 0         $p = $state
330             ? $p - $score # For threats, add the score to the move.
331             : $p + $score; # For protection, subract from the move.
332             }
333             else {
334             # All moves not influenced.
335 0 0         if ($size != 0) {
336 0 0         $p = $state
337             ? $p + $score / $size # For threats, subtract a fraction of the score.
338             : $p - $score / $size; # For protection, add a fraction of the score.
339             }
340             }
341              
342             # Increment the move number of the piece.
343 0           $n++;
344             }
345             }
346              
347             # Make sure all elements sum to 1.
348 0           _cross_check($piece);
349              
350 0           return $piece;
351             }
352              
353             sub _output_state {
354 0     0     my $piece = shift;
355 0           my $i = 0;
356 0           print 'P: ', join(' ', map { sprintf '%d:%.4f', ++$i, $_ } @$piece), "\n";
  0            
357             }
358              
359             sub _set_level {
360 0     0     my ($level, $value, $moves) = @_;
361              
362             # Unless given, default value is zero.
363 0 0         $value = defined $value ? $value : 0;
364             # Bail out unless the value is either zero or greater than moves.
365 0 0         die ucfirst($level) . "level must be zero or more.\n" if $value < 0;
366              
367             # Set a non-zero value in relation to the number of moves.
368 0 0         $value = $moves + $value - 1 if $value > 0;
369              
370             # Make value something that can be used in probability equations.
371 0 0         $value = 1 / $value if $value != 0;
372              
373 0           return $value;
374             }
375              
376             sub _cross_check {
377 0     0     my $vector = shift;
378 0           my $sum = 0;
379             # Make sure all elements sum to unity.
380 0           $sum += $_ for @$vector;
381             # TODO Make == work, instead of eq.
382 0 0         warn "Sum: $sum\n" if $sum ne '1';
383             }
384              
385             sub _usage {
386 0     0     return <
387              
388             Compute probabilites of chess moves in a protective, threatening environment.
389             Copyright 2012 Gene Boggs
390              
391             Usage: perl $0 [0-9...] [0-9...] [t1,t2...] [0-9...] [p1,p2...]
392              
393             Ordered arguments:
394             'moves' is the number of moves of a piece.
395             For example, a unobstructed knight can make eight moves.
396             'threat' is the value or score of a single threat.
397             'threatened moves' is a CSV list of threatened move numbers.
398             This means that you can be captured by your enemy if you move there.
399             'protect' is the value or score of a single protection.
400             'protected moves' is a CSV list of protected move numbers.
401             This means that you will be protected by an ally if you move there.
402              
403             Examples:
404             perl move-probability 8 # An unobstructed, unprotected knight
405             perl move-probability 8 1 # Same
406             perl move-probability 8 1 0 # Ditto
407             perl move-probability 8 1 0 1 # "
408             perl move-probability 8 1 0 1 0 # Right. Gotchya. 10-4 Good buddy.
409             perl move-probability 8 1 1,8 # Threaten the 1st & 8th moves.
410             perl move-probability 8 0 0 1 1,2 # Protect the 1st & 2nd moves.
411             perl move-probability 8 1 1,8 1 1,2 # Protect & threaten
412             perl move-probability 8 10 1,8 # Threaten with a penalty of 10.
413              
414             * This logic does not treat "not making a move" as a value, at the moment...
415             USAGE
416             }
417            
418             1;
419              
420             __END__