File Coverage

blib/lib/Game/DijkstraMap.pm
Criterion Covered Total %
statement 311 316 98.4
branch 124 152 81.5
condition 74 107 69.1
subroutine 31 31 100.0
pod 22 23 95.6
total 562 629 89.3


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