File Coverage

lib/Catan/Map.pm
Criterion Covered Total %
statement 152 154 98.7
branch 35 50 70.0
condition 28 60 46.6
subroutine 27 28 96.4
pod 0 9 0.0
total 242 301 80.4


line stmt bran cond sub pod time code
1             package Catan::Map;
2             $Catan::Map::VERSION = '0.03';
3 3     3   29618 use strict;
  3     1   6  
  3         78  
  1         4  
  1         3  
  1         36  
4 3     3   16 use warnings;
  3     1   6  
  3         73  
  1         5  
  1         2  
  1         25  
5 3     3   1172 use Catan::Map::Tile;
  3     1   7  
  3         75  
  1         5  
  1         2  
  1         29  
6 3     3   1149 use Catan::Map::Intersection;
  3     1   7  
  3         79  
  1         5  
  1         1  
  1         19  
7 3     3   1028 use Catan::Map::Path;
  3     1   8  
  3         99  
  1         6  
  1         6  
  1         22  
8 3     3   19 use List::Util 'shuffle';
  3     1   5  
  3         442  
  1         5  
  1         3  
  1         60  
9 3     3   18 use Math::HexGrid::Hex 0.03;
  3     1   106  
  3         6940  
  1         5  
  1         36  
  1         2589  
10              
11             my @resource_numbers = (5,2,6,3,8,10,9,12,11,4,8,10,9,4,5,6,3,11);
12              
13             my %tile_types = (
14             D => 'Catan::Map::Tile::Desert',
15             F => 'Catan::Map::Tile::Fields',
16             FO => 'Catan::Map::Tile::Forest',
17             HR => 'Catan::Map::Tile::Harbor',
18             HRB => 'Catan::Map::Tile::HarborBrick',
19             HRG => 'Catan::Map::Tile::HarborGrain',
20             HRL => 'Catan::Map::Tile::HarborLumber',
21             HRO => 'Catan::Map::Tile::HarborOre',
22             HRW => 'Catan::Map::Tile::HarborWool',
23             H => 'Catan::Map::Tile::Hills',
24             M => 'Catan::Map::Tile::Mountains',
25             P => 'Catan::Map::Tile::Pastures',
26             S => 'Catan::Map::Tile::Sea',
27             );
28             eval "require $_" for values %tile_types;
29              
30             sub new
31             {
32 7     7 0 90 my ($class, $args) = @_;
33 7         19 my $self = bless {}, $class;
34              
35             die "$class new requires a type argument (starter, random or custom)\n"
36 7 50       29 unless exists $args->{type};
37              
38             # pick map type
39 7 100       49 if ($args->{type} eq 'starter')
    100          
    50          
40             {
41 1         5 $self->{tiles} = _build_tiles($self->_starter);
42             }
43             elsif ($args->{type} eq 'random')
44             {
45 1         8 $self->{tiles} = _build_tiles($self->_random);
46             }
47             elsif ($args->{type} eq 'custom')
48             {
49             die "A custom map requires a map argument; an arrayref defining the map\n"
50 5 100       30 unless exists $args->{map};
51 4         18 $self->{tiles} = _build_tiles($args->{map});
52             }
53             else
54             {
55 0         0 die "$class->new requires a type argument (starter, random or custom)\n";
56             }
57              
58 6         33 $self->{type} = $args->{type};
59 6         19 $self->{intersections} = _build_intersections($self->{tiles});
60 6         26 $self->{paths} = _build_paths($self->{intersections});
61 6         59 return $self;
62             }
63              
64 0     0 0 0 sub type { $_[0]->{type} }
65              
66 2     2 0 15 sub tiles { $_[0]->{tiles} }
67              
68             sub find_tile
69             {
70 256     256 0 403 my ($self, $coordinates) = @_;
71              
72 256 50 33     1952 die 'find_tile requires a coordinates arrayref of two integers'
      33        
73             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 2;
74              
75 256         704 my $uuid = "$coordinates->[0],$coordinates->[1]";
76 256   50     1171 return $self->{tiles}{$uuid} || die "Tile $uuid not found!";
77             }
78              
79             sub tiles_by_type_code
80             {
81 2     2 0 6 my ($self, $type_code) = @_;
82              
83 2 50 33     42 die "tiles_by_type_code requires a type code letter"
84             unless $type_code && $type_code =~ qr/^[A-Z]$/;
85              
86 2         8 my @tiles;
87 2         5 for my $tile (values %{$self->{tiles}})
  2         16  
88             {
89 74 100       254 push(@tiles, $tile) if $tile->code eq $type_code;
90             }
91 2         11 return \@tiles;
92             }
93              
94             sub tiles_by_resource_number
95             {
96 54     54 0 94 my ($self, $number) = @_;
97              
98 54 50 33     746 die "tiles_by_resource_number requires a resource number"
99             unless defined $number && $number =~ qr/^[0-9]+$/;
100              
101 54         138 my @tiles;
102 54         76 for my $tile (values %{$self->{tiles}})
  54         364  
103             {
104 1998 100       4732 push(@tiles, $tile) if $tile->number == $number;
105             }
106 54         167 return \@tiles;
107             }
108              
109             sub tiles_by_intersection
110             {
111 33     33 0 58 my ($self, $intersection) = @_;
112              
113 33 50 33     85 die "tiles_by_intersection requires an intersection argument"
114             unless $intersection && ref $intersection eq 'Catan::Map::Intersection';
115              
116 33         56 my @tiles;
117 33         45 for my $tile (values %{$self->{tiles}})
  33         164  
118             {
119 1221 100       2927 push(@tiles, $tile) if $intersection->is_adjacent($tile);
120             }
121 33         250 return \@tiles;
122             }
123              
124             sub find_intersection
125             {
126 82     82 0 183 my ($self, $coordinates) = @_;
127 82 50 33     675 die 'find_intersection requires an arrayref of 3 coordinates pairs'
      33        
128             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 3;
129              
130 82         110 my @tiles;
131 82         187 for (@$coordinates)
132             {
133 246         578 push @tiles, $self->find_tile($_);
134             }
135 82         318 my $uuid = Catan::Map::Intersection->new(\@tiles)->uuid;
136 82   50     444 return $self->{intersections}{$uuid} || die "Intersection $uuid not found!";
137             }
138              
139             sub find_path
140             {
141 31     31 0 77 my ($self, $coordinates) = @_;
142 31 50 33     286 die 'find_path requires an arrayref of two triples of coordinates pairs'
      33        
143             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 2;
144              
145 31         44 my @intersections;
146 31         90 for (@$coordinates)
147             {
148 62         167 push @intersections, $self->find_intersection($_);
149             }
150 31         174 my $uuid = Catan::Map::Path->new(\@intersections)->uuid;
151 31   50     173 return $self->{paths}{$uuid} || die "Path $uuid not found!";
152             }
153              
154             sub _build_tiles
155             {
156 6     6   14 my ($map_plan) = @_;
157              
158 6         14 my %tiles = ();
159              
160 6 50 33     41 die 'build_tiles requires a arrayref of key/pairs describing the map'
161             unless $map_plan && ref $map_plan eq 'ARRAY';
162              
163 6         21 for (@$map_plan)
164             {
165 222         512 my ($q, $r, $tile_code, $resource_number) = @$_;
166              
167 222 50 66     1032 die 'Error building tiles, invalid resource number'
168             unless !defined $resource_number # undef is valid
169             || grep $resource_number == $_, @resource_numbers;
170              
171              
172             my $tile_class = exists $tile_types{$tile_code}
173 222 50       535 ? $tile_types{$tile_code}
174             : die 'Error building tiles, invalid tile type';
175 222         877 my $tile = $tile_class->new($q, $r, $resource_number);
176 222         513 $tiles{$tile} = $tile;
177             }
178 6         25 return \%tiles;
179             }
180              
181             sub _build_intersections
182             {
183 6     6   12 my $map = shift;
184              
185 6 50 33     76 die '_building_intersections requires a hashref of 37 tiles'
      33        
186             unless $map && ref $map eq 'HASH' && keys %$map == 37;
187              
188 6         11 my %intersections;
189 6         10 my $centre_tile = $map->{"0,0"};
190              
191 6 50       20 die '_building_intersections requires a map with a centre tile'
192             unless $centre_tile;
193              
194 6         40 for my $k (keys %$map)
195             {
196 222         352 my $tile1 = $map->{$k};
197              
198 222         405 for my $d (0..5)
199             {
200 1332         3365 my $tile2 = $map->{ $tile1->tile_neighbor_uuid($d) };
201 1332         27306 my $tile3 = $map->{ $tile1->tile_neighbor_uuid($d + 1) };
202              
203             # avoid creating intersections that don't exist in map
204 1332 100 100     25974 next unless $tile2 && $tile3;
205              
206 972         3531 my $i = Catan::Map::Intersection->new([$tile1, $tile2, $tile3]);
207 972         7480 $intersections{$i->uuid} = $i;
208             }
209             }
210 6         40 return \%intersections;
211             }
212              
213             sub _build_paths
214             {
215 6     6   12 my $intersections = shift;
216              
217 6 50 33     86 die '_building_paths requires a hashref of 54 intersections'
      33        
218             unless $intersections && ref $intersections eq 'HASH'
219             && keys %$intersections == 54;
220              
221 6         11 my %paths;
222              
223 6         50 for my $i (keys %$intersections)
224             {
225 324         2243 for my $j (keys %$intersections)
226             {
227 17496         25097 my $i1 = $intersections->{$i};
228 17496         22402 my $i2 = $intersections->{$j};
229              
230             # skip colliding and non-adjacent
231 17496 100 100     39374 next if $i1 eq $i2 || !$i1->is_adjacent($i2);
232 864         3041 my $p = Catan::Map::Path->new([$i1, $i2]);
233 864         2807 $paths{$p->uuid} = $p;
234             }
235             }
236 6         57 return \%paths;
237             }
238              
239             sub _starter
240             {
241             return [
242 4     4   151 [0,-3,"HR",undef],
243             [1,-3,"S",undef],
244             [2,-3,"HRW",undef],
245             [3,-3,"S",undef],
246             [3,-2,"HR",undef],
247             [3,-1,"S",undef],
248             [3,0,"HRG",undef],
249             [2,1,"S",undef],
250             [1,2,"HRB",undef],
251             [0,3,"S",undef],
252             [-1,3,"HRL",undef],
253             [-2,3,"S",undef],
254             [-3,3,"HR",undef],
255             [-3,2,"S",undef],
256             [-3,1,"HR",undef],
257             [-3,0,"S",undef],
258             [-2,-1,"HRO",undef],
259             [-1,-2,"S",undef],
260             [0,-2,"FO",11],
261             [1,-2,"P",12],
262             [2,-2,"F",9],
263             [2,-1,"P",10],
264             [2,0,"F",8],
265             [1,1,"M",3],
266             [0,2,"FO",6],
267             [-1,2,"F",2],
268             [-2,2,"M",5],
269             [-2,1,"H",8],
270             [-2,0,"D",undef],
271             [-1,-1,"H",4],
272             [0,-1,"M",6],
273             [1,-1,"H",5],
274             [1,0,"FO",4],
275             [0,1,"P",9],
276             [-1,1,"P",10],
277             [-1,0,"FO",3],
278             [0,0,"F",11]
279             ];
280             }
281              
282             sub _random
283             {
284             # only the land tiles are random
285 2     2   110 my @land_types = shuffle qw/H H H D F F F F FO FO FO FO P P P P M M M/;
286              
287             # make a local copy so we can mutate it
288 2         11 my @local_resource_numbers = @resource_numbers;
289              
290 2         5 my $type = shift @land_types;
291 2 50       8 my $number = $type eq 'D' ? undef : pop @local_resource_numbers;
292 2         9 my @land_tiles = ([0, 0, $type, $number]);
293              
294 2         9 while (scalar @land_types)
295             {
296 4         8 my $i = 0;
297 4         7 my @new_tiles = ();
298 4         10 for my $tile (@land_tiles)
299             {
300 16         57 my $hex = Math::HexGrid::Hex->new($tile->[0], $tile->[1]);
301              
302 16         189 for my $direction (map { $_ + (2*$hex->hex_length) - $i } reverse 5..10)
  96         487  
303             {
304 96         300 my $neighbor = $hex->hex_neighbor($direction);
305              
306             # skip neighbors we already have
307 96 100 100     3230 next if grep($neighbor->{q} == $_->[0] && $neighbor->{r} == $_->[1],
308             @land_tiles, @new_tiles);
309              
310 36         52 my $type = shift @land_types;
311             # pop the number as we're working inside out instead of outside in
312 36 100       77 my $number = $type eq 'D' ? undef : pop @local_resource_numbers;
313 36         171 push @new_tiles, [$neighbor->{q}, $neighbor->{r}, $type, $number];
314             }
315 16         45 $i++;
316             }
317 4         20 push @land_tiles, @new_tiles;
318             }
319              
320 2         30 my @tiles = (
321             [0,-3,"HR",undef],
322             [1,-3,"S",undef],
323             [2,-3,"HRW",undef],
324             [3,-3,"S",undef],
325             [3,-2,"HR",undef],
326             [3,-1,"S",undef],
327             [3,0,"HRG",undef],
328             [2,1,"S",undef],
329             [1,2,"HRB",undef],
330             [0,3,"S",undef],
331             [-1,3,"HRL",undef],
332             [-2,3,"S",undef],
333             [-3,3,"HR",undef],
334             [-3,2,"S",undef],
335             [-3,1,"HR",undef],
336             [-3,0,"S",undef],
337             [-2,-1,"HRO",undef],
338             [-1,-2,"S",undef],
339             );
340 2         8 push @tiles, @land_tiles;
341 2         13 return \@tiles;
342             }
343             1;
344              
345             __END__