File Coverage

lib/Catan/Game/Player.pm
Criterion Covered Total %
statement 60 252 23.8
branch 0 70 0.0
condition 0 33 0.0
subroutine 23 60 38.3
pod 0 42 0.0
total 83 457 18.1


line stmt bran cond sub pod time code
1             package Catan::Game::Player;
2             $Catan::Game::Player::VERSION = '0.02';
3 4     4   22722 use strict;
  4     1   6  
  4         94  
  1         979  
  1         1  
  1         24  
4 4     4   18 use warnings;
  4     1   8  
  4         100  
  1         4  
  1         2  
  1         25  
5 4     4   1431 use Catan::Asset::Road;
  4     1   10  
  4         99  
  1         6  
  1         2  
  1         19  
6 4     4   1395 use Catan::Asset::Settlement;
  4     1   10  
  4         101  
  1         5  
  1         8  
  1         20  
7 4     4   1485 use Catan::Asset::City;
  4     1   9  
  4         106  
  1         4  
  1         2  
  1         22  
8 4     4   1435 use Catan::Asset::DevelopmentCard;
  4     1   9  
  4         107  
  1         5  
  1         2  
  1         18  
9 4     4   2710 use Games::Dice;
  4     1   5412  
  4         204  
  1         5  
  1         2  
  1         42  
10 4     4   22 use List::Util qw/sum/;
  4     1   1113  
  4         318  
  1         4  
  1         2  
  1         55  
11              
12             use overload
13 4         23 '""' => 'name',
  1         7  
14 4     4   4616 fallback => 1;
  4     1   3120  
  1         4  
  1         2  
15              
16             sub new
17             {
18 9     9 0 99 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         145 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 0     0 0 0 my $self = shift;
52 0         0 return sum values %{$self->resources};
  0         0  
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 0     0 0 0 my ($self, $bank, $class) = @_;
80 0         0 my %costs = ();
81 0         0 for (@{$class->cost})
  0         0  
82             {
83 0         0 $costs{$_->code} = $_->amount;
84             }
85 0         0 my $trade = Catan::Game::Trade->new($bank, [$self], { $self->number => \%costs }, 1);
86 0         0 $trade->execute;
87             }
88              
89             sub road_build
90             {
91 0     0 0 0 my ($self, $location, $bank, $free) = @_;
92              
93             die "$self has reached the maximum road limit\n"
94 0 0       0 unless @{$self->roads} < 15;
  0         0  
95              
96 0 0 0     0 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 0         0 if @{$self->settlements} == 2 && !$location->is_adjacent($self->settlements->[1]->location)
101 0 0 0     0 && @{$self->roads} == 1;
  0   0     0  
102              
103 0 0       0 $self->buy_asset($bank, 'Catan::Asset::Road') unless ($free);
104 0         0 my $road = Catan::Asset::Road->new($location);
105 0         0 push @{$self->roads}, $road;
  0         0  
106 0         0 push @{$self->{actions_this_turn}}, 'BR';
  0         0  
107 0         0 return $road;
108             }
109              
110             sub has_connecting_road
111             {
112 0     0 0 0 my ($self, $location) = @_;
113 0         0 return grep($_->location->is_adjacent($location), @{$self->roads});
  0         0  
114             }
115              
116             sub has_connecting_property
117             {
118 0     0 0 0 my ($self, $location) = @_;
119 0         0 return grep($location->is_adjacent($_->location), @{$self->properties});
  0         0  
120             }
121              
122             sub settlement_build
123             {
124 0     0 0 0 my ($self, $location, $bank, $free) = @_;
125              
126             die "$self has reached the maximum settlement limit\n"
127 0 0       0 unless @{$self->settlements} < 5;
  0         0  
128              
129 0 0 0     0 die "$self does not have an adjacent road to build a property\n"
130             unless $free || $self->has_connecting_road($location);
131              
132 0 0       0 $self->buy_asset($bank, 'Catan::Asset::Settlement') unless ($free);
133 0         0 my $settlement = Catan::Asset::Settlement->new($location);
134 0         0 push @{$self->{settlements}}, $settlement;
  0         0  
135 0         0 $self->action_add('BS');
136 0         0 return $settlement;
137             }
138              
139             sub city_build
140             {
141 0     0 0 0 my ($self, $intersection, $bank) = @_;
142 0         0 $self->buy_asset($bank, 'Catan::Asset::City');
143              
144             die "$self has reached the maximum city limit\n"
145 0 0       0 unless @{$self->cities} < 4;
  0         0  
146              
147             # remove old settlement
148 0         0 my ($city, @settlements);
149 0         0 for (@{$self->settlements})
  0         0  
150             {
151 0 0       0 if ($_->location->is_colliding($intersection))
152             {
153 0         0 $city = Catan::Asset::City->new($intersection);
154 0         0 undef $_;
155             }
156             else
157             {
158 0         0 push(@settlements, $_);
159             }
160             }
161 0 0       0 if ($city)
162             {
163 0         0 push @{$self->{cities}}, $city;
  0         0  
164 0         0 $self->action_add('BC');
165 0         0 $self->{settlements} = \@settlements;
166 0         0 return $city;
167             }
168 0         0 die "$self has no eligible settlements!\n";
169             }
170              
171             sub development_card_build
172             {
173 0     0 0 0 my ($self, $type, $bank) = @_;
174 0         0 $self->buy_asset($bank, 'Catan::Asset::DevelopmentCard');
175 0         0 my $card = Catan::Asset::DevelopmentCard->new($type);
176 0 0       0 $self->victory_points_add if $type eq 'VP';
177 0         0 push @{$self->{cards}}, $card;
  0         0  
178 0         0 $self->action_add('BD');
179 0         0 return $card;
180             }
181              
182             sub development_card_play
183             {
184 0     0 0 0 my ($self, $type) = @_;
185 0 0       0 die "development_card_play requires a type argument\n" unless $type;
186              
187 0 0       0 die "$self has already played a development card this turn\n"
188             if $self->has_played_dc;
189              
190 0 0       0 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 0         0 my ($card, @cards);
195 0         0 for (@{$self->{cards}})
  0         0  
196             {
197 0 0 0     0 if ($_->type eq $type && !$card)
198             {
199 0         0 $card = $_;
200             }
201             else
202             {
203 0         0 push(@cards, $_);
204             }
205             }
206 0 0       0 die "$self does not have that development card!\n" unless $card;
207 0         0 $self->{cards} = \@cards;
208 0         0 $self->action_add('PD');
209 0         0 return $card;
210             }
211              
212 0     0 0 0 sub action_history { $_[0]->{action_history} }
213 0     0 0 0 sub actions_taken { $_[0]->{actions_this_turn} }
214 0     0 0 0 sub action_add { push @{$_[0]->{actions_this_turn}}, $_[1] }
  0         0  
215              
216             sub actions_clear
217             {
218 0     0 0 0 my $self = shift;
219 0         0 push @{$self->{action_history}}, $self->actions_taken;
  0         0  
220 0         0 $self->{actions_this_turn} = [];
221             }
222              
223 0     0 0 0 sub victory_points_add { ++$_[0]->{points} }
224              
225             sub victory_points_count
226             {
227 0     0 0 0 my $self = shift;
228             return $self->{points} # vp cards played
229             + ($self->{longest_road} ? 2 : 0)
230             + ($self->{largest_army} ? 2 : 0)
231 0         0 + @{$self->{settlements}}
232 0 0       0 + (@{$self->{cities}} * 2);
  0 0       0  
233             }
234              
235             sub largest_army_toggle
236             {
237 0     0 0 0 my $self = shift;
238 0 0       0 $self->{largest_army} = $self->{largest_army} ? 0 : 1;
239             }
240              
241             sub longest_road_toggle
242             {
243 0     0 0 0 my $self = shift;
244 0 0       0 $self->{longest_road} = $self->{longest_road} ? 0 : 1;
245             }
246              
247             sub max_road_calculate
248             {
249 0     0 0 0 my ($self, $all_properties) = @_;
250              
251 0         0 my %intersections;
252              
253 0         0 for (@{$self->roads})
  0         0  
254             {
255 0         0 push @{$intersections{$_->location->start->uuid}}, $_;
  0         0  
256 0         0 push @{$intersections{$_->location->end->uuid}}, $_;
  0         0  
257             }
258              
259             # delete intersections occupied by enemy properties
260             # as they break roads
261 0         0 for my $prop (@$all_properties)
262             {
263             # skip player's own properties
264 0 0       0 next if grep($prop->location->is_colliding($_->location), @{$self->properties});
  0         0  
265 0         0 delete $intersections{$prop->location->uuid};
266             }
267              
268 0         0 my @paths;
269 0         0 for my $k (keys %intersections)
270             {
271 0         0 for my $r (@{$intersections{$k}})
  0         0  
272             {
273 0         0 push @paths, @{backtrack([[{uuid =>$k, road => $r}]], \%intersections)};
  0         0  
274             }
275             }
276 0         0 my @sorted_paths = sort { @$b <=> @$a } @paths, [];
  0         0  
277 0         0 my $max_road = shift @sorted_paths;
278 0         0 $self->{max_road_length} = scalar @$max_road;
279 0         0 return $max_road;
280             }
281              
282 0     0 0 0 sub max_road_length { $_[0]->{max_road_length} }
283              
284             sub backtrack
285             {
286 0     0 0 0 my ($paths, $intersections) = @_;
287              
288 0 0 0     0 die 'backtrack requires an arrayref and a hashref as arguments'
      0        
      0        
      0        
      0        
289             unless $paths && ref $paths eq 'ARRAY' && @$paths
290             && $intersections && ref $intersections eq 'HASH' && keys %$intersections;
291              
292 0         0 my @new_paths = ();
293              
294 0         0 for my $path (@$paths)
295             {
296 0         0 my $uuid = $path->[-1]{uuid};
297 0         0 my $road = $path->[-1]{road};
298              
299 0         0 for my $connecting_road (@{ $intersections->{$uuid} })
  0         0  
300             {
301 0 0       0 next if scalar grep($connecting_road->location->is_colliding($_->{road}->location), @$path);
302              
303 0         0 my $next_uuid;
304              
305 0 0       0 if ($connecting_road->location->start eq $uuid)
    0          
306             {
307 0         0 $next_uuid = $connecting_road->location->end->uuid;
308             }
309             elsif ($connecting_road->location->end eq $uuid)
310             {
311 0         0 $next_uuid = $connecting_road->location->start->uuid;
312             }
313              
314 0 0       0 if ($next_uuid)
315             {
316             # make a copy of the path
317 0         0 my @new_path = @$path;
318 0         0 push @new_path, {uuid => $next_uuid, road => $connecting_road};
319 0         0 push @new_paths, \@new_path;
320             }
321             }
322             }
323 0 0       0 if (@new_paths)
324             {
325 0         0 return backtrack(\@new_paths, $intersections);
326             }
327 0         0 return $paths;
328             }
329              
330             # players with generic and specific harbors get better trading ratios with the bank
331             sub update_ratios
332             {
333 0     0 0 0 my ($self, $map) = @_;
334 0         0 my @tiles = map(@{$map->tiles_by_intersection($_->location)}, @{$self->properties});
  0         0  
  0         0  
335 0         0 my %ratios = %{$self->ratios};
  0         0  
336              
337 0         0 for (@tiles)
338             {
339 0 0       0 if ($_->code eq 'HR')
    0          
    0          
    0          
    0          
    0          
340             {
341 0         0 while (my ($k, $v) = each %ratios)
342             {
343 0 0       0 $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 0         0 $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 0         0 $self->{ratios} = \%ratios;
368             }
369              
370             sub roll_dice
371             {
372 0     0 0 0 my ($self, $result) = @_;
373 0         0 $self->action_add('RD');
374 0   0     0 return ($result || Games::Dice::roll('2d6'));
375             }
376              
377 74     74 0 268 sub number { $_[0]->{number} }
378 0     0 0 0 sub properties { [ @{$_[0]->{settlements}}, @{$_[0]->{cities}} ] }
  0         0  
  0         0  
379 0     0 0 0 sub roads { $_[0]->{roads} }
380 0     0 0 0 sub settlements { $_[0]->{settlements} }
381 0     0 0 0 sub cities { $_[0]->{cities} }
382 13     13 0 48 sub resources { $_[0]->{resources} }
383 0     0 0 0 sub has_rolled_dice { grep $_ eq 'RD', @{$_[0]->actions_taken} }
  0         0  
384 0     0 0 0 sub has_built_road { grep $_ eq 'BR', @{$_[0]->actions_taken} }
  0         0  
385 0     0 0 0 sub has_built_settlement { grep $_ eq 'BS', @{$_[0]->actions_taken} }
  0         0  
386 0     0 0 0 sub has_built_city { grep $_ eq 'BC', @{$_[0]->actions_taken} }
  0         0  
387 0     0 0 0 sub has_played_dc { grep $_ eq 'PD', @{$_[0]->actions_taken} }
  0         0  
388 0     0 0 0 sub add_knight { ++$_[0]->{knights} }
389 0     0 0 0 sub knights { $_[0]->{knights} }
390 0     0 0 0 sub longest_road { $_[0]->{longest_road} }
391 0     0 0 0 sub largest_army { $_[0]->{largest_army} }
392 1     1 0 72 sub name { "Player $_[0]->{number}" }
393 3     3 0 32 sub ratios { $_[0]->{ratios} }
394             1;
395              
396             __END__