File Coverage

lib/Catan/Game/Player.pm
Criterion Covered Total %
statement 231 252 91.6
branch 48 70 68.5
condition 18 33 54.5
subroutine 55 60 91.6
pod 0 42 0.0
total 352 457 77.0


line stmt bran cond sub pod time code
1             package Catan::Game::Player;
2             $Catan::Game::Player::VERSION = '0.03';
3 4     4   24060 use strict;
  4     1   9  
  4         97  
  1         820  
  1         2  
  1         23  
4 4     4   20 use warnings;
  4     1   5  
  4         101  
  1         5  
  1         2  
  1         24  
5 4     4   1426 use Catan::Asset::Road;
  4     1   11  
  4         108  
  1         6  
  1         2  
  1         20  
6 4     4   1386 use Catan::Asset::Settlement;
  4     1   10  
  4         99  
  1         5  
  1         2  
  1         21  
7 4     4   1482 use Catan::Asset::City;
  4     1   9  
  4         97  
  1         6  
  1         8  
  1         28  
8 4     4   1544 use Catan::Asset::DevelopmentCard;
  4     1   9  
  4         106  
  1         6  
  1         2  
  1         25  
9 4     4   2753 use Games::Dice;
  4     1   5687  
  4         228  
  1         6  
  1         2  
  1         47  
10 4     4   1206 use List::Util qw/sum/;
  4     1   6  
  4         268  
  1         5  
  1         2  
  1         63  
11              
12             use overload
13 4         26 '""' => 'name',
  1         6  
14 4     4   4605 fallback => 1;
  4     1   3326  
  1         5  
  1         2  
15              
16             sub new
17             {
18 9     9 0 106 my ($class, $args) = @_;
19              
20             bless {
21             actions_this_turn=> [],
22             action_history => [],
23             number => $args->{number},
24             # all resources start at zero!
25 9         161 resources => {B => 0, G => 0, L => 0, O => 0, W => 0},
26             ratios => {B => 4, G => 4, L => 4, O => 4, W => 4},
27             roads => [],
28             settlements => [],
29             cities => [],
30             cards => [],
31             points => 0,
32             knights => 0,
33             longest_road => 0,
34             largest_army => 0,
35             max_road => 0,
36             }, $class;
37             }
38              
39             sub resource_lose_random
40             {
41 0     0 0 0 my $self = shift;
42 0         0 for (keys %{$self->{resources}})
  0         0  
43             {
44 0 0       0 return $_ if $self->{resources}{$_} > 0;
45             }
46 0         0 die "$self does not have any resources to lose\n";
47             }
48              
49             sub resource_total
50             {
51 20     20 0 24 my $self = shift;
52 20         24 return sum values %{$self->resources};
  20         43  
53             }
54              
55             sub summary
56             {
57 0     0 0 0 my $self = shift;
58             return {
59             victory_points => $self->victory_points_count,
60             resources => $self->resources,
61             assets => {
62 0         0 settlements => scalar @{$self->settlements},
63 0         0 cities => scalar @{$self->cities},
64 0         0 cards => scalar @{$self->{cards}},
65 0         0 roads => scalar @{$self->roads},
66             max_road_length => $self->max_road_length,
67             longest_road => $self->longest_road,
68             },
69             military => {
70             knights_played => $self->{knights},
71             largest_army => $self->largest_army,
72             },
73 0         0 actions_this_turn => join ', ', @{$_[0]->actions},
  0         0  
74             };
75             }
76              
77             sub buy_asset
78             {
79 37     37 0 81 my ($self, $bank, $class) = @_;
80 37         95 my %costs = ();
81 37         53 for (@{$class->cost})
  37         170  
82             {
83 92         246 $costs{$_->code} = $_->amount;
84             }
85 37         230 my $trade = Catan::Game::Trade->new($bank, [$self], { $self->number => \%costs }, 1);
86 36         148 $trade->execute;
87             }
88              
89             sub road_build
90             {
91 30     30 0 60 my ($self, $location, $bank, $free) = @_;
92              
93             die "$self has reached the maximum road limit\n"
94 30 50       52 unless @{$self->roads} < 15;
  30         73  
95              
96 30 50 66     106 die "$self does not have an adjacent property to build a road there\n"
97             unless $self->has_connecting_property($location) || $self->has_connecting_road($location);
98              
99             die "$self must build a road next to their second property during deployment\n"
100 30         190 if @{$self->settlements} == 2 && !$location->is_adjacent($self->settlements->[1]->location)
101 30 50 100     84 && @{$self->roads} == 1;
  14   66     35  
102              
103 30 100       135 $self->buy_asset($bank, 'Catan::Asset::Road') unless ($free);
104 30         158 my $road = Catan::Asset::Road->new($location);
105 30         49 push @{$self->roads}, $road;
  30         67  
106 30         53 push @{$self->{actions_this_turn}}, 'BR';
  30         112  
107 30         110 return $road;
108             }
109              
110             sub has_connecting_road
111             {
112 18     18 0 37 my ($self, $location) = @_;
113 18         32 return grep($_->location->is_adjacent($location), @{$self->roads});
  18         40  
114             }
115              
116             sub has_connecting_property
117             {
118 30     30 0 49 my ($self, $location) = @_;
119 30         52 return grep($location->is_adjacent($_->location), @{$self->properties});
  30         283  
120             }
121              
122             sub settlement_build
123             {
124 13     13 0 30 my ($self, $location, $bank, $free) = @_;
125              
126             die "$self has reached the maximum settlement limit\n"
127 13 50       18 unless @{$self->settlements} < 5;
  13         48  
128              
129 13 50 66     56 die "$self does not have an adjacent road to build a property\n"
130             unless $free || $self->has_connecting_road($location);
131              
132 13 100       62 $self->buy_asset($bank, 'Catan::Asset::Settlement') unless ($free);
133 13         68 my $settlement = Catan::Asset::Settlement->new($location);
134 13         29 push @{$self->{settlements}}, $settlement;
  13         38  
135 13         38 $self->action_add('BS');
136 13         87 return $settlement;
137             }
138              
139             sub city_build
140             {
141 6     6 0 52 my ($self, $intersection, $bank) = @_;
142 6         21 $self->buy_asset($bank, 'Catan::Asset::City');
143              
144             die "$self has reached the maximum city limit\n"
145 5 50       19 unless @{$self->cities} < 4;
  5         17  
146              
147             # remove old settlement
148 5         10 my ($city, @settlements);
149 5         9 for (@{$self->settlements})
  5         16  
150             {
151 13 100       39 if ($_->location->is_colliding($intersection))
152             {
153 5         23 $city = Catan::Asset::City->new($intersection);
154 5         35 undef $_;
155             }
156             else
157             {
158 8         24 push(@settlements, $_);
159             }
160             }
161 5 50       22 if ($city)
162             {
163 5         9 push @{$self->{cities}}, $city;
  5         13  
164 5         30 $self->action_add('BC');
165 5         11 $self->{settlements} = \@settlements;
166 5         22 return $city;
167             }
168 0         0 die "$self has no eligible settlements!\n";
169             }
170              
171             sub development_card_build
172             {
173 8     8 0 23 my ($self, $type, $bank) = @_;
174 8         29 $self->buy_asset($bank, 'Catan::Asset::DevelopmentCard');
175 8         47 my $card = Catan::Asset::DevelopmentCard->new($type);
176 8 100       27 $self->victory_points_add if $type eq 'VP';
177 8         11 push @{$self->{cards}}, $card;
  8         20  
178 8         21 $self->action_add('BD');
179 8         22 return $card;
180             }
181              
182             sub development_card_play
183             {
184 7     7 0 14 my ($self, $type) = @_;
185 7 50       19 die "development_card_play requires a type argument\n" unless $type;
186              
187 7 50       20 die "$self has already played a development card this turn\n"
188             if $self->has_played_dc;
189              
190 7 50       21 die "Victory Point cards cannot be played - they are automatically added to a player's VP total\n"
191             if $type eq 'VP';
192              
193             # remove card
194 7         14 my ($card, @cards);
195 7         10 for (@{$self->{cards}})
  7         21  
196             {
197 10 100 100     45 if ($_->type eq $type && !$card)
198             {
199 7         19 $card = $_;
200             }
201             else
202             {
203 3         7 push(@cards, $_);
204             }
205             }
206 7 50       22 die "$self does not have that development card!\n" unless $card;
207 7         16 $self->{cards} = \@cards;
208 7         23 $self->action_add('PD');
209 7         21 return $card;
210             }
211              
212 0     0 0 0 sub action_history { $_[0]->{action_history} }
213 247     247 0 1423 sub actions_taken { $_[0]->{actions_this_turn} }
214 89     89 0 174 sub action_add { push @{$_[0]->{actions_this_turn}}, $_[1] }
  89         343  
215              
216             sub actions_clear
217             {
218 64     64 0 86 my $self = shift;
219 64         72 push @{$self->{action_history}}, $self->actions_taken;
  64         172  
220 64         234 $self->{actions_this_turn} = [];
221             }
222              
223 1     1 0 3 sub victory_points_add { ++$_[0]->{points} }
224              
225             sub victory_points_count
226             {
227 441     441 0 615 my $self = shift;
228             return $self->{points} # vp cards played
229             + ($self->{longest_road} ? 2 : 0)
230             + ($self->{largest_army} ? 2 : 0)
231 441         1255 + @{$self->{settlements}}
232 441 100       1354 + (@{$self->{cities}} * 2);
  441 100       1593  
233             }
234              
235             sub largest_army_toggle
236             {
237 1     1 0 2 my $self = shift;
238 1 50       6 $self->{largest_army} = $self->{largest_army} ? 0 : 1;
239             }
240              
241             sub longest_road_toggle
242             {
243 3     3 0 6 my $self = shift;
244 3 100       12 $self->{longest_road} = $self->{longest_road} ? 0 : 1;
245             }
246              
247             sub max_road_calculate
248             {
249 172     172 0 248 my ($self, $all_properties) = @_;
250              
251 172         230 my %intersections;
252              
253 172         189 for (@{$self->roads})
  172         357  
254             {
255 596         771 push @{$intersections{$_->location->start->uuid}}, $_;
  596         1537  
256 596         918 push @{$intersections{$_->location->end->uuid}}, $_;
  596         1365  
257             }
258              
259             # delete intersections occupied by enemy properties
260             # as they break roads
261 172         317 for my $prop (@$all_properties)
262             {
263             # skip player's own properties
264 1400 100       1760 next if grep($prop->location->is_colliding($_->location), @{$self->properties});
  1400         2757  
265 1050         3112 delete $intersections{$prop->location->uuid};
266             }
267              
268 172         228 my @paths;
269 172         546 for my $k (keys %intersections)
270             {
271 763         910 for my $r (@{$intersections{$k}})
  763         1511  
272             {
273 1192         1346 push @paths, @{backtrack([[{uuid =>$k, road => $r}]], \%intersections)};
  1192         5163  
274             }
275             }
276 172         746 my @sorted_paths = sort { @$b <=> @$a } @paths, [];
  3839         4907  
277 172         274 my $max_road = shift @sorted_paths;
278 172         348 $self->{max_road_length} = scalar @$max_road;
279 172         2148 return $max_road;
280             }
281              
282 0     0 0 0 sub max_road_length { $_[0]->{max_road_length} }
283              
284             sub backtrack
285             {
286 3624     3624 0 5002 my ($paths, $intersections) = @_;
287              
288 3624 50 33     44710 die 'backtrack requires an arrayref and a hashref as arguments'
      33        
      33        
      33        
      33        
289             unless $paths && ref $paths eq 'ARRAY' && @$paths
290             && $intersections && ref $intersections eq 'HASH' && keys %$intersections;
291              
292 3624         5158 my @new_paths = ();
293              
294 3624         5863 for my $path (@$paths)
295             {
296 4456         7671 my $uuid = $path->[-1]{uuid};
297 4456         6485 my $road = $path->[-1]{road};
298              
299 4456         4840 for my $connecting_road (@{ $intersections->{$uuid} })
  4456         8958  
300             {
301 8120 100       23827 next if scalar grep($connecting_road->location->is_colliding($_->{road}->location), @$path);
302              
303 3264         3781 my $next_uuid;
304              
305 3264 100       8004 if ($connecting_road->location->start eq $uuid)
    50          
306             {
307 1486         3591 $next_uuid = $connecting_road->location->end->uuid;
308             }
309             elsif ($connecting_road->location->end eq $uuid)
310             {
311 1778         4037 $next_uuid = $connecting_road->location->start->uuid;
312             }
313              
314 3264 50       8146 if ($next_uuid)
315             {
316             # make a copy of the path
317 3264         7608 my @new_path = @$path;
318 3264         8954 push @new_path, {uuid => $next_uuid, road => $connecting_road};
319 3264         9443 push @new_paths, \@new_path;
320             }
321             }
322             }
323 3624 100       7510 if (@new_paths)
324             {
325 2432         5018 return backtrack(\@new_paths, $intersections);
326             }
327 1192         6544 return $paths;
328             }
329              
330             # players with generic and specific harbors get better trading ratios with the bank
331             sub update_ratios
332             {
333 13     13 0 28 my ($self, $map) = @_;
334 13         20 my @tiles = map(@{$map->tiles_by_intersection($_->location)}, @{$self->properties});
  28         83  
  13         40  
335 13         39 my %ratios = %{$self->ratios};
  13         43  
336              
337 13         50 for (@tiles)
338             {
339 84 100       233 if ($_->code eq 'HR')
    50          
    50          
    100          
    50          
    50          
340             {
341 1         7 while (my ($k, $v) = each %ratios)
342             {
343 5 50       24 $ratios{$k} = 3 if $v == 4;
344             }
345             }
346             elsif ($_->code eq 'HRB')
347             {
348 0         0 $ratios{B} = 2;
349             }
350             elsif ($_->code eq 'HRG')
351             {
352 0         0 $ratios{G} = 2;
353             }
354             elsif ($_->code eq 'HRL')
355             {
356 3         10 $ratios{L} = 2;
357             }
358             elsif ($_->code eq 'HRO')
359             {
360 0         0 $ratios{O} = 2;
361             }
362             elsif ($_->code eq 'HRW')
363             {
364 0         0 $ratios{W} = 2;
365             }
366             }
367 13         58 $self->{ratios} = \%ratios;
368             }
369              
370             sub roll_dice
371             {
372 56     56 0 99 my ($self, $result) = @_;
373 56         152 $self->action_add('RD');
374 56   33     213 return ($result || Games::Dice::roll('2d6'));
375             }
376              
377 2494     2494 0 10061 sub number { $_[0]->{number} }
378 1982     1982 0 2179 sub properties { [ @{$_[0]->{settlements}}, @{$_[0]->{cities}} ] }
  1982         3480  
  1982         7903  
379 392     392 0 1433 sub roads { $_[0]->{roads} }
380 76     76 0 372 sub settlements { $_[0]->{settlements} }
381 5     5 0 22 sub cities { $_[0]->{cities} }
382 798     798 0 2517 sub resources { $_[0]->{resources} }
383 108     108 0 175 sub has_rolled_dice { grep $_ eq 'RD', @{$_[0]->actions_taken} }
  108         330  
384 39     39 0 68 sub has_built_road { grep $_ eq 'BR', @{$_[0]->actions_taken} }
  39         138  
385 22     22 0 37 sub has_built_settlement { grep $_ eq 'BS', @{$_[0]->actions_taken} }
  22         58  
386 0     0 0 0 sub has_built_city { grep $_ eq 'BC', @{$_[0]->actions_taken} }
  0         0  
387 14     14 0 19 sub has_played_dc { grep $_ eq 'PD', @{$_[0]->actions_taken} }
  14         34  
388 3     3 0 10 sub add_knight { ++$_[0]->{knights} }
389 12     12 0 55 sub knights { $_[0]->{knights} }
390 115     115 0 447 sub longest_road { $_[0]->{longest_road} }
391 10     10 0 37 sub largest_army { $_[0]->{largest_army} }
392 39     39 0 285 sub name { "Player $_[0]->{number}" }
393 136     136 0 429 sub ratios { $_[0]->{ratios} }
394             1;
395              
396             __END__