File Coverage

blib/lib/Game/Life/Faster.pm
Criterion Covered Total %
statement 208 213 97.6
branch 58 72 80.5
condition 30 53 56.6
subroutine 36 37 97.3
pod 22 22 100.0
total 354 397 89.1


line stmt bran cond sub pod time code
1             package Game::Life::Faster;
2              
3 2     2   455530 use 5.008001;
  2         6  
4              
5 2     2   9 use strict;
  2         9  
  2         40  
6 2     2   7 use warnings;
  2         2  
  2         90  
7              
8 2     2   8 use Carp;
  2         3  
  2         160  
9 2     2   11 use List::Util qw{ max min };
  2         3  
  2         152  
10              
11             our $VERSION = '0.008';
12              
13 2     2   8 use constant ARRAY_REF => ref [];
  2         2  
  2         132  
14              
15 2     2   9 use constant DEFAULT_BREED => [ 3 ];
  2         3  
  2         94  
16 2     2   8 use constant DEFAULT_LIVE => [ 2, 3 ];
  2         2  
  2         75  
17 2     2   7 use constant DEFAULT_SIZE => 100;
  2         2  
  2         104  
18              
19 2     2   7 use constant NEW_LINE_RE => qr< \n >smx;
  2         3  
  2         145  
20 2     2   7 use constant NON_NEGATIVE_INTEGER_RE => qr< \A [0-9]+ \z >smx;
  2         2  
  2         144  
21 2     2   8 use constant POSITIVE_INTEGER_RE => qr< \A [1-9][0-9]* \z >smx;
  2         2  
  2         104  
22              
23 2     2   6 use constant TOGGLE_STATE => do { bless \my $x, 'Toggle_State' };
  2         3  
  2         2  
  2         75  
24 2     2   15 use constant TOGGLE_STATE_REF => ref TOGGLE_STATE;
  2         2  
  2         2482  
25              
26             sub new {
27 3     3 1 155843 my ( $class, $size, $breed, $live ) = @_;
28              
29 3         5 my $self;
30              
31 3         8 my $ref = ref $size;
32 3 100       13 if ( ARRAY_REF eq $ref ) {
    50          
33 1         2 $self->{size_x} = $size->[1];
34 1         2 $self->{size_y} = $size->[0];
35             } elsif ( ! $ref ) {
36 2         17 $self->{size_x} = $self->{size_y} = $size;
37             } else {
38 0         0 croak "Argument may not be $size";
39             }
40 3   100     14 $self->{size_x} ||= DEFAULT_SIZE;
41 3   100     10 $self->{size_y} ||= DEFAULT_SIZE;
42             $self->{size_x} =~ POSITIVE_INTEGER_RE
43 3 50 33     36 and $self->{size_y} =~ POSITIVE_INTEGER_RE
44             or croak 'Sizes must be positive integers';
45 3         9 $self->{max_x} = $self->{size_x} - 1;
46 3         5 $self->{max_y} = $self->{size_y} - 1;
47              
48 3   66     15 bless $self, ref $class || $class;
49 3         13 $self->set_rules( $breed, $live );
50              
51 3         8 $self->clear();
52              
53 3         19 return $self;
54             }
55              
56             sub clear {
57 4     4 1 8 my ( $self ) = @_;
58 4         17 delete $self->{grid};
59 4         8 delete $self->{changed};
60 4         9 $self->{living_x} = [];
61 4         7 $self->{living_y} = [];
62 4         8 $self->{change_count} = 0;
63 4         6 return $self;
64             }
65              
66             sub get_active_grid_coord {
67 2     2 1 287 my ( $self ) = @_;
68             my ( $min_x, $max_x, $min_y, $max_y ) = ( $self->{size_x}, 0,
69 2         6 $self->{size_y}, 0 );
70 2         2 foreach my $ix ( keys %{ $self->{changed} } ) {
  2         7  
71 10 100       16 $min_x = $ix if $ix < $min_x;
72 10 100       28 $max_x = $ix if $ix > $max_x;
73 10         7 foreach my $iy ( keys %{ $self->{changed}{$ix} } ) {
  10         17  
74 40 100       45 $min_y = $iy if $iy < $min_y;
75 40 100       53 $max_y = $iy if $iy > $max_y;
76             }
77             }
78 2 50       5 $max_x < $min_x
79             and croak 'No active cells';
80 2         6 return [ $min_x, $max_x, $min_y, $max_y ];
81             }
82              
83             sub get_breeding_rules {
84 1     1 1 11347 my ( $self ) = @_;
85 1         11 return $self->get_rule( 'breed' );
86             }
87              
88             sub get_grid {
89 1     1 1 3 my ( $self, $coord ) = @_;
90 1   33     7 $coord ||= $self->get_grid_coord();
91             $self->{grid}
92 1 50       3 or return [ ( [ ( 0 ) x ( $coord->[3] - $coord->[2] + 1 ) ] ) x
93             ( $coord->[1] - $coord->[0] + 1 ) ];
94 1         2 my @rslt;
95 1         4 foreach my $x ( $coord->[0] .. $coord->[1] ) {
96 10 100       13 if ( $self->{grid}{$x} ) {
97 7         7 push @rslt, [];
98 7         10 foreach my $y ( $coord->[2] .. $coord->[3] ) {
99 70         131 push @{ $rslt[-1] }, $self->{grid}{$x}{$y} ?
100 70 100       61 $self->{grid}{$x}{$y}[0] ? 1 : 0 : 0;
    100          
101             }
102             } else {
103 3         8 push @rslt, [ ( 0 ) x ( $coord->[3] - $coord->[2] + 1 ) ];
104             }
105             }
106 1         26 return \@rslt;
107             }
108              
109             sub get_grid_coord {
110 9     9 1 12 my ( $self ) = @_;
111 9         32 return [ 0, $self->{max_x}, 0, $self->{max_y} ];
112             }
113              
114             sub get_living_rules {
115 1     1 1 2 my ( $self ) = @_;
116 1         2 return $self->get_rule( 'live' );
117             }
118              
119             sub get_text_grid {
120 10     10 1 20 my ( $self, $living, $dead, $coord ) = @_;
121 10   50     34 $living ||= 'X';
122 10   50     27 $dead ||= '.';
123 10   66     41 $coord ||= $self->get_grid_coord();
124 10         10 my @rslt;
125 10 100       17 if ( $self->{grid} ) {
126 7         18 foreach my $x ( $coord->[0] .. $coord->[1] ) {
127 58 100       75 if ( $self->{grid}{$x} ) {
128             push @rslt, join '', map {
129 37 100 100     69 ( $self->{grid}{$x}{$_} && $self->{grid}{$x}{$_}[0]) ?
  324         700  
130             $living : $dead
131             } $coord->[2] .. $coord->[3];
132             } else {
133 21         34 push @rslt, $dead x ( $coord->[3] - $coord->[2] + 1 );
134             }
135             }
136             } else {
137 3         12 @rslt = ( $dead x ( $coord->[3] - $coord->[2] + 1 ) ) x (
138             $coord->[1] - $coord->[0] + 1 );
139             }
140 10 50       18 return wantarray ? @rslt : join '', map { "$_\n" } @rslt;
  83         171  
141             }
142              
143             sub get_active_text_grid {
144 1     1 1 658 my ( $self, $living, $dead ) = @_;
145 1         4 return $self->get_text_grid( $living, $dead,
146             $self->get_active_grid_coord() );
147             }
148              
149             sub get_used_text_grid {
150 1     1 1 678 my ( $self, $living, $dead ) = @_;
151 1         2 return $self->get_text_grid( $living, $dead,
152             $self->get_used_grid_coord() );
153             }
154              
155             sub get_used_grid {
156 0     0 1 0 my ( $self ) = @_;
157 0         0 return $self->get_grid( $self->get_used_grid_coord() );
158             }
159              
160             sub get_used_grid_coord {
161 2     2 1 4 my ( $self ) = @_;
162 2         4 my $min_x = $self->{size_x};
163 2         6 for ( $min_x = 0; $min_x < $self->{size_x}; $min_x++ ) {
164 8 100       16 $self->{living_x}[$min_x]
165             or next;
166 2         3 my ( $max_x, $min_y, $max_y );
167 2         4 for ( $max_x = $self->{size_x}; $max_x >= $min_x; ) {
168 10 100       16 $self->{living_x}[--$max_x]
169             and last;
170             }
171 2         6 for ( $min_y = 0; $min_y < $self->{size_y}; $min_y++ ) {
172 6 100       10 $self->{living_y}[$min_y]
173             and last;
174             }
175 2         5 for ( $max_y = $self->{size_y}; $max_y >= $min_y; ) {
176 12 100       18 $self->{living_y}[--$max_y]
177             and last;
178             }
179 2         7 return [ $min_x, $max_x, $min_y, $max_y ];
180             }
181 0         0 croak 'No occupied cells';
182             }
183              
184             sub place_points {
185 1     1 1 282 my ( $self, $x, $y, $array ) = @_;
186 1         2 my $ix = $x;
187 1         2 foreach my $row ( @{ $array } ) {
  1         3  
188 1         2 my $iy = $y;
189 1         1 foreach my $state ( @{ $row } ) {
  1         2  
190 3         7 $self->set_point_state( $ix, $iy, $state );
191 3         3 $iy++;
192             }
193 1         2 $ix++;
194             }
195 1         2 return;
196             }
197              
198             sub place_text_points {
199 3     3 1 12 my ( $self, $x, $y, $living, @array ) = @_;
200 3         5 my $ix = $x;
201 3 100 66     29 1 == @array
202             and $array[0] =~ NEW_LINE_RE
203 2         88 and @array = split qr< @{[ NEW_LINE_RE ]} >smx, $array[0];
204 3         8 foreach my $line ( @array ) {
205 7         9 my $iy = $y;
206 7         39 foreach my $state ( map { $living eq $_ } split qr<>, $line ) {
  17         30  
207 17         27 $self->set_point_state( $ix, $iy, $state );
208 17         20 $iy++;
209             }
210 7         12 $ix++;
211             }
212 3         7 return;
213             }
214              
215             sub process {
216 4     4 1 7 my ( $self, $steps ) = @_;
217 4   100     12 $steps ||= 1;
218              
219 4         9 foreach ( 1 .. $steps ) {
220              
221 13         14 my $changed = delete $self->{changed};
222 13         16 $self->{change_count} = 0;
223              
224 13         12 foreach my $x ( keys %{ $changed } ) {
  13         25  
225 60         55 foreach my $y ( keys %{ $changed->{$x} } ) {
  60         97  
226 253         273 my $cell = $self->{grid}{$x}{$y};
227 2     2   13 no warnings qw{ uninitialized };
  2         11  
  2         1521  
228 253 100       310 if ( $cell->[0] ) {
229 66 100       129 $self->{live}[ $changed->{$x}{$y} ]
230             or $self->unset_point( $x, $y );
231             } else {
232 187 100       312 $self->{breed}[ $changed->{$x}{$y} ]
233             and $self->set_point( $x, $y );
234             }
235             }
236             }
237              
238             $self->{change_count}
239 13 100       52 or last;
240             }
241 4         15 return $self->{change_count};
242             }
243              
244             sub set_point {
245 26     26 1 46 my ( $self, $x, $y ) = @_;
246 26         31 return $self->set_point_state( $x, $y, 1 );
247             }
248              
249             sub set_point_state {
250 74     74 1 98 my ( $self, $x, $y, $state ) = @_;
251              
252 74 50 33     407 defined $x
      33        
      33        
253             and defined $y
254             and $x =~ NON_NEGATIVE_INTEGER_RE
255             and $y =~ NON_NEGATIVE_INTEGER_RE
256             or croak 'Coordinates must be non-negative integers';
257 74 50       106 defined $state
258             or return $state;
259              
260 74 50 33     279 if ( $x >= 0 && $x < $self->{size_x} &&
    0 33        
      33        
261             $y >= 0 && $y < $self->{size_y}
262             ) {
263             # We're on-grid.
264              
265             # This autovivifies, but we're going to assign it anyway, so ...
266 74   100     143 my $prev_state = $self->{grid}{$x}{$y}[0] || 0;
267 74 100       113 $state = TOGGLE_STATE_REF eq ref $state ? 1 - $prev_state :
    100          
268             $state ? 1 : 0;
269              
270 74         92 $self->{grid}{$x}{$y}[0] = $state;
271 74   100     119 $self->{grid}{$x}{$y}[1] ||= 0;
272 74 100       109 my $delta = $state - $prev_state
273             or return $state;
274 68         84 $self->{living_x}[$x] += $delta;
275 68         72 $self->{living_y}[$y] += $delta;
276              
277 68         66 $self->{change_count}++;
278              
279 68         190 foreach my $ix ( max( 0, $x - 1 ) .. min( $self->{max_x}, $x + 1 ) ) {
280 197         334 foreach my $iy ( max( 0, $y - 1 ) .. min( $self->{max_y}, $y + 1 )
281             ) {
282             $self->{changed}{$ix}{$iy} =
283 563         998 $self->{grid}{$ix}{$iy}[1] += $delta;
284             }
285             }
286              
287             # A cell is not its own neighbor, but the above nested loops
288             # assumed that it was. We fix that here, rather than skip it
289             # inside the loops.
290             $self->{changed}{$x}{$y} =
291 68         102 $self->{grid}{$x}{$y}[1] -= $delta;
292              
293             } elsif ( $state ) {
294 0         0 croak 'Attempt to place living cell outside grid';
295             }
296              
297 68         149 return $state;
298             }
299              
300             {
301             my %dflt = (
302             breed => DEFAULT_BREED,
303             live => DEFAULT_LIVE,
304             );
305              
306             sub get_rule {
307 2     2 1 4 my ( $self, $kind ) = @_;
308 2 50       8 $dflt{$kind}
309             or croak "'$kind' is not a valid rule kind";
310 2         2 return( grep { $self->{$kind}[$_] } 0 .. $#{ $self->{$kind} } );
  8         19  
  2         6  
311             }
312              
313             sub set_rule {
314 6     6 1 13 my ( $self, $kind, $rule ) = @_;
315 6 50       19 $dflt{$kind}
316             or croak "'$kind' is not a valid rule name";
317 6   33     39 $rule ||= $dflt{$kind};
318 6 50       15 ARRAY_REF eq ref $rule
319             or croak "\u$kind rule must be an array reference";
320 6         21 $self->{$kind} = [];
321 6         10 foreach ( @{ $rule } ) {
  6         13  
322 9 50       27 $_ =~ NON_NEGATIVE_INTEGER_RE
323             or croak "\u$kind rule must be a reference to an array of non-negative integers";
324 9         19 $self->{$kind}[$_] = 1;
325             }
326 6         11 return;
327             }
328             }
329              
330             sub set_rules {
331 3     3 1 7 my ( $self, $breed, $live ) = @_;
332 3         10 $self->set_rule( breed => $breed );
333 3         6 $self->set_rule( live => $live );
334 3         4 return;
335             }
336              
337             sub toggle_point {
338 2     2 1 7 my ( $self, $x, $y ) = @_;
339 2         11 return $self->set_point_state( $x, $y, TOGGLE_STATE );
340             }
341              
342             sub unset_point {
343 26     26 1 40 my ( $self, $x, $y ) = @_;
344 26         41 return $self->set_point_state( $x, $y, 0 );
345             }
346              
347             1;
348              
349             __END__