File Coverage

blib/lib/Chess/Rep/Coverage.pm
Criterion Covered Total %
statement 145 183 79.2
branch 38 64 59.3
condition 23 32 71.8
subroutine 11 17 64.7
pod 3 3 100.0
total 220 299 73.5


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