File Coverage

lib/Settlers/Game/Player.pm
Criterion Covered Total %
statement 236 257 91.8
branch 49 74 66.2
condition 20 35 57.1
subroutine 55 60 91.6
pod 0 42 0.0
total 360 468 76.9


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