File Coverage

blib/lib/Game/DijkstraMap.pm
Criterion Covered Total %
statement 311 316 98.4
branch 124 152 81.5
condition 72 107 67.2
subroutine 31 31 100.0
pod 22 23 95.6
total 560 629 89.0


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