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   186450 use 5.24.0;
  2         16  
9 2     2   11 use warnings;
  2         4  
  2         65  
10              
11 2     2   13 use Carp qw(croak);
  2         4  
  2         118  
12 2     2   13 use List::Util 1.26 qw(shuffle sum0);
  2         33  
  2         167  
13 2     2   1039 use Moo;
  2         22387  
  2         9  
14 2     2   3885 use namespace::clean;
  2         22604  
  2         13  
15 2     2   599 use Scalar::Util qw(looks_like_number);
  2         4  
  2         132  
16              
17             our $VERSION = '1.01';
18              
19 2     2   15 use constant SQRT2 => sqrt(2);
  2         4  
  2         7443  
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 47 my ( $self, $param ) = @_;
43             croak "cannot have both map and str2map arguments"
44 9 50 66     46 if exists $param->{'map'} and exists $param->{'str2map'};
45             $self->map( $param->{'map'} )
46 8 50       16 if exists $param->{'map'};
47             $self->map( $self->str2map( $param->{'str2map'} ) )
48 8 100       34 if exists $param->{'str2map'};
49             }
50              
51             sub adjacent_values {
52 5     5 1 4765 my ( $dimap, $r, $c, $maxrow, $maxcol ) = @_;
53 5         8 my @values;
54 5         12 for my $i ( -1, 1 ) {
55 10         16 my $x = $c + $i;
56 10 100 100     43 push @values, $dimap->[$r][$x] if $x >= 0 and $x <= $maxcol;
57 10         19 for my $j ( -1 .. 1 ) {
58 30         34 $x = $r + $i;
59 30         38 my $y = $c + $j;
60 30 100 100     122 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         20 return @values;
68             }
69              
70             sub adjacent_values_diag {
71 50     50 1 2019 my ( $dimap, $r, $c, $maxrow, $maxcol ) = @_;
72 50         61 my @values;
73 50 100 100     149 push @values, $dimap->[ $r - 1 ][ $c - 1 ] if $r > 0 and $c > 0;
74 50 100 100     135 push @values, $dimap->[ $r - 1 ][ $c + 1 ] if $r > 0 and $c < $maxcol;
75 50 100 100     137 push @values, $dimap->[ $r + 1 ][ $c - 1 ] if $r < $maxrow and $c > 0;
76 50 100 100     146 push @values, $dimap->[ $r + 1 ][ $c + 1 ] if $r < $maxrow and $c < $maxcol;
77 50         190 return @values;
78             }
79              
80             sub adjacent_values_sq {
81 230     230 1 2209 my ( $dimap, $r, $c, $maxrow, $maxcol ) = @_;
82 230         275 my @values;
83 230 100       529 push @values, $dimap->[$r][ $c - 1 ] if $c > 0;
84 230 100       435 push @values, $dimap->[$r][ $c + 1 ] if $c < $maxcol;
85 230 100       404 push @values, $dimap->[ $r - 1 ][$c] if $r > 0;
86 230 100       396 push @values, $dimap->[ $r + 1 ][$c] if $r < $maxrow;
87 230         517 return @values;
88             }
89              
90             sub dimap_with {
91 2     2 1 50 my ( $self, $param ) = @_;
92 2         7 my $dimap = $self->dimap;
93 2 100       16 croak "cannot make new dimap from unset map" if !defined $dimap;
94 1         1 my $new_dimap;
95 1         4 my $badcost = $self->bad_cost;
96 1         3 my $cols = $dimap->[0]->$#*;
97 1         4 for my $r ( 0 .. $dimap->$#* ) {
98 3         8 COL: for my $c ( 0 .. $cols ) {
99 9         11 my $value = $dimap->[$r][$c];
100 9 50       19 if ( $value == $badcost ) {
101 0         0 $new_dimap->[$r][$c] = $badcost;
102 0         0 next COL;
103             }
104 9   50     15 $value *= $param->{my_weight} // 1;
105 9         29 my @here = map $_->values( [ $r, $c ] )->[0], $param->{objs}->@*;
106 9         19 for my $h ( 0 .. $#here ) {
107 17 100       44 if ( $here[$h] == $badcost ) {
108 1         2 $new_dimap->[$r][$c] = $badcost;
109 1         4 next COL;
110             }
111 16   50     39 $value += $here[$h] * ( $param->{weights}->[$h] // 0 );
112             }
113 8         21 $new_dimap->[$r][$c] = $value;
114             }
115             }
116 1         17 return $new_dimap;
117             }
118              
119             sub map {
120 8     8 1 8883 my ( $self, $map ) = @_;
121 8         18 my $dimap = [];
122 8 50 66     76 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         15 my $cols = $map->[0]->@*;
128 7         23 for my $r ( 0 .. $map->$#* ) {
129 21 50       45 croak "unexpected column count at row $r" if $map->[$r]->@* != $cols;
130 21         36 for my $c ( 0 .. $cols - 1 ) {
131 81         146 $dimap->[$r][$c] = $self->costfn->( $self, $map->[$r][$c] );
132             }
133             }
134             $self->_set_iters(
135 7         22 $self->normfn->( $dimap, $self->min_cost, $self->max_cost ) );
136 7         27 $self->dimap($dimap);
137 7         88 return $self;
138             }
139              
140             sub next {
141 16     16 1 7199 my ( $self, $r, $c, $value ) = @_;
142 16         33 my $dimap = $self->dimap;
143 16 100       62 croak "cannot pathfind on unset map" if !defined $dimap;
144 13         21 my $maxrow = $dimap->$#*;
145 13         23 my $maxcol = $dimap->[0]->$#*;
146 13 50 33     56 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
147 13 50 33     44 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
148 13         24 my @adj;
149 13   100     45 $value //= $dimap->[$r][$c];
150 13 100       33 return \@adj if $value <= $self->min_cost;
151              
152 12         23 for my $i ( -1, 1 ) {
153 24         38 my $x = $c + $i;
154 24 100 66     89 push @adj, [ [ $r, $x ], $dimap->[$r][$x] ] if $x >= 0 and $x <= $maxcol;
155 24         45 for my $j ( -1 .. 1 ) {
156 72         93 $x = $r + $i;
157 72         92 my $y = $c + $j;
158 72 100 66     334 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         26 my $badcost = $self->bad_cost;
166 12 100       21 return [ grep { $_->[1] < $value and $_->[1] != $badcost } @adj ];
  76         329  
167             }
168              
169             sub next_best {
170 15     15 1 704 my ( $self, $r, $c ) = @_;
171 15         35 my $method = $self->next_m;
172             my @ret =
173 15         37 sort { $a->[1] <=> $b->[1] } shuffle $self->$method( $r, $c )->@*;
  2         7  
174 13         128 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 762 my ( $self, $r, $c, $value ) = @_;
183 7         15 my $dimap = $self->dimap;
184 7 100       22 croak "cannot pathfind on unset map" if !defined $dimap;
185 6         10 my $maxrow = $dimap->$#*;
186 6         12 my $maxcol = $dimap->[0]->$#*;
187 6 50 33     23 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         9 my @adj;
190 6   66     22 $value //= $dimap->[$r][$c];
191 6 100       16 return \@adj if $value <= $self->min_cost;
192              
193 5 50       10 if ( $c > 0 ) {
194 5         15 push @adj, [ [ $r, $c - 1 ], $dimap->[$r][ $c - 1 ] ];
195             }
196 5 50       12 if ( $c < $maxcol ) {
197 5         13 push @adj, [ [ $r, $c + 1 ], $dimap->[$r][ $c + 1 ] ];
198             }
199 5 50       9 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         11 push @adj, [ [ $r + 1, $c ], $dimap->[ $r + 1 ][$c] ];
204             }
205              
206 5         12 my $badcost = $self->bad_cost;
207 5 100       8 return [ grep { $_->[1] < $value and $_->[1] != $badcost } @adj ];
  20         75  
208             }
209              
210             sub next_with {
211 1     1 1 5 my ( $self, $r, $c, $param ) = @_;
212 1         3 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       4 return undef if $curcost <= $self->min_cost;
219 1   50     4 $curcost *= $param->{my_weight} // 1;
220 1         5 my @here = map $_->values( [ $r, $c ] )->[0], $param->{objs}->@*;
221 1         4 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       16 return undef if $here[$h] == $badcost;
226 2   50     8 $curcost += $here[$h] * ( $param->{weights}->[$h] // 0 );
227             }
228              
229 1         5 my $method = $self->next_m;
230 1         6 my $coords = $self->$method( $r, $c, $self->max_cost );
231 1 50       4 return undef unless $coords->@*;
232 1         5 my @costs = map $_->values( map $_->[0], $coords->@* ), $param->{objs}->@*;
233 1         2 my @ret;
234 1         11 COORD: for my $p ( 0 .. $coords->$#* ) {
235 3         5 my @weights;
236 3         5 for my $k ( 0 .. $#costs ) {
237 5 100       14 next COORD if $costs[$k][$p] == $badcost;
238 4   50     11 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       3 return undef unless @ret;
245 1         4 @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 16 my ( $dimap, $mincost, $maxcost, $avfn ) = @_;
254 7   100     32 $avfn //= \&adjacent_values_sq;
255 7         12 my $iters = 0;
256 7         12 my $maxrow = $dimap->$#*;
257 7         11 my $maxcol = $dimap->[0]->$#*;
258 7         9 my $stable;
259 7         11 while (1) {
260 21         35 $stable = 1;
261 21         30 $iters++;
262 21         34 for my $r ( 0 .. $maxrow ) {
263 77         125 for my $c ( 0 .. $maxcol ) {
264 403         529 my $value = $dimap->[$r][$c];
265 403 100       640 next if $value <= $mincost;
266 182         217 my $best = $maxcost;
267 182         263 for my $nv ( $avfn->( $dimap, $r, $c, $maxrow, $maxcol ) ) {
268 532 100 100     1284 $best = $nv if $nv < $best and $nv >= $mincost;
269 532 100       946 last if $best == $mincost;
270             }
271 182 100       340 if ( $value > $best + 2 ) {
272 34         53 $dimap->[$r][$c] = $best + 1;
273 34         43 $stable = 0;
274             }
275             }
276             }
277 21 100       39 last if $stable;
278             }
279 7         48 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 4 push @_, \&adjacent_values;
287 1         12 &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 5 my ( $dimap, $mincost, $maxcost ) = @_;
295 2         5 my $iters = 0;
296 2         3 my $maxrow = $dimap->$#*;
297 2         4 my $maxcol = $dimap->[0]->$#*;
298 2         4 my $stable;
299 2         3 while (1) {
300 5         8 $stable = 1;
301 5         6 $iters++;
302 5         11 for my $r ( 0 .. $maxrow ) {
303 16         25 for my $c ( 0 .. $maxcol ) {
304 56         80 my $value = $dimap->[$r][$c];
305 56 100       101 next if $value <= $mincost;
306 47         76 my $best = [ $maxcost, 0 ];
307 47         78 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     443 $best = $nr if $nr->[0] < $best->[0] and $nr->[0] >= $mincost;
312 186 100       333 last if $best->[0] == $mincost;
313             }
314             # TODO should this be + 2 like the others or is + SQRT2
315             # a better check?
316 47 100       182 if ( $value > $best->[0] + SQRT2 ) {
317 17         29 $dimap->[$r][$c] = $best->[0] + $best->[1];
318 17         33 $stable = 0;
319             }
320             }
321             }
322 5 100       9 last if $stable;
323             }
324 2         7 return $iters;
325             }
326              
327             sub path_best {
328 3     3 1 715 my ( $self, $r, $c, $method ) = @_;
329 3         6 my @path;
330 3         9 while ( my $next = $self->next_best( $r, $c, $method ) ) {
331 7         13 push @path, $next;
332 7         17 ( $r, $c ) = @$next;
333             }
334 2         24 return \@path;
335             }
336              
337             sub recalc {
338 3     3 1 2464 my ($self) = @_;
339 3         9 my $dimap = $self->dimap;
340 3 100       16 croak "cannot recalc unset map" if !defined $dimap;
341 2         6 my $maxcost = $self->max_cost;
342 2         4 my $mincost = $self->min_cost;
343 2         5 my $maxcol = $dimap->[0]->$#*;
344 2         6 for my $r ( 0 .. $dimap->$#* ) {
345 7         14 for my $c ( 0 .. $maxcol ) {
346 39 100       70 $dimap->[$r][$c] = $maxcost if $dimap->[$r][$c] > $mincost;
347             }
348             }
349 2         6 $self->_set_iters( $self->normfn->( $dimap, $mincost, $maxcost ) );
350 2         16 $self->dimap($dimap);
351 2         8 return $self;
352             }
353              
354             sub str2map {
355 6     6 1 862 my ( $self_or_class, $str, $lf ) = @_;
356 6 50       16 croak "no string given" if !defined $str;
357 6   33     32 $lf //= $/;
358 6         9 my @map;
359 6         85 for my $line ( split $lf, $str ) {
360 18         78 push @map, [ split //, $line ];
361             }
362 6         24 return \@map;
363             }
364              
365             sub to_tsv {
366 3     3 1 1561 my ( $self, $ref ) = @_;
367 3 100       10 if ( !defined $ref ) {
368 2         7 $ref = $self->dimap;
369 2 100       12 croak "cannot use an unset map" if !defined $ref;
370             }
371 2         5 my $s = '';
372 2         4 my $cols = $ref->[0]->$#*;
373 2         7 for my $r ( 0 .. $ref->$#* ) {
374 5         9 my $d = "\t";
375 5         9 for my $c ( 0 .. $cols ) {
376 13         24 $s .= $ref->[$r][$c] . $d;
377 13 100       29 $d = '' if $c == $cols - 1;
378             }
379 5         10 $s .= $/;
380             }
381 2         16 return $s;
382             }
383              
384             sub unconnected {
385 3     3 1 975 my ($self) = @_;
386 3         9 my $dimap = $self->dimap;
387 3 100       17 croak "nothing unconnected on unset map" if !defined $dimap;
388 2         3 my @points;
389 2         5 my $maxcost = $self->max_cost;
390 2         5 my $maxcol = $dimap->[0]->$#*;
391 2         7 for my $r ( 0 .. $dimap->$#* ) {
392 7         12 for my $c ( 0 .. $maxcol ) {
393 39 100       75 push @points, [ $r, $c ] if $dimap->[$r][$c] == $maxcost;
394             }
395             }
396 2         17 return \@points;
397             }
398              
399             sub update {
400 2     2 1 710 my $self = shift;
401 2         7 my $dimap = $self->dimap;
402 2 100       15 croak "cannot update unset map" if !defined $dimap;
403 1         2 my $maxrow = $dimap->$#*;
404 1         3 my $maxcol = $dimap->[0]->$#*;
405 1         3 for my $ref (@_) {
406 1         3 my ( $r, $c ) = ( $ref->[0], $ref->[1] );
407 1 50 33     8 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
408 1 50 33     9 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
409 1 50       23 croak "value must be a number" unless looks_like_number $ref->[2];
410 1         5 $dimap->[$r][$c] = int $ref->[2];
411             }
412 1         5 $self->dimap($dimap);
413 1         3 return $self;
414             }
415              
416             sub values {
417 24     24 1 692 my $self = shift;
418 24         46 my $dimap = $self->dimap;
419 24 100       53 croak "cannot get values from unset map" if !defined $dimap;
420 23         32 my @values;
421 23         32 my $maxrow = $dimap->$#*;
422 23         34 my $maxcol = $dimap->[0]->$#*;
423 23         39 for my $point (@_) {
424 29         51 my ( $r, $c ) = ( $point->[0], $point->[1] );
425 29 50 33     101 croak "row $r out of bounds" if $r > $maxrow or $r < 0;
426 29 50 33     105 croak "col $c out of bounds" if $c > $maxcol or $c < 0;
427 29         63 push @values, $dimap->[$r][$c];
428             }
429 23         114 return \@values;
430             }
431              
432             1;
433             __END__