File Coverage

lib/Settlers/Map.pm
Criterion Covered Total %
statement 168 170 98.8
branch 37 54 68.5
condition 31 66 46.9
subroutine 30 31 96.7
pod 0 12 0.0
total 266 333 79.8


line stmt bran cond sub pod time code
1             package Settlers::Map;
2             $Settlers::Map::VERSION = '0.07';
3 3     3   27607 use strict;
  3     1   6  
  3         77  
  1         5  
  1         1  
  1         20  
4 3     3   15 use warnings;
  3     1   5  
  3         74  
  1         5  
  1         1  
  1         23  
5 3     3   1058 use Settlers::Map::Tile;
  3     1   7  
  3         69  
  1         5  
  1         2  
  1         24  
6 3     3   1084 use Settlers::Map::Intersection;
  3     1   9  
  3         91  
  1         5  
  1         2  
  1         18  
7 3     3   1041 use Settlers::Map::Path;
  3     1   8  
  3         94  
  1         5  
  1         1  
  1         23  
8 3     3   16 use List::Util 'shuffle';
  3     1   5  
  3         316  
  1         80  
  1         2  
  1         50  
9 3     3   16 use Math::HexGrid::Hex 0.03;
  3     1   86  
  3         8043  
  1         5  
  1         26  
  1         3799  
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 => 'Settlers::Map::Tile::Desert',
15             F => 'Settlers::Map::Tile::Fields',
16             FO => 'Settlers::Map::Tile::Forest',
17             H => 'Settlers::Map::Tile::Hills',
18             M => 'Settlers::Map::Tile::Mountains',
19             P => 'Settlers::Map::Tile::Pastures',
20             S => 'Settlers::Map::Tile::Sea',
21             );
22             eval "require $_" for values %tile_types;
23              
24             sub new
25             {
26 7     7 0 163 my ($class, $args) = @_;
27 7         21 my $self = bless {}, $class;
28              
29             die "$class new requires a type argument (starter, random or custom)\n"
30 7 50       27 unless exists $args->{type};
31              
32             # pick map type
33 7 100       47 if ($args->{type} eq 'starter')
    100          
    50          
34             {
35 2         17 $self->{schema} = $self->_starter;
36 2         17 $self->{tiles} = _build_tiles($self->{schema}{tiles});
37             }
38             elsif ($args->{type} eq 'random')
39             {
40 2         10 $self->{schema} = $self->_random;
41 2         11 $self->{tiles} = _build_tiles($self->{schema}{tiles});
42             }
43             elsif ($args->{type} eq 'custom')
44             {
45             die "A custom map requires a schema containing the tiles and harbor data (see https://github.com/dnmfarrell/Settlers-Game-Notation)\n"
46 3 100 66     34 unless exists $args->{schema}{tiles} && exists $args->{schema}{harbors};
47 2         6 $self->{schema} = $args->{schema};
48 2         10 $self->{tiles} = _build_tiles($args->{schema}{tiles});
49             }
50             else
51             {
52 0         0 die "$class->new requires a type argument (starter, random or custom)\n";
53             }
54              
55 6         18 $self->{type} = $args->{type};
56 6         20 $self->{intersections} = _build_intersections($self->{tiles});
57 6         24 $self->{paths} = _build_paths($self->{intersections});
58 6         36 $self->{harbors} = $self->build_harbors($self->{schema}{harbors});
59 6         48 return $self;
60             }
61              
62 5     5 0 66 sub schema { $_[0]->{schema} }
63 15     15 0 110 sub harbors { $_[0]->{harbors} }
64 0     0 0 0 sub type { $_[0]->{type} }
65 1     1 0 8 sub tiles { $_[0]->{tiles} }
66              
67             sub find_tile
68             {
69 580     580 0 834 my ($self, $coordinates) = @_;
70              
71 580 50 33     3820 die 'find_tile requires a coordinates arrayref of two integers'
      33        
72             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 2;
73              
74 580         1475 my $uuid = "$coordinates->[0],$coordinates->[1]";
75 580   50     1810 return $self->{tiles}{$uuid} || die "Tile $uuid not found!";
76             }
77              
78             sub tiles_by_type_code
79             {
80 2     2 0 5 my ($self, $type_code) = @_;
81              
82 2 50 33     33 die "tiles_by_type_code requires a type code letter"
83             unless $type_code && $type_code =~ qr/^[A-Z]$/;
84              
85 2         8 my @tiles;
86 2         5 for my $tile (values %{$self->{tiles}})
  2         17  
87             {
88 74 100       212 push(@tiles, $tile) if $tile->code eq $type_code;
89             }
90 2         11 return \@tiles;
91             }
92              
93             sub tiles_by_resource_number
94             {
95 54     54 0 102 my ($self, $number) = @_;
96              
97 54 50 33     776 die "tiles_by_resource_number requires a resource number"
98             unless defined $number && $number =~ qr/^[0-9]+$/;
99              
100 54         138 my @tiles;
101 54         91 for my $tile (values %{$self->{tiles}})
  54         375  
102             {
103 1998 100       4815 push(@tiles, $tile) if $tile->number == $number;
104             }
105 54         184 return \@tiles;
106             }
107              
108             sub tiles_by_intersection
109             {
110 5     5 0 10 my ($self, $intersection) = @_;
111              
112 5 50 33     15 die "tiles_by_intersection requires an intersection argument"
113             unless $intersection && ref $intersection eq 'Settlers::Map::Intersection';
114              
115 5         10 my @tiles;
116 5         9 for my $tile (values %{$self->{tiles}})
  5         26  
117             {
118 185 100       447 push(@tiles, $tile) if $intersection->is_adjacent($tile);
119             }
120 5         21 return \@tiles;
121             }
122              
123             sub find_intersection
124             {
125 190     190 0 286 my ($self, $coordinates) = @_;
126 190 50 33     1325 die 'find_intersection requires an arrayref of 3 coordinates pairs'
      33        
127             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 3;
128              
129 190         224 my @tiles;
130 190         363 for (@$coordinates)
131             {
132 570         1228 push @tiles, $self->find_tile($_);
133             }
134 190         582 my $uuid = Settlers::Map::Intersection->new(\@tiles)->uuid;
135 190   50     866 return $self->{intersections}{$uuid} || die "Intersection $uuid not found!";
136             }
137              
138             sub find_path
139             {
140 85     85 0 177 my ($self, $coordinates) = @_;
141 85 50 33     689 die 'find_path requires an arrayref of two triples of coordinates pairs'
      33        
142             unless $coordinates && ref $coordinates eq 'ARRAY' && @$coordinates == 2;
143              
144 85         103 my @intersections;
145 85         194 for (@$coordinates)
146             {
147 170         382 push @intersections, $self->find_intersection($_);
148             }
149 85         294 my $uuid = Settlers::Map::Path->new(\@intersections)->uuid;
150 85   50     394 return $self->{paths}{$uuid} || die "Path $uuid not found!";
151             }
152              
153             sub _build_tiles
154             {
155 6     6   13 my ($map_plan) = @_;
156              
157 6         16 my %tiles = ();
158              
159 6 50 33     57 die 'build_tiles requires a arrayref of key/pairs describing the map'
160             unless $map_plan && ref $map_plan eq 'ARRAY';
161              
162 6         19 for (@$map_plan)
163             {
164 222         553 my ($q, $r, $tile_code, $resource_number) = @$_;
165              
166 222 50 66     1102 die 'Error building tiles, invalid resource number'
167             unless !defined $resource_number # undef is valid
168             || grep $resource_number == $_, @resource_numbers;
169              
170              
171             my $tile_class = exists $tile_types{$tile_code}
172 222 50       538 ? $tile_types{$tile_code}
173             : die 'Error building tiles, invalid tile type';
174 222         792 my $tile = $tile_class->new($q, $r, $resource_number);
175 222         510 $tiles{$tile} = $tile;
176             }
177 6         19 return \%tiles;
178             }
179              
180             sub _build_intersections
181             {
182 6     6   10 my $map = shift;
183              
184 6 50 33     78 die '_building_intersections requires a hashref of 37 tiles'
      33        
185             unless $map && ref $map eq 'HASH' && keys %$map == 37;
186              
187 6         14 my %intersections;
188 6         13 my $centre_tile = $map->{"0,0"};
189              
190 6 50       18 die '_building_intersections requires a map with a centre tile'
191             unless $centre_tile;
192              
193 6         55 for my $k (keys %$map)
194             {
195 222         366 my $tile1 = $map->{$k};
196              
197 222         493 for my $d (0..5)
198             {
199 1332         3425 my $tile2 = $map->{ $tile1->tile_neighbor_uuid($d) };
200 1332         28340 my $tile3 = $map->{ $tile1->tile_neighbor_uuid($d + 1) };
201              
202             # avoid creating intersections that don't exist in map
203 1332 100 100     26928 next unless $tile2 && $tile3;
204              
205 972         3733 my $i = Settlers::Map::Intersection->new([$tile1, $tile2, $tile3]);
206 972         8086 $intersections{$i->uuid} = $i;
207             }
208             }
209 6         43 return \%intersections;
210             }
211              
212             sub _build_paths
213             {
214 6     6   15 my $intersections = shift;
215              
216 6 50 33     82 die '_building_paths requires a hashref of 54 intersections'
      33        
217             unless $intersections && ref $intersections eq 'HASH'
218             && keys %$intersections == 54;
219              
220 6         12 my %paths;
221              
222 6         53 for my $i (keys %$intersections)
223             {
224 324         2283 for my $j (keys %$intersections)
225             {
226 17496         26206 my $i1 = $intersections->{$i};
227 17496         23667 my $i2 = $intersections->{$j};
228              
229             # skip colliding and non-adjacent
230 17496 100 100     40255 next if $i1 eq $i2 || !$i1->is_adjacent($i2);
231 864         3161 my $p = Settlers::Map::Path->new([$i1, $i2]);
232 864         2812 $paths{$p->uuid} = $p;
233             }
234             }
235 6         47 return \%paths;
236             }
237              
238             my %harbor_types = (
239             HR => 'Generic Harbor',
240             HRB => 'Brick Harbor',
241             HRG => 'Grain Harbor',
242             HRL => 'Lumber Harbor',
243             HRO => 'Ore Harbor',
244             HRW => 'Wool Harbor',
245             );
246              
247             sub build_harbors
248             {
249 6     6 0 14 my ($self, $harbors) = @_;
250 6 50 33     43 die 'build_harbors requires an arrayref of arrays of paths and harbor types'
251             unless $harbors && ref $harbors eq 'ARRAY';
252              
253 6         19 my @harbors = ();
254 6         17 for my $pair (@$harbors)
255             {
256 54         136 my ($coordinates, $code) = @$pair;
257 54         128 my $path = $self->find_path($coordinates);
258 54 50       148 die "invalid harbor code!\n" unless exists $harbor_types{$code};
259 54         227 push @harbors, { code => $code, location => $path };
260             }
261 6         23 return \@harbors;
262             }
263              
264             sub _starter
265             {
266             return {
267 4     4   379 tiles => [
268             [0,-3,"S",undef],
269             [1,-3,"S",undef],
270             [2,-3,"S",undef],
271             [3,-3,"S",undef],
272             [3,-2,"S",undef],
273             [3,-1,"S",undef],
274             [3,0,"S",undef],
275             [2,1,"S",undef],
276             [1,2,"S",undef],
277             [0,3,"S",undef],
278             [-1,3,"S",undef],
279             [-2,3,"S",undef],
280             [-3,3,"S",undef],
281             [-3,2,"S",undef],
282             [-3,1,"S",undef],
283             [-3,0,"S",undef],
284             [-2,-1,"S",undef],
285             [-1,-2,"S",undef],
286             [0,-2,"FO",11],
287             [1,-2,"P",12],
288             [2,-2,"F",9],
289             [2,-1,"P",10],
290             [2,0,"F",8],
291             [1,1,"M",3],
292             [0,2,"FO",6],
293             [-1,2,"F",2],
294             [-2,2,"M",5],
295             [-2,1,"H",8],
296             [-2,0,"D",undef],
297             [-1,-1,"H",4],
298             [0,-1,"M",6],
299             [1,-1,"H",5],
300             [1,0,"FO",4],
301             [0,1,"P",9],
302             [-1,1,"P",10],
303             [-1,0,"FO",3],
304             [0,0,"F",11]
305             ],
306             harbors => [
307             [ [[[0,-3],[0,-2],[-1,-2]], [[1,-3],[0,-2],[0,-3]]], "HR"],
308             [ [[[2,-3],[1,-2],[1,-3]], [[2,-3],[2,-2],[1,-2]]], "HRW"],
309             [ [[[3,-2],[2,-1],[2,-2]], [[3,-2],[3,-1],[2,-1]]], "HR"],
310             [ [[[3,-1],[3,0],[2,0]], [[3,0],[2,1],[2,0]]], "HR"],
311             [ [[[2,1],[1,2],[1,1]], [[1,1],[1,2],[0,2]]], "HRB"],
312             [ [[[0,2],[-1,3],[-1,2]], [[-1,2],[-1,3],[-2,3]]], "HRL"],
313             [ [[[-2,2],[-2,3],[-3,3]], [[-2,2],[-3,3],[-3,2]]], "HR"],
314             [ [[[-2,1],[-3,2],[-3,1]], [[-2,0],[-2,1],[-3,1]]], "HRG"],
315             [ [[[-1,-1],[-2,0],[-2,-1]], [[-1,-2],[-1,-1],[-2,-1]]],"HRO"]
316             ]
317             };
318             }
319              
320             sub _random
321             {
322             # only the land tiles are random
323 2     2   122 my @land_types = shuffle qw/H H H D F F F F FO FO FO FO P P P P M M M/;
324              
325             # make a local copy so we can mutate it
326 2         9 my @local_resource_numbers = @resource_numbers;
327              
328 2         4 my $type = shift @land_types;
329 2 50       8 my $number = $type eq 'D' ? undef : pop @local_resource_numbers;
330 2         7 my @land_tiles = ([0, 0, $type, $number]);
331              
332 2         15 while (scalar @land_types)
333             {
334 4         7 my $i = 0;
335 4         8 my @new_tiles = ();
336 4         7 for my $tile (@land_tiles)
337             {
338 16         59 my $hex = Math::HexGrid::Hex->new($tile->[0], $tile->[1]);
339              
340 16         181 for my $direction (map { $_ + (2*$hex->hex_length) - $i } reverse 5..10)
  96         573  
341             {
342 96         318 my $neighbor = $hex->hex_neighbor($direction);
343              
344             # skip neighbors we already have
345 96 100 100     3397 next if grep($neighbor->{q} == $_->[0] && $neighbor->{r} == $_->[1],
346             @land_tiles, @new_tiles);
347              
348 36         54 my $type = shift @land_types;
349             # pop the number as we're working inside out instead of outside in
350 36 100       83 my $number = $type eq 'D' ? undef : pop @local_resource_numbers;
351 36         178 push @new_tiles, [$neighbor->{q}, $neighbor->{r}, $type, $number];
352             }
353 16         48 $i++;
354             }
355 4         20 push @land_tiles, @new_tiles;
356             }
357              
358 2         48 my @tiles = (
359             [0,-3,"S",undef],
360             [1,-3,"S",undef],
361             [2,-3,"S",undef],
362             [3,-3,"S",undef],
363             [3,-2,"S",undef],
364             [3,-1,"S",undef],
365             [3,0,"S",undef],
366             [2,1,"S",undef],
367             [1,2,"S",undef],
368             [0,3,"S",undef],
369             [-1,3,"S",undef],
370             [-2,3,"S",undef],
371             [-3,3,"S",undef],
372             [-3,2,"S",undef],
373             [-3,1,"S",undef],
374             [-3,0,"S",undef],
375             [-2,-1,"S",undef],
376             [-1,-2,"S",undef],
377             );
378 2         11 push @tiles, @land_tiles;
379              
380 2         96 my @harbors = (
381             [ [[[0,-3],[0,-2],[-1,-2]], [[1,-3],[0,-2],[0,-3]]], "HR"],
382             [ [[[2,-3],[1,-2],[1,-3]], [[2,-3],[2,-2],[1,-2]]], "HRW"],
383             [ [[[3,-2],[2,-1],[2,-2]], [[3,-2],[3,-1],[2,-1]]], "HR"],
384             [ [[[3,-1],[3,0],[2,0]], [[3,0],[2,1],[2,0]]], "HR"],
385             [ [[[2,1],[1,2],[1,1]], [[1,1],[1,2],[0,2]]], "HRB"],
386             [ [[[0,2],[-1,3],[-1,2]], [[-1,2],[-1,3],[-2,3]]], "HRL"],
387             [ [[[-2,2],[-2,3],[-3,3]], [[-2,2],[-3,3],[-3,2]]], "HR"],
388             [ [[[-2,1],[-3,2],[-3,1]], [[-2,0],[-2,1],[-3,1]]], "HRG"],
389             [ [[[-1,-1],[-2,0],[-2,-1]], [[-1,-2],[-1,-1],[-2,-1]]],"HRO"]
390             );
391 2         21 return { tiles => \@tiles, harbors => \@harbors };
392             }
393             1;
394              
395             __END__