File Coverage

blib/lib/Game/DijkstraMap.pm
Criterion Covered Total %
statement 271 274 98.9
branch 108 132 81.8
condition 69 101 68.3
subroutine 28 28 100.0
pod 19 20 95.0
total 495 555 89.1


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Dijkstra Map path finding. run perldoc(1) on this file for additional
4             # documentation
5              
6             package Game::DijkstraMap;
7              
8 2     2   181349 use 5.24.0;
  2         17  
9 2     2   12 use warnings;
  2         4  
  2         60  
10              
11 2     2   10 use Carp qw(croak);
  2         4  
  2         137  
12 2     2   14 use List::Util 1.26 qw(shuffle sum0);
  2         37  
  2         197  
13 2     2   1133 use Moo;
  2         22242  
  2         10  
14 2     2   3975 use namespace::clean;
  2         22286  
  2         15  
15 2     2   629 use Scalar::Util qw(looks_like_number);
  2         5  
  2         141  
16              
17             our $VERSION = '1.02';
18              
19 2     2   14 use constant SQRT2 => sqrt(2);
  2         5  
  2         7495  
20              
21             has bad_cost => ( is => 'rw', default => sub { -2147483648 } );
22             has min_cost => ( is => 'rw', default => sub { 0 } );
23             has max_cost => ( is => 'rw', default => sub { 2147483647 } );
24              
25             has costfn => (
26             is => 'rw',
27             default => sub {
28             return sub {
29             my ( $self, $c ) = @_;
30             if ( $c eq '#' ) { return $self->bad_cost }
31             if ( $c eq 'x' ) { return $self->min_cost }
32             return $self->max_cost;
33             };
34             }
35             );
36             has dimap => ( is => 'rw' );
37             has iters => ( is => 'rwp', default => sub { 0 } );
38             has next_m => ( is => 'rw', default => sub { 'next' } );
39             has normfn => ( is => 'rw', default => sub { \&norm_4way } );
40              
41             sub BUILD {
42 9     9 0 44 my ( $self, $param ) = @_;
43             croak "cannot have both map and str2map arguments"
44 9 50 66     47 if exists $param->{'map'} and exists $param->{'str2map'};
45             $self->map( $param->{'map'} )
46 8 50       20 if exists $param->{'map'};
47             $self->map( $self->str2map( $param->{'str2map'} ) )
48 8 100       39 if exists $param->{'str2map'};
49             }
50              
51             sub adjacent_values {
52 5     5 1 4834 my ( $dimap, $r, $c, $maxrow, $maxcol ) = @_;
53 5         10 my @values;
54 5         10 for my $i ( -1, 1 ) {
55 10         15 my $x = $c + $i;
56 10 100 100     41 push @values, $dimap->[$r][$x] if $x >= 0 and $x <= $maxcol;
57 10         19 for my $j ( -1 .. 1 ) {
58 30         42 $x = $r + $i;
59 30         38 my $y = $c + $j;
60 30 100 100     119 push @values, $dimap->[$x][$y]
      100        
      100        
61             if $x >= 0
62             and $x <= $maxrow
63             and $y >= 0
64             and $y <= $maxcol;
65             }
66             }
67 5         21 return @values;
68             }
69              
70             sub adjacent_values_diag {
71 50     50 1 1997 my ( $dimap, $r, $c, $maxrow, $maxcol ) = @_;
72 50         58 my @values;
73 50 100 100     149 push @values, $dimap->[ $r - 1 ][ $c - 1 ] if $r > 0 and $c > 0;
74 50 100 100     130 push @values, $dimap->[ $r - 1 ][ $c + 1 ] if $r > 0 and $c < $maxcol;
75 50 100 100     138 push @values, $dimap->[ $r + 1 ][ $c - 1 ] if $r < $maxrow and $c > 0;
76 50 100 100     137 push @values, $dimap->[ $r + 1 ][ $c + 1 ] if $r < $maxrow and $c < $maxcol;
77 50         186 return @values;
78             }
79              
80             sub adjacent_values_sq {
81 230     230 1 2151 my ( $dimap, $r, $c, $maxrow, $maxcol ) = @_;
82 230         286 my @values;
83 230 100       416 push @values, $dimap->[$r][ $c - 1 ] if $c > 0;
84 230 100       438 push @values, $dimap->[$r][ $c + 1 ] if $c < $maxcol;
85 230 100       402 push @values, $dimap->[ $r - 1 ][$c] if $r > 0;
86 230 100       408 push @values, $dimap->[ $r + 1 ][$c] if $r < $maxrow;
87 230         498 return @values;
88             }
89              
90             sub dimap_with {
91 2     2 1 54 my ( $self, $param ) = @_;
92 2         8 my $dimap = $self->dimap;
93 2 100       15 croak "cannot make new dimap from unset map" if !defined $dimap;
94 1         3 my $new_dimap;
95 1         3 my $badcost = $self->bad_cost;
96 1         4 my $cols = $dimap->[0]->$#*;
97 1         5 for my $r ( 0 .. $dimap->$#* ) {
98 3         8 COL: for my $c ( 0 .. $cols ) {
99 9         13 my $value = $dimap->[$r][$c];
100 9 50       18 if ( $value == $badcost ) {
101 0         0 $new_dimap->[$r][$c] = $badcost;
102 0         0 next COL;
103             }
104 9   50     19 $value *= $param->{my_weight} // 1;
105 9         25 my @here = map $_->values( [ $r, $c ] )->[0], $param->{objs}->@*;
106 9         18 for my $h ( 0 .. $#here ) {
107 17 100       34 if ( $here[$h] == $badcost ) {
108 1         3 $new_dimap->[$r][$c] = $badcost;
109 1         4 next COL;
110             }
111 16   50     45 $value += $here[$h] * ( $param->{weights}->[$h] // 0 );
112             }
113 8         16 $new_dimap->[$r][$c] = $value;
114             }
115             }
116 1         36 return $new_dimap;
117             }
118              
119             sub map {
120 8     8 1 8841 my ( $self, $map ) = @_;
121 8         16 my $dimap = [];
122 8 50 66     84 croak "no valid map supplied"
      66        
      66        
123             if !defined $map
124             or ref $map ne 'ARRAY'
125             or !defined $map->[0]
126             or ref $map->[0] ne 'ARRAY';
127 7         17 my $cols = $map->[0]->@*;
128 7         25 for my $r ( 0 .. $map->$#* ) {
129 21 50       46 croak "unexpected column count at row $r" if $map->[$r]->@* != $cols;
130 21         36 for my $c ( 0 .. $cols - 1 ) {
131 81         162 $dimap->[$r][$c] = $self->costfn->( $self, $map->[$r][$c] );
132             }
133             }
134             $self->_set_iters(
135 7         32 $self->normfn->( $dimap, $self->min_cost, $self->max_cost ) );
136 7         24 $self->dimap($dimap);
137 7         77 return $self;
138             }
139              
140             sub next {
141 16     16 1 7233 my ( $self, $r, $c, $value ) = @_;
142 16         35 my $dimap = $self->dimap;
143 16 100       63 croak "cannot pathfind on unset map" if !defined $dimap;
144 13         20 my $maxrow = $dimap->$#*;
145 13         26 my $maxcol = $dimap->[0]->$#*;
146 13 50 33     57 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
147 13 50 33     49 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
148 13         21 my @adj;
149 13   100     50 $value //= $dimap->[$r][$c];
150 13 100       36 return \@adj if $value <= $self->min_cost;
151              
152 12         25 for my $i ( -1, 1 ) {
153 24         35 my $x = $c + $i;
154 24 100 66     93 push @adj, [ [ $r, $x ], $dimap->[$r][$x] ] if $x >= 0 and $x <= $maxcol;
155 24         41 for my $j ( -1 .. 1 ) {
156 72         97 $x = $r + $i;
157 72         91 my $y = $c + $j;
158 72 100 66     331 push @adj, [ [ $x, $y ], $dimap->[$x][$y] ]
      100        
      66        
159             if $x >= 0
160             and $x <= $maxrow
161             and $y >= 0
162             and $y <= $maxcol;
163             }
164             }
165 12         25 my $badcost = $self->bad_cost;
166 12 100       23 return [ grep { $_->[1] < $value and $_->[1] != $badcost } @adj ];
  76         307  
167             }
168              
169             sub next_best {
170 15     15 1 683 my ( $self, $r, $c ) = @_;
171 15         33 my $method = $self->next_m;
172             my @ret =
173 15         41 sort { $a->[1] <=> $b->[1] } shuffle $self->$method( $r, $c )->@*;
  2         7  
174 13         88 return $ret[0]->[0];
175             }
176              
177             # next() but only in square directions or "orthogonal" (but diagonals
178             # are orthogonal to one another) or in the "cardinal directions" (NSEW)
179             # but that term also seems unsatisfactory. "4-way" is also used for this
180             # with the assumption of cardinal directions
181             sub next_sq {
182 7     7 1 748 my ( $self, $r, $c, $value ) = @_;
183 7         14 my $dimap = $self->dimap;
184 7 100       25 croak "cannot pathfind on unset map" if !defined $dimap;
185 6         9 my $maxrow = $dimap->$#*;
186 6         11 my $maxcol = $dimap->[0]->$#*;
187 6 50 33     24 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
188 6 50 33     18 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
189 6         11 my @adj;
190 6   66     21 $value //= $dimap->[$r][$c];
191 6 100       19 return \@adj if $value <= $self->min_cost;
192              
193 5 50       12 if ( $c > 0 ) {
194 5         15 push @adj, [ [ $r, $c - 1 ], $dimap->[$r][ $c - 1 ] ];
195             }
196 5 50       10 if ( $c < $maxcol ) {
197 5         12 push @adj, [ [ $r, $c + 1 ], $dimap->[$r][ $c + 1 ] ];
198             }
199 5 50       12 if ( $r > 0 ) {
200 5         12 push @adj, [ [ $r - 1, $c ], $dimap->[ $r - 1 ][$c] ];
201             }
202 5 50       10 if ( $r < $maxrow ) {
203 5         12 push @adj, [ [ $r + 1, $c ], $dimap->[ $r + 1 ][$c] ];
204             }
205              
206 5         11 my $badcost = $self->bad_cost;
207 5 100       9 return [ grep { $_->[1] < $value and $_->[1] != $badcost } @adj ];
  20         71  
208             }
209              
210             sub next_with {
211 1     1 1 4 my ( $self, $r, $c, $param ) = @_;
212 1         4 my $dimap = $self->dimap;
213 1 50       5 croak "cannot pathfind on unset map" if !defined $dimap;
214              
215 1         3 my $badcost = $self->bad_cost;
216              
217 1         3 my $curcost = $dimap->[$r][$c];
218 1 50       5 return undef if $curcost <= $self->min_cost;
219 1   50     4 $curcost *= $param->{my_weight} // 1;
220 1         7 my @here = map $_->values( [ $r, $c ] )->[0], $param->{objs}->@*;
221 1         5 for my $h ( 0 .. $#here ) {
222             # this may cause problems if something is standing on a cell
223             # they can no longer move into but where it is still legal for
224             # them to leave that cell
225 2 50       6 return undef if $here[$h] == $badcost;
226 2   50     6 $curcost += $here[$h] * ( $param->{weights}->[$h] // 0 );
227             }
228              
229 1         14 my $method = $self->next_m;
230 1         7 my $coords = $self->$method( $r, $c, $self->max_cost );
231 1 50       4 return undef unless $coords->@*;
232 1         8 my @costs = map $_->values( map $_->[0], $coords->@* ), $param->{objs}->@*;
233 1         2 my @ret;
234 1         4 COORD: for my $p ( 0 .. $coords->$#* ) {
235 3         7 my @weights;
236 3         7 for my $k ( 0 .. $#costs ) {
237 5 100       13 next COORD if $costs[$k][$p] == $badcost;
238 4   50     12 push @weights, $costs[$k][$p] * ( $param->{weights}->[$k] // 0 );
239             }
240 2   50     11 my $newcost = sum0 $coords->[$p][1] * ( $param->{my_weight} // 1 ), @weights;
241 2 100       8 push @ret, [ $coords->[$p][0], $newcost ] if $newcost < $curcost;
242             }
243              
244 1 50       4 return undef unless @ret;
245 1         6 @ret = sort { $a->[1] <=> $b->[1] } shuffle @ret;
  0         0  
246 1         7 return $ret[0]->[0];
247             }
248              
249             # 4-way "square" normalization as seen in the Brogue article (was called
250             # normalize_costs and used to be a method). one could possibly also
251             # normalize only in the diagonal directions...
252             sub norm_4way {
253 7     7 1 17 my ( $dimap, $mincost, $maxcost, $avfn ) = @_;
254 7   100     36 $avfn //= \&adjacent_values_sq;
255 7         13 my $iters = 0;
256 7         13 my $maxrow = $dimap->$#*;
257 7         13 my $maxcol = $dimap->[0]->$#*;
258 7         10 my $stable;
259 7         10 while (1) {
260 21         37 $stable = 1;
261 21         30 $iters++;
262 21         35 for my $r ( 0 .. $maxrow ) {
263 77         115 for my $c ( 0 .. $maxcol ) {
264 403         516 my $value = $dimap->[$r][$c];
265 403 100       673 next if $value <= $mincost;
266 182         214 my $best = $maxcost;
267 182         284 for my $nv ( $avfn->( $dimap, $r, $c, $maxrow, $maxcol ) ) {
268 532 100 100     1255 $best = $nv if $nv < $best and $nv >= $mincost;
269 532 100       927 last if $best == $mincost;
270             }
271 182 100       350 if ( $value >= $best + 2 ) {
272 34         50 $dimap->[$r][$c] = $best + 1;
273 34         49 $stable = 0;
274             }
275             }
276             }
277 21 100       40 last if $stable;
278             }
279 7         26 return $iters;
280             }
281              
282             # 8-way normalization could either be done with small integers where
283             # diagonals cost the same as square motion (this is non-Euclidean though
284             # traditional in roguelikes) ...
285             sub norm_8way {
286 1     1 1 5 push @_, \&adjacent_values;
287 1         11 &norm_4way; # perldoc perlsub explains this calling form
288             }
289              
290             # ... or one could instead use floating point values to better
291             # approximate diagonals costing sqrt(2) but this is more complicated,
292             # which is perhaps why many roguelikes use 4-way or non-Euclidean 8-way
293             sub norm_8way_euclid {
294 2     2 1 4 my ( $dimap, $mincost, $maxcost ) = @_;
295 2         5 my $iters = 0;
296 2         4 my $maxrow = $dimap->$#*;
297 2         4 my $maxcol = $dimap->[0]->$#*;
298 2         4 my $stable;
299 2         4 while (1) {
300 5         6 $stable = 1;
301 5         6 $iters++;
302 5         10 for my $r ( 0 .. $maxrow ) {
303 16         25 for my $c ( 0 .. $maxcol ) {
304 56         80 my $value = $dimap->[$r][$c];
305 56 100       95 next if $value <= $mincost;
306 47         73 my $best = [ $maxcost, 0 ];
307 47         80 for my $nr (
308             map( [ $_, 1 ], adjacent_values_sq( $dimap, $r, $c, $maxrow, $maxcol ) ),
309             map( [ $_, SQRT2 ], adjacent_values_diag( $dimap, $r, $c, $maxrow, $maxcol ) )
310             ) {
311 186 100 100     516 $best = $nr if $nr->[0] < $best->[0] and $nr->[0] >= $mincost;
312 186 100       319 last if $best->[0] == $mincost;
313             }
314 47 100       137 if ( $value > $best->[0] + SQRT2 ) {
315 17         31 $dimap->[$r][$c] = $best->[0] + $best->[1];
316 17         29 $stable = 0;
317             }
318             }
319             }
320 5 100       13 last if $stable;
321             }
322 2         8 return $iters;
323             }
324              
325             sub path_best {
326 3     3 1 659 my ( $self, $r, $c, $method ) = @_;
327 3         6 my @path;
328 3         10 while ( my $next = $self->next_best( $r, $c, $method ) ) {
329 7         11 push @path, $next;
330 7         18 ( $r, $c ) = @$next;
331             }
332 2         14 return \@path;
333             }
334              
335             sub recalc {
336 3     3 1 2531 my ($self) = @_;
337 3         10 my $dimap = $self->dimap;
338 3 100       19 croak "cannot recalc unset map" if !defined $dimap;
339 2         7 my $maxcost = $self->max_cost;
340 2         5 my $mincost = $self->min_cost;
341 2         4 my $maxcol = $dimap->[0]->$#*;
342 2         7 for my $r ( 0 .. $dimap->$#* ) {
343 7         14 for my $c ( 0 .. $maxcol ) {
344 39 100       68 $dimap->[$r][$c] = $maxcost if $dimap->[$r][$c] > $mincost;
345             }
346             }
347 2         10 $self->_set_iters( $self->normfn->( $dimap, $mincost, $maxcost ) );
348 2         6 $self->dimap($dimap);
349 2         7 return $self;
350             }
351              
352             sub str2map {
353 6     6 1 1148 my ( $self_or_class, $str, $lf ) = @_;
354 6 50       16 croak "no string given" if !defined $str;
355 6   33     35 $lf //= $/;
356 6         9 my @map;
357 6         94 for my $line ( split $lf, $str ) {
358 18         79 push @map, [ split //, $line ];
359             }
360 6         39 return \@map;
361             }
362              
363             sub to_tsv {
364 3     3 1 1512 my ( $self, $ref ) = @_;
365 3 100       12 if ( !defined $ref ) {
366 2         6 $ref = $self->dimap;
367 2 100       15 croak "cannot use an unset map" if !defined $ref;
368             }
369 2         5 my $s = '';
370 2         4 my $cols = $ref->[0]->$#*;
371 2         7 for my $r ( 0 .. $ref->$#* ) {
372 5         10 my $d = "\t";
373 5         7 for my $c ( 0 .. $cols ) {
374 13         31 $s .= $ref->[$r][$c] . $d;
375 13 100       31 $d = '' if $c == $cols - 1;
376             }
377 5         11 $s .= $/;
378             }
379 2         46 return $s;
380             }
381              
382             sub unconnected {
383 3     3 1 974 my ($self) = @_;
384 3         11 my $dimap = $self->dimap;
385 3 100       19 croak "nothing unconnected on unset map" if !defined $dimap;
386 2         5 my @points;
387 2         7 my $maxcost = $self->max_cost;
388 2         6 my $maxcol = $dimap->[0]->$#*;
389 2         8 for my $r ( 0 .. $dimap->$#* ) {
390 7         14 for my $c ( 0 .. $maxcol ) {
391 39 100       102 push @points, [ $r, $c ] if $dimap->[$r][$c] == $maxcost;
392             }
393             }
394 2         16 return \@points;
395             }
396              
397             sub update {
398 2     2 1 687 my $self = shift;
399 2         8 my $dimap = $self->dimap;
400 2 100       16 croak "cannot update unset map" if !defined $dimap;
401 1         2 my $maxrow = $dimap->$#*;
402 1         4 my $maxcol = $dimap->[0]->$#*;
403 1         4 for my $ref (@_) {
404 1         3 my ( $r, $c ) = ( $ref->[0], $ref->[1] );
405 1 50 33     8 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
406 1 50 33     8 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
407 1 50       23 croak "value must be a number" unless looks_like_number $ref->[2];
408 1         6 $dimap->[$r][$c] = int $ref->[2];
409             }
410 1         6 $self->dimap($dimap);
411 1         3 return $self;
412             }
413              
414             sub values {
415 24     24 1 688 my $self = shift;
416 24         40 my $dimap = $self->dimap;
417 24 100       56 croak "cannot get values from unset map" if !defined $dimap;
418 23         31 my @values;
419 23         30 my $maxrow = $dimap->$#*;
420 23         39 my $maxcol = $dimap->[0]->$#*;
421 23         39 for my $point (@_) {
422 29         50 my ( $r, $c ) = ( $point->[0], $point->[1] );
423 29 50 33     85 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
424 29 50 33     107 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
425 29         77 push @values, $dimap->[$r][$c];
426             }
427 23         104 return \@values;
428             }
429              
430             1;
431             __END__