File Coverage

lib/Catan/Map.pm
Criterion Covered Total %
statement 86 154 55.8
branch 14 50 28.0
condition 8 60 13.3
subroutine 19 28 67.8
pod 0 9 0.0
total 127 301 42.1


line stmt bran cond sub pod time code
1             package Catan::Map;
2             $Catan::Map::VERSION = '0.02';
3 3     3   28106 use strict;
  3     1   6  
  3         76  
  1         4  
  1         1  
  1         24  
4 3     3   14 use warnings;
  3     1   6  
  3         72  
  1         4  
  1         2  
  1         27  
5 3     3   1108 use Catan::Map::Tile;
  3     1   6  
  3         79  
  1         4  
  1         8  
  1         28  
6 3     3   1146 use Catan::Map::Intersection;
  3     1   7  
  3         84  
  1         5  
  1         1  
  1         19  
7 3     3   1076 use Catan::Map::Path;
  3     1   7  
  3         94  
  1         4  
  1         2  
  1         25  
8 3     3   17 use List::Util 'shuffle';
  3     1   3  
  3         276  
  1         4  
  1         2  
  1         45  
9 3     3   17 use Math::HexGrid::Hex;
  3     1   5  
  3         6381  
  1         5  
  1         1  
  1         3268  
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 4     4 0 54 my ($class, $args) = @_;
33 4         8 my $self = bless {}, $class;
34              
35             die "$class new requires a type argument (starter, random or custom)\n"
36 4 50       15 unless exists $args->{type};
37              
38             # pick map type
39 4 100       19 if ($args->{type} eq 'starter')
    50          
    50          
40             {
41 1         6 $self->{tiles} = _build_tiles($self->_starter);
42             }
43             elsif ($args->{type} eq 'random')
44             {
45 0         0 $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 3 50       9 unless exists $args->{map};
51 3         7 $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 4         19 $self->{type} = $args->{type};
59 4         14 $self->{intersections} = _build_intersections($self->{tiles});
60 0         0 $self->{paths} = _build_paths($self->{intersections});
61 0         0 return $self;
62             }
63              
64 0     0 0 0 sub type { $_[0]->{type} }
65              
66 0     0 0 0 sub tiles { $_[0]->{tiles} }
67              
68             sub find_tile
69             {
70 0     0 0 0 my ($self, $coordinates) = @_;
71              
72 0 0 0     0 die 'find_tile requires a coordinates arrayref of two integers'
      0        
73             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 2;
74              
75 0         0 my $uuid = "$coordinates->[0],$coordinates->[1]";
76 0   0     0 return $self->{tiles}{$uuid} || die "Tile $uuid not found!";
77             }
78              
79             sub tiles_by_type_code
80             {
81 0     0 0 0 my ($self, $type_code) = @_;
82              
83 0 0 0     0 die "tiles_by_type_code requires a type code letter"
84             unless $type_code && $type_code =~ qr/^[A-Z]$/;
85              
86 0         0 my @tiles;
87 0         0 for my $tile (values %{$self->{tiles}})
  0         0  
88             {
89 0 0       0 push(@tiles, $tile) if $tile->code eq $type_code;
90             }
91 0         0 return \@tiles;
92             }
93              
94             sub tiles_by_resource_number
95             {
96 0     0 0 0 my ($self, $number) = @_;
97              
98 0 0 0     0 die "tiles_by_resource_number requires a resource number"
99             unless defined $number && $number =~ qr/^[0-9]+$/;
100              
101 0         0 my @tiles;
102 0         0 for my $tile (values %{$self->{tiles}})
  0         0  
103             {
104 0 0       0 push(@tiles, $tile) if $tile->number == $number;
105             }
106 0         0 return \@tiles;
107             }
108              
109             sub tiles_by_intersection
110             {
111 0     0 0 0 my ($self, $intersection) = @_;
112              
113 0 0 0     0 die "tiles_by_intersection requires an intersection argument"
114             unless $intersection && ref $intersection eq 'Catan::Map::Intersection';
115              
116 0         0 my @tiles;
117 0         0 for my $tile (values %{$self->{tiles}})
  0         0  
118             {
119 0 0       0 push(@tiles, $tile) if $intersection->is_adjacent($tile);
120             }
121 0         0 return \@tiles;
122             }
123              
124             sub find_intersection
125             {
126 0     0 0 0 my ($self, $coordinates) = @_;
127 0 0 0     0 die 'find_intersection requires an arrayref of 3 coordinates pairs'
      0        
128             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 3;
129              
130 0         0 my @tiles;
131 0         0 for (@$coordinates)
132             {
133 0         0 push @tiles, $self->find_tile($_);
134             }
135 0         0 my $uuid = Catan::Map::Intersection->new(\@tiles)->uuid;
136 0   0     0 return $self->{intersections}{$uuid} || die "Intersection $uuid not found!";
137             }
138              
139             sub find_path
140             {
141 0     0 0 0 my ($self, $coordinates) = @_;
142 0 0 0     0 die 'find_path requires an arrayref of two triples of coordinates pairs'
      0        
143             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 2;
144              
145 0         0 my @intersections;
146 0         0 for (@$coordinates)
147             {
148 0         0 push @intersections, $self->find_intersection($_);
149             }
150 0         0 my $uuid = Catan::Map::Path->new(\@intersections)->uuid;
151 0   0     0 return $self->{paths}{$uuid} || die "Path $uuid not found!";
152             }
153              
154             sub _build_tiles
155             {
156 4     4   8 my ($map_plan) = @_;
157              
158 4         8 my %tiles = ();
159              
160 4 50 33     56 die 'build_tiles requires a arrayref of key/pairs describing the map'
161             unless $map_plan && ref $map_plan eq 'ARRAY';
162              
163 4         22 for (@$map_plan)
164             {
165 148         396 my ($q, $r, $tile_code, $resource_number) = @$_;
166              
167 148 50 66     766 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 148 50       378 ? $tile_types{$tile_code}
174             : die 'Error building tiles, invalid tile type';
175 148         631 my $tile = $tile_class->new($q, $r, $resource_number);
176 148         365 $tiles{$tile->uuid} = $tile;
177             }
178 4         23 return \%tiles;
179             }
180              
181             sub _build_intersections
182             {
183 4     4   9 my $map = shift;
184              
185 4 50 33     141 die '_building_intersections requires a hashref of 37 tiles'
      33        
186             unless $map && ref $map eq 'HASH' && keys %$map == 37;
187              
188 0         0 my %intersections;
189 0         0 my $centre_tile = $map->{"0,0"};
190              
191 0 0       0 die '_building_intersections requires a map with a centre tile'
192             unless $centre_tile;
193              
194 0         0 for my $k (keys %$map)
195             {
196 0         0 my $tile1 = $map->{$k};
197              
198 0         0 for my $d (0..5)
199             {
200 0         0 my $tile2 = $map->{ $tile1->tile_neighbor_uuid($d) };
201 0         0 my $tile3 = $map->{ $tile1->tile_neighbor_uuid($d + 1) };
202              
203             # avoid creating intersections that don't exist in map
204 0 0 0     0 next unless $tile2 && $tile3;
205              
206 0         0 my $i = Catan::Map::Intersection->new([$tile1, $tile2, $tile3]);
207 0         0 $intersections{$i->uuid} = $i;
208             }
209             }
210 0         0 return \%intersections;
211             }
212              
213             sub _build_paths
214             {
215 0     0   0 my $intersections = shift;
216              
217 0 0 0     0 die '_building_paths requires a hashref of 54 intersections'
      0        
218             unless $intersections && ref $intersections eq 'HASH'
219             && keys %$intersections == 54;
220              
221 0         0 my %paths;
222              
223 0         0 for my $i (keys %$intersections)
224             {
225 0         0 for my $j (keys %$intersections)
226             {
227 0         0 my $i1 = $intersections->{$i};
228 0         0 my $i2 = $intersections->{$j};
229              
230             # skip colliding and non-adjacent
231 0 0 0     0 next if $i1 eq $i2 || !$i1->is_adjacent($i2);
232 0         0 my $p = Catan::Map::Path->new([$i1, $i2]);
233 0         0 $paths{$p->uuid} = $p;
234             }
235             }
236 0         0 return \%paths;
237             }
238              
239             sub _starter
240             {
241             return [
242 3     3   124 [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 1     1   9 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 1         12 my @local_resource_numbers = @resource_numbers;
289              
290 1         2 my $type = shift @land_types;
291 1 50       5 my $number = $type eq 'D' ? undef : pop @local_resource_numbers;
292 1         3 my @land_tiles = ([0, 0, $type, $number]);
293              
294 1         4 while (scalar @land_types)
295             {
296 2         4 my $i = 0;
297 2         3 my @new_tiles = ();
298 2         4 for my $tile (@land_tiles)
299             {
300 8         27 my $hex = Math::HexGrid::Hex->new($tile->[0], $tile->[1]);
301              
302 8         77 for my $direction (map { $_ + (2*$hex->hex_length) - $i } reverse 5..10)
  48         232  
303             {
304 48         151 my $neighbor = $hex->hex_neighbor($direction);
305              
306             # skip neighbors we already have
307 48 100 100     1460 next if grep($neighbor->{q} == $_->[0] && $neighbor->{r} == $_->[1],
308             @land_tiles, @new_tiles);
309              
310 18         26 my $type = shift @land_types;
311             # pop the number as we're working inside out instead of outside in
312 18 50       41 my $number = $type eq 'D' ? undef : pop @local_resource_numbers;
313 18         68 push @new_tiles, [$neighbor->{q}, $neighbor->{r}, $type, $number];
314             }
315 8         19 $i++;
316             }
317 2         9 push @land_tiles, @new_tiles;
318             }
319              
320 1         18 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 1         5 push @tiles, @land_tiles;
341 1         5 return \@tiles;
342             }
343             1;
344              
345             __END__