File Coverage

lib/Catan/Game.pm
Criterion Covered Total %
statement 607 640 94.8
branch 241 414 58.2
condition 97 181 53.5
subroutine 66 69 95.6
pod 0 50 0.0
total 1011 1354 74.6


line stmt bran cond sub pod time code
1             package Catan::Game;
2             $Catan::Game::VERSION = '0.03';
3 2     2   36360 use strict;
  2         5  
  2         55  
4 2     2   32 use 5.20.0;
  2         7  
5 2     2   1723 use experimental qw/postderef signatures/;
  2         8214  
  2         12  
6 2     2   418 use warnings;
  2         4  
  2         50  
7 2     2   8 no warnings 'experimental';
  2         5  
  2         67  
8 2     2   823 use Catan::Event::Monopoly;
  2         5  
  2         54  
9 2     2   862 use Catan::Event::RoadBuilding;
  2         5  
  2         53  
10 2     2   774 use Catan::Event::Robber;
  2         6  
  2         75  
11 2     2   963 use Catan::Event::YearOfPlenty;
  2         4  
  2         65  
12 2     2   855 use Catan::Game::Bank;
  2         6  
  2         84  
13 2     2   837 use Catan::Game::Player;
  2         5  
  2         62  
14 2     2   13 use Catan::Game::Trade;
  2         4  
  2         45  
15 2     2   523 use Catan::Map;
  2         7  
  2         69  
16 2     2   2105 use Data::Dumper;
  2         14518  
  2         139  
17 2     2   1462 use Data::UUID;
  2         1518  
  2         183  
18 2     2   2042 use JSON::XS 'encode_json';
  2         120702  
  2         267  
19 2     2   27 use List::Util qw/all/;
  2         6  
  2         26279  
20              
21             #ABSTRACT: a class for managing games of Catan
22              
23             # valid phases
24             our @phases = qw/Setup Deployment Play End/;
25              
26             our %actions = (
27             BC => \&build_city,
28             BD => \&build_dc,
29             BR => \&build_road,
30             BS => \&build_settlement,
31             CH => \&chat,
32             CR => \&concede_resources,
33             DR => \&dice_roll,
34             MD => \&map_define,
35             MO => \&monopoly,
36             PA => \&player_add,
37             PD => \&play_dc,
38             PE => \&phase_end,
39             RM => \&robber_move,
40             RP => \&resource_production,
41             RR => \&robber_rob,
42             TA => \&trade_accept,
43             TB => \&trade_bank,
44             TC => \&trade_cancel,
45             TE => \&turn_end,
46             TO => \&trade_offer,
47             TS => \&turn_start,
48             YP => \&year_of_plenty,
49             # these are called automatically:
50             # GO => \&game_over,
51             # LA => \&largest_army,
52             # LR => \&longest_road,
53             # PS => \&phase_start,
54             # RA => \&robber_activate,
55             # RD => \&robber_deactivate,
56             # RE => \&round_end,
57             # RS => \&round_start,
58             );
59              
60 1 50   1 0 1274 sub new ($class, $args = {})
  1 50       4  
  1 50       4  
  1         5  
61 1         2 {
62             my $self = bless {
63             bank => Catan::Game::Bank->new,
64             log => exists $args->{log} && ref $args->{log} eq 'GLOB' ? $args->{log} : undef,
65 1 50 33     13 map => undef,
66             max_players => 4,
67             max_victory_points => 10,
68             monopoly => undef,
69             phase_index => -1,
70             players => [],
71             road_building => undef,
72             round => 0,
73             trades => {},
74             turn_index => -1,
75             year_of_plenty => undef,
76             }, $class;
77              
78             # begin setup phase (and print to log if present)
79 1         7 $self->_log($self->phase_start);
80              
81 1         9 return $self;
82             }
83              
84 239 50   239 0 899 sub action ($self, $action_code, $args = {})
  239 50       622  
  239 100       353  
  239         366  
  239         654  
85 239         278 {
86 239         400 my @action_history = ();
87              
88 239 50       930 if (!$action_code)
    100          
89             {
90 0         0 push @action_history, $self->chat({player => 'A', msg => "action requires an action code\n"});
91             }
92             elsif (exists $actions{$action_code})
93             {
94 238         504 my $results = eval { $actions{$action_code}->($self, $args) };
  238         806  
95 238 100       604 push @$results, $self->chat({player => 'A', msg => $@}) if $@;
96 238         1037 push @action_history, @$results;
97             }
98             else
99             {
100 1         9 push @action_history, $self->chat({player => 'A', msg => "action type $action_code is unknown\n"});
101             }
102 239         716 $self->_log(\@action_history);
103 239         1455 return \@action_history;
104             }
105              
106 30 50   30 0 95 sub build_road ($self, $args)
  30 50       86  
  30         54  
  30         56  
107 30         51 {
108 30         69 my $player_number = $args->{player};
109 30         56 my $location = $args->{path};
110              
111 30 50 33     174 die "deploy road requires player and path arguments!\n"
112             unless $player_number && $location;
113              
114 30 50       94 die "It is not player $player_number\'s turn!\n"
115             unless $self->is_players_turn($player_number);
116              
117 30 50       102 die "You can only build during the Deployment and Play phases\n"
118             unless $self->phase =~ /^(?:Deployment|Play)$/;
119              
120 30 50 66     100 die "Player $player_number has already deployed a road this turn!\n"
121             if $self->player->has_built_road && $self->phase eq 'Deployment';
122              
123             die "Player $player_number hasn't rolled the dice yet"
124             unless $self->phase eq 'Deployment' || $self->player->has_rolled_dice
125 30 50 100     91 || (defined $self->{road_building} && $self->{road_building}->can_build_road);
      33        
      66        
126              
127 30 50       108 die "Player $player_number must move the robber first\n"
128             if $self->robber->active;
129              
130 30 50       96 die "Invalid path\n" unless my $path = $self->map->find_path($location);
131              
132 30         62 for my $player (@{$self->players})
  30         84  
133             {
134             die "That path is occupied\n"
135 120 50       161 if grep($path->is_colliding($_->location), @{$player->roads});
  120         334  
136             }
137              
138             my $free = ($self->phase eq 'Deployment' ||
139 30 100 66     86 (defined $self->{road_building} && $self->{road_building}->can_build_road())) ? 1 : 0;
140 30         110 $self->player->road_build($path, $self->bank, $free);
141              
142 30         55 my @actions = ();
143             push @actions, { BR => { player => $player_number, path => $location }},
144 30         120 @{$self->longest_road};
  30         104  
145 30         123 return \@actions;
146             }
147              
148 43 50   43 0 134 sub longest_road ($self)
  43 50       117  
  43         86  
149 43         53 {
150 43         81 my @actions = ();
151              
152 43         63 my @properties = map { @{$_->properties} } @{$self->players};
  172         267  
  172         466  
  43         103  
153 43         111 my $players_by_road_length = {};
154 43         83 for (@{$self->players})
  43         110  
155             {
156 172         200 my $length = scalar @{$_->max_road_calculate(\@properties)};
  172         716  
157 172         488 push @{ $players_by_road_length->{ $length } }, $_;
  172         651  
158             }
159              
160 43         78 my $have_tested_leading_road;
161 43         276 for my $road_length (sort {$b <=> $a} keys %$players_by_road_length)
  96         247  
162             {
163 118         160 my @players = @{$players_by_road_length->{$road_length}};
  118         340  
164              
165 118         214 for my $player (@players)
166             {
167 172 100 100     708 if ($have_tested_leading_road)
    100 100        
168             {
169 104 100       309 $player->longest_road_toggle if $player->longest_road;
170             }
171             elsif (@players == 1 && $road_length >= 6 && !$player->longest_road)
172             {
173 2         10 $player->longest_road_toggle;
174 2         9 push @actions, { LR => {player => $player->number, length => $road_length}};
175             }
176             }
177 118         266 $have_tested_leading_road = 1;
178             }
179             # check for player victory from longest road
180 43 50       173 if (my $game_over = $self->game_over)
181             {
182 0         0 push @actions, @$game_over;
183             }
184 43         269 return \@actions;
185             }
186              
187 46 50   46 0 142 sub game_over ($self)
  46 50       130  
  46         112  
188 46         72 {
189 46 100       180 if (my $player = $self->player_victory_check)
190             {
191 1         4 return [{ GO => { player => $player->number } }, @{$self->phase_end}];
  1         6  
192             }
193             }
194              
195 15 50   15 0 55 sub build_settlement ($self, $args)
  15 50       46  
  15         26  
  15         26  
196 15         28 {
197 15         32 my $player_number = $args->{player};
198 15         37 my $location = $args->{intersection};
199              
200 15 50       45 die "You can only build during the Deployment and Play phases\n"
201             unless $self->phase =~ /^(?:Deployment|Play)$/;
202              
203 15 50 33     94 die "deploy settlement requires player and intersection arguments!\n"
204             unless $player_number && $location;
205              
206 15 100       45 die "It is not $player_number\'s turn!\n"
207             unless $self->is_players_turn($player_number);
208              
209 14 50 66     506 die "Player $player_number hasn't rolled the dice yet\n"
210             unless $self->phase eq 'Deployment' || $self->player->has_rolled_dice;
211              
212 14 100 66     35 die "Player $player_number has already deployed a settlement this turn!\n"
213             if $self->player->has_built_settlement && $self->phase eq 'Deployment';
214              
215 13 50       38 die "Player $player_number must move the robber first\n"
216             if $self->robber->active;
217              
218 13 50       39 die "Invalid intersection\n" unless my $intersection = $self->map->find_intersection($location);
219              
220 13         29 for my $player (@{$self->players})
  13         32  
221             {
222             die "That intersection is occupied or too close to another property\n"
223             unless 0 == grep(($intersection->is_colliding($_->location)
224 52 50 33     67 || $intersection->is_adjacent($_->location)), @{$player->properties});
  52         146  
225             }
226              
227 13 100       40 my $free = $self->phase eq 'Deployment' ? 1 : 0;
228 13         40 my $settlement = $self->player->settlement_build($intersection, $self->bank, $free);
229 13         68 my @actions = ({ BS => { player => $player_number, intersection => $location } });
230 13         34 $self->player->update_ratios($self->map); # in case they built next to a harbor
231              
232             # special resource production on deploying 2nd settlement
233 13 100 100     70 if ($self->phase eq 'Deployment' && $self->round == 2)
234             {
235 4         10 push @actions, @{$self->resource_production_deployment($settlement->location)};
  4         17  
236             }
237 13         29 push @actions, @{$self->longest_road};
  13         45  
238 13         62 return \@actions;
239             }
240              
241 6 50   6 0 28 sub build_city ($self, $args)
  6 50       20  
  6         14  
  6         13  
242 6         11 {
243 6         18 my $player_number = $args->{player};
244 6         17 my $location = $args->{intersection};
245              
246 6 50       19 die "You can only build during the Deployment and Play phases\n"
247             unless $self->phase =~ /^(?:Deployment|Play)$/;
248              
249 6 50 33     40 die "build settlement requires player and intersection arguments!\n"
250             unless $player_number && $location;
251              
252 6 50       19 die "It is not Player $player_number\'s turn!\n"
253             unless $self->is_players_turn($player_number);
254              
255 6 50       21 die "Player $player_number hasn't rolled the dice yet\n"
256             unless $self->player->has_rolled_dice;
257              
258 6 50       20 die "Player $player_number must move the robber first\n"
259             if $self->robber->active;
260              
261 6 50       18 die "Invalid intersection\n" unless my $intersection = $self->map->find_intersection($location);
262              
263 6         27 $self->player->city_build($intersection, $self->bank);
264 5         30 return [{ BC => { player => $player_number, intersection => $location } }];
265             }
266              
267 8 50   8 0 31 sub build_dc ($self, $args)
  8 50       19  
  8         19  
  8         11  
268 8         14 {
269 8         15 my $player_number = $args->{player};
270 8         13 my $type = $args->{type};
271              
272 8 50       30 die "You can only build during the Deployment and Play phases\n"
273             unless $self->phase =~ /^(?:Deployment|Play)$/;
274              
275 8 50       18 die "deploy settlement requires a player argument!\n"
276             unless $player_number;
277              
278 8 50       24 die "It is not $player_number\'s turn!\n"
279             unless $self->player->number == $player_number;
280              
281 8 50       23 die "Player $player_number hasn't rolled the dice yet\n"
282             unless $self->player->has_rolled_dice;
283              
284 8 50       26 die "Player $player_number must move the robber first\n"
285             if $self->robber->active;
286              
287 8         22 my $card = $self->player->development_card_build($self->bank->deck_draw($type), $self->bank);
288 8         34 return [{ BD => { player => $player_number, type => $card->type } }];
289             }
290              
291 7 50   7 0 28 sub play_dc ($self, $args)
  7 50       24  
  7         16  
  7         15  
292 7         9 {
293 7         18 my $player_number = $args->{player};
294 7         16 my $type = $args->{type};
295              
296 7 50 33     45 die "play development card requires player and development card type arguments!\n"
297             unless $player_number && $type;
298              
299 7 50       20 die "You can only play development cards during the Play phase\n"
300             unless $self->phase eq 'Play';
301              
302 7 50       23 die "It is not Player $player_number\'s turn!\n"
303             unless $self->is_players_turn($player_number);
304              
305 7 50       29 die "Player $player_number has already played a development card this turn!\n"
306             if $self->player->has_played_dc;
307              
308 7 50       21 die "Player $player_number must move the robber first\n"
309             if $self->robber->active;
310              
311 7         20 my $card = $self->player->development_card_play($type);
312              
313 7         17 my @actions = ();
314 7         28 push @actions, { PD => { player => $player_number, type => $card->type } };
315              
316 7 100       27 if ($card->type eq 'KN')
    100          
    100          
    50          
317             {
318 3         8 $self->player->add_knight;
319 3         18 push @actions, @{$self->robber_activate({from_7 => undef})},
320 3         5 @{$self->largest_army};
  3         14  
321             }
322             elsif ($card->type eq 'YP')
323             {
324 1         12 $self->{year_of_plenty} = Catan::Event::YearOfPlenty->new;
325             }
326             elsif ($card->type eq 'MO')
327             {
328 1         11 $self->{monopoly} = Catan::Event::Monopoly->new();
329             }
330             elsif ($card->type eq 'RB')
331             {
332 2         7 $self->{road_building} = Catan::Event::RoadBuilding->new($self->player);
333             }
334             else
335             {
336 0         0 die sprintf "Invalid development card type %s\n", $card->type;
337             }
338              
339 7         48 return \@actions;
340             }
341              
342             sub largest_army
343             {
344 3     3 0 6 my $self = shift;
345              
346             # largest army is the player who has played at least 3 knights
347             # and more knights than any other player
348 3         7 my %players_by_knights;
349 3         7 for (@{$self->players})
  3         10  
350             {
351 12         16 push @{$players_by_knights{ $_->knights }}, $_;
  12         34  
352             }
353              
354 3         8 my @actions = ();
355 3         5 my $have_tested_leaders;
356 3         20 for my $knights_played (sort {$b <=> $a} keys %players_by_knights)
  3         16  
357             {
358 6         8 my @players = @{$players_by_knights{$knights_played}};
  6         17  
359              
360 6         13 for my $player (@players)
361             {
362 12 100 66     53 if ($have_tested_leaders)
    100 66        
363             {
364 9 50       24 $player->largest_army->toggle if $player->largest_army;
365             }
366             elsif (@players == 1 && $knights_played >= 3 && !$player->largest_army)
367             {
368 1         5 $player->largest_army_toggle;
369 1         5 push @actions, {LA => {player => $player->number, strength => $knights_played}};
370             }
371             }
372 6         12 $have_tested_leaders = 1;
373             }
374              
375             # check for player victory
376 3 100       12 if (my $game_over = $self->game_over)
377             {
378 1         3 push @actions, @$game_over;
379             }
380 3         15 return \@actions;
381             }
382              
383 1 50   1 0 5 sub monopoly ($self, $args)
  1 50       4  
  1         2  
  1         2  
384 1         6 {
385 1         3 my $player_number = $args->{player};
386 1         3 my $code = $args->{resource_code};
387              
388 1 50 33     7 die "Monopoly requires player and resource code arguments!\n"
389             unless $player_number && $code;
390              
391 1 50       4 die "It is not Player $player_number\'s turn!\n"
392             unless $self->is_players_turn($player_number);
393              
394             die "Player $player_number does not have a monopoly!\n"
395 1 50       5 unless defined $self->{monopoly};
396              
397 1 50       4 die "Player $player_number must move the robber first\n"
398             if $self->robber->active;
399              
400 1         4 my $resources = $self->{monopoly}->calculate($player_number, $code, $self->players, $self->bank);
401 1         10 undef $self->{monopoly};
402              
403 1         6 return [{MO => {resources => $resources} }];
404             }
405              
406 1 50   1 0 5 sub year_of_plenty ($self, $args)
  1 50       3  
  1         2  
  1         2  
407 1         2 {
408 1         2 my $player_number = $args->{player};
409 1         3 my $resources = $args->{resources};
410              
411 1 50 33     14 die "Year of Plenty requires player and resource arguments!\n"
      33        
412             unless $player_number && $resources && ref $resources eq 'HASH';
413              
414 1 50       3 die "It is not Player $player_number\'s turn!\n"
415             unless $self->is_players_turn($player_number);
416              
417             die "Player $player_number does not have year of plenty!\n"
418 1 50       4 unless defined $self->{year_of_plenty};
419              
420 1         4 my $trade = Catan::Game::Trade->new($self->bank, $self->players, $resources, 1);
421              
422 1         6 $self->{year_of_plenty}->validate($trade);
423 1         4 $trade->execute;
424 1         8 undef $self->{year_of_plenty};
425              
426 1         5 return [{YP => {resources => $trade->as_hashref} }];
427             }
428              
429 4 50   4 0 13 sub phase_start ($self, $args = {})
  4 50       11  
  4 50       10  
  4         12  
430 4         8 {
431 4 50       21 die "The end phase is the last phase!\n" if $self->{phase_index} == $#phases;
432              
433 4         13 my $new_phase = $phases[ ++$self->{phase_index} ];
434              
435             # reset the round and turns counters
436 4         9 $self->{round} = 0;
437 4         10 $self->{turn_index} = -1;
438              
439 4         54 my @actions = ({PS => {phase => $new_phase}});
440              
441             # deploy the robber, start deployment round 1
442 4 100       22 if ($new_phase eq 'Deployment')
    100          
443             {
444 1         3 push @actions, @{$self->robber_setup}, @{$self->round_start};
  1         5  
  1         5  
445             }
446             elsif ($new_phase eq 'Play')
447             {
448 1         3 push @actions, @{$self->round_start};
  1         3  
449             }
450 4         24 return \@actions;
451             }
452              
453 0 0   0 0 0 sub concede_resources ($self, $args)
  0 0       0  
  0         0  
  0         0  
454 0         0 {
455 0         0 my $player_number = $args->{player};
456 0         0 my $resources = $args->{resources};
457 0         0 my $player = $self->player_by_number($player_number);
458              
459             die "Concede resources requires player and resource argument for 1 player!\n"
460             unless $player && $resources && ref $resources eq 'HASH'
461             && (1 == keys %$resources)
462             && (exists $resources->{$player_number})
463 0     0   0 && (all { $_ < 0 } values %{$resources->{$player_number}})
  0         0  
464 0 0 0     0 && (1 == grep($player_number == $_->{player}->number, @{$self->robber->check_players_to_concede}));
  0   0     0  
      0        
      0        
      0        
      0        
465              
466 0         0 my @actions = ();
467              
468 0         0 my $trade = Catan::Game::Trade->new($self->bank, $self->players, $resources, 1);
469 0         0 my $summary = $trade->execute;
470              
471 0         0 push @actions, {CR => { player => $player_number, resources => $summary }};
472              
473 0         0 my $msg;
474 0         0 for (@{$self->robber->check_players_to_concede})
  0         0  
475             {
476             $msg .= sprintf "Player %d must concede %d resources. ",
477 0         0 $_->{player}->number, ($_->{player}->resource_total - $_->{target_total});
478             }
479 0 0       0 push(@actions, $self->chat({player => 'A', msg => $msg})) if $msg;
480              
481 0         0 return \@actions;
482             }
483              
484 9 50   9 0 31 sub chat ($self,$args)
  9 50       25  
  9         16  
  9         12  
485 9         10 {
486 9         21 my $player_number = $args->{player};
487 9         20 my $msg = $args->{msg};
488              
489 9 50 33     69 die "Chat requires player and msg arguments\n"
      33        
490             unless ($player_number eq 'A' || $self->is_player_number($player_number)) && $msg;
491              
492 9         44 return { CH => {player => $player_number, msg => $msg} };
493             }
494              
495 1 50   1 0 6 sub robber_setup ($self, $args = {})
  1 50       10  
  1 50       4  
  1         5  
496 1         3 {
497 1         4 $self->{robber} = Catan::Event::Robber->new({map => $self->map});
498 1         7 return [ { RM => $self->robber->location->uuid } ];
499             }
500              
501 8 50   8 0 32 sub robber_activate ($self, $args = {})
  8 50       23  
  8 50       16  
  8         25  
502 8         12 {
503 8         29 my @actions = ({ RA => undef });
504              
505 8 100       32 my $players = $args->{from_7} ? $self->players : [];
506              
507 8         14 my $msg;
508 8         12 for (@{$self->robber->activate($players)})
  8         20  
509             {
510             $msg .= sprintf "Player %d must concede %d resources. ",
511 0         0 $_->{player}->number, ($_->{player}->resource_total - $_->{target_total});
512             }
513 8 50       25 push(@actions, $self->chat({player => 'A', msg => $msg})) if $msg;
514 8         23 return \@actions;
515             }
516              
517 0 0   0 0 0 sub robber_deactivate ($self, $args = {})
  0 0       0  
  0 0       0  
  0         0  
518 0         0 {
519 0         0 $self->robber->deactivate;
520 0         0 return [ { RD => undef } ];
521             }
522              
523             # steal and deactivate
524 6 50   6 0 23 sub robber_rob ($self, $args)
  6 50       19  
  6         11  
  6         11  
525 6         9 {
526 6         28 my $player = $self->player_by_number($args->{player});
527 6         22 my $target_player = $self->player_by_number($args->{target_player});
528 6         13 my $code = $args->{code};
529              
530 6 50 33     26 die "robber steal requires a player and target player arguments\n"
531             unless $player && $target_player;
532              
533 6 50       23 die "It is not $player\'s turn\n" unless $self->is_players_turn($player->number);
534              
535 6   33     17 $code ||= $self->robber->steal($target_player);
536 6         15 my $trade = Catan::Game::Trade->new($self->bank, $self->players, {
537             $target_player->number=> {$code =>-1},
538             $player->number => {$code => 1},
539             });
540 5         26 $trade->execute;
541 5         35 $self->robber->deactivate;
542             return [
543 5         20 { RR => $trade->as_hashref },
544             { RD => undef },
545             ];
546             }
547              
548 7 50   7 0 33 sub robber_move ($self, $args)
  7 50       22  
  7         16  
  7         9  
549 7         15 {
550 7         16 my $player_number = $args->{player};
551 7         15 my $location = $args->{tile};
552              
553 7 50 33     43 die "build settlement requires player and tile arguments!\n"
554             unless $player_number && $location;
555              
556 7 50       23 die "It is not player $player_number\'s turn!"
557             unless $player_number == $self->player->number;
558              
559 7         27 my $tile = $self->map->find_tile($location);
560              
561 7         28 $self->robber->move($tile, $self->players);
562 7         11 my @actions;
563 7         23 push @actions, { RM => { tile => $tile->uuid} };
564              
565             # robber will deactivate if there are no eligible players
566             # to steal from
567 7 50       30 push @actions, { RD => undef } unless $self->robber->active;
568 7         23 return \@actions;
569             }
570              
571 3 50   3 0 12 sub phase_end ($self, $args = {})
  3 50       11  
  3 100       6  
  3         11  
572 3         5 {
573             # check a map is defined, and there are 3+ players
574 3 100       10 if ($self->phase eq 'Setup')
    100          
    50          
575             {
576 1 50       5 die "The Setup phase cannot end until a map has been defined (MD)\n"
577             unless defined $self->map;
578              
579             die "The Setup phase cannot end until there are at least 3 players\n"
580 1 50       4 unless @{$self->players} >= 3;
  1         4  
581             }
582             elsif ($self->phase eq 'Deployment')
583             {
584 1         3 my $player = $self->player;
585 1 50 33     4 die "The Deployment phase cannot end until all players have gone $player\n"
586             unless $self->round == 2 && $self->player->number eq "1";
587             }
588             elsif ($self->phase eq 'Play')
589             {
590 1 50       4 die "The Play phase cannot end until a player has won the game\n"
591             unless defined $self->player_victory_check;
592             }
593              
594 3         12 return [ { PE => {phase => $self->phase} }, @{$self->phase_start} ];
  3         12  
595             }
596              
597 17 50   17 0 72 sub round_start ($self, $args = {})
  17 50       45  
  17 50       32  
  17         47  
598 17         30 {
599 17         88 return [ { RS => {round => ++$self->{round}} }, @{$self->turn_start} ];
  17         52  
600             }
601              
602 16 50   16 0 51 sub round_end ($self, $args = {})
  16 50       40  
  16 50       34  
  16         48  
603 16         30 {
604 16         23 $_->actions_clear for (@{$self->players});
  16         31  
605              
606             # 2 round limit on deployment phase
607 16 100 100     54 if ($self->phase eq 'Deployment' && $self->round == 2)
608             {
609 1         4 return [ { RE => {round => $self->round}}, @{$self->phase_end} ];
  1         4  
610             }
611 15         52 return [ { RE => {round => $self->round}}, @{$self->round_start} ];
  15         56  
612             }
613              
614 65 50   65 0 217 sub turn_start ($self, $args = {})
  65 50       157  
  65 50       102  
  65         168  
615 65         98 {
616 65 50       190 die "cannot start turn during setup\n" if $self->phase eq 'Setup';
617              
618 65 100 100     160 if ($self->turn == 0)
    100 100        
    100 100        
    100 100        
619             {
620 2         5 $self->{turn_index}++;
621             }
622             # if its the last players turn in deployment
623             # & they haven't deployed 2 settlements
624             # it's their turn again
625             elsif ($self->phase eq 'Deployment'
626 7         15 && $self->turn == @{$self->players}
627 2         6 && @{$self->player->settlements} == 1)
628             {
629 1         3 $self->{turn_index} = $self->{turn_index};
630             }
631             # if its deployment and the current player
632             # has deployed 2 settlements
633             # switch to the previous player
634             elsif ($self->phase eq 'Deployment'
635 6         16 && @{$self->player->settlements} == 2)
636             {
637 3         7 $self->{turn_index}--;
638             }
639             elsif ($self->phase eq 'Play'
640 56         155 && $self->{turn_index} + 1 == @{$self->players})
641             {
642 14         34 $self->{turn_index} = 0;
643             }
644             else
645             {
646 45         81 $self->{turn_index}++;
647             }
648              
649 65         243 my @actions = ({ TS => {player => $self->player->number}});
650 65         258 return \@actions;
651             }
652              
653 67 50   67 0 207 sub turn_end ($self, $args = {})
  67 50       302  
  67 50       103  
  67         171  
654 67         83 {
655 67         139 my $player_number = $args->{player};
656              
657 67 100       205 die "It is not Player $player_number\'s turn!\n"
658             unless $self->is_players_turn($player_number);
659              
660 66 100 100     235 die "Player $player_number hasn't rolled the dice yet\n"
661             unless $self->phase eq 'Deployment' || $self->player->has_rolled_dice;
662              
663 65 100 66     175 die "Player has not built a road and a settlement\n"
      100        
664             if $self->phase eq 'Deployment'
665             && !($self->player->has_built_road && $self->player->has_built_settlement);
666              
667 64         120 my @actions = ();
668              
669 64 100       189 if ($self->robber->active)
670             {
671 2         7 $self->robber->deactivate;
672 2         6 push @actions, { RD => undef };
673             }
674              
675             # clear outstanding events, open trades
676 64         184 $self->trades_clear;
677 64         150 undef $self->{monopoly};
678 64         149 undef $self->{road_building};
679 64         96 undef $self->{year_of_plenty};
680              
681 64         154 push @actions, { TE => {player => $self->player->number }};
682              
683             # if it's the last players turn
684             # or its development phase & the current player has two properties
685             # and it's player #1
686 64 100 100     201 if (($self->turn == @{$self->players}
  64   100     154  
      100        
      66        
      100        
      100        
      66        
687             && $self->phase eq 'Play')
688             || ($self->phase eq 'Deployment'
689 8         16 && @{$self->player->properties} == 2
690             && $self->turn == 1)
691             || ($self->phase eq 'Deployment'
692 7         15 && @{$self->player->properties} == 1
693 4         8 && $self->turn == @{$self->players}))
694             {
695 16         31 push @actions, @{$self->round_end};
  16         81  
696             }
697             # else move to the next player
698             else
699             {
700 48         66 push @actions, @{$self->turn_start};
  48         124  
701             }
702 64         216 return \@actions;
703             }
704              
705 47 50   47 0 155 sub player_victory_check ($self, $args = {})
  47 50       123  
  47 50       85  
  47         145  
706 47         83 {
707             my @players_by_vps = sort {
708 47         86 $b->victory_points_count <=> $a->victory_points_count } @{$self->players};
  195         580  
  47         167  
709              
710 47 100       164 return $players_by_vps[0] if $players_by_vps[0]->victory_points_count >= 10;
711             }
712              
713 5 50   5 0 14 sub player_add ($self, $args = {})
  5 50       11  
  5 50       6  
  5         12  
714 5         6 {
715 5 50       14 die "cannot add player outside of setup phase\n" unless $self->phase eq 'Setup';
716             die "cannot add player as max players has been reached\n"
717 5 100       12 unless $self->{max_players} > @{$self->players};
  5         13  
718              
719 4         7 my $number = @{$self->players} + 1;
  4         9  
720 4         26 my $player = Catan::Game::Player->new({number => "$number"});
721 4         11 push @{$self->players}, $player;
  4         10  
722 4         15 return [{PA => {player => $player->number} }];
723             }
724              
725 56 50   56 0 200 sub dice_roll ($self, $args)
  56 50       158  
  56         101  
  56         92  
726 56         88 {
727 56         149 my $player_number = $args->{player};
728              
729 56 50       162 die "It is not Player $player_number\'s turn\n"
730             unless $self->is_players_turn($player_number);
731              
732 56         164 my $dice_roll = $self->player->roll_dice($args->{result});
733 56         163 my @actions = ({ DR => {player => $self->player->number, result => $dice_roll} });
734              
735             # trigger robber action
736 56 100       157 if ($dice_roll == 7)
737             {
738 5         10 push @actions, @{$self->robber_activate({from_7 => 1})};
  5         28  
739             }
740             else
741             {
742 51         75 push @actions, @{$self->resource_production({resource_number => $dice_roll})};
  51         225  
743             }
744 56         203 return \@actions;
745             }
746              
747 51 50   51 0 141 sub resource_production ($self, $args)
  51 50       114  
  51         77  
  51         89  
748 51         54 {
749 51         100 my %resources = ();
750 51         189 my $tiles = $self->map->tiles_by_resource_number($args->{resource_number});
751              
752 51         112 for my $tile (@$tiles)
753             {
754             # the robber prevents resource production
755 85 100       272 next if $self->robber->location->uuid eq $tile->uuid;
756              
757 68         132 for my $player (@{$self->players})
  68         147  
758             {
759 272         341 for my $property (@{$player->properties})
  272         712  
760             {
761 708 100       1923 if ($property->location->is_adjacent($tile))
762             {
763             # update the player and bank resource amounts
764 108 100       421 my $amount = $property->isa('Catan::Asset::Settlement') ? 1 : 2;
765 108 50       389 next unless my $resource = $tile->yields($amount);
766 108         346 $resources{$player->number}{$resource->code} += $amount;
767             }
768             }
769             }
770             }
771 51         79 for my $player (@{$self->players})
  51         105  
772             {
773 204 100       524 if (exists $resources{$player->number})
774             {
775             my $trade = Catan::Game::Trade->new(
776 73         183 $self->bank, $self->players, {$player->number => $resources{$player->number}}, 1);
777 73         281 my $results = $trade->execute;
778 73         120 for (keys %{$results->{$self->bank->number}})
  73         174  
779             {
780 76         182 $resources{$self->bank->number}{$_} += $results->{$self->bank->number}{$_};
781             }
782             }
783             }
784 51         300 return [{ RP => {resources => \%resources} }];
785             }
786              
787             # during deployment each player collects adjacent resources for their 2nd property
788 4 50   4 0 17 sub resource_production_deployment ($self, $intersection)
  4 50       29  
  4         8  
  4         8  
789 4         8 {
790 4         10 my %resources = ();
791              
792 4         8 for my $tile (@{$self->map->tiles_by_intersection($intersection)})
  4         11  
793             {
794             # update the player and bank resource amounts
795 12         62 my $resource = $tile->yields(1);
796             # sea tiles don't give out!
797 12 50       30 next unless $resource;
798              
799 12         31 $resources{$self->player->number}{$resource->code} += 1;
800             }
801 4         18 my $trade = Catan::Game::Trade->new($self->bank, $self->players, \%resources, 1);
802 4         18 $trade->execute;
803 4         73 return [{ RP => {resources => \%resources} }];
804             }
805              
806 10 50   10 0 40 sub trade_offer ($self, $args)
  10 50       32  
  10         22  
  10         18  
807 10         19 {
808 10         17 my $offering_player = $args->{player};
809 10         20 my $details = $args->{resources};
810 10   33     32 my $uuid = $args->{uuid} || Data::UUID->new->create_str;
811              
812 10 50       52 die "Offer is not for current player!\n"
813             unless grep($self->player->number == $_, keys %$details);
814              
815 10 50       51 die "Offer does not include offering player!\n"
816             unless grep($offering_player == $_, keys %$details);
817              
818 10 50       31 die "Player $offering_player hasn't rolled the dice yet\n"
819             unless $self->player->has_rolled_dice;
820              
821 10 50       36 die "UUID $uuid is not unique!\n" if exists $self->trades->{$uuid};
822              
823 10         32 my $trade = Catan::Game::Trade->new($self->bank, $self->players, $details);
824 10         57 my $trade_offer = {
825             trade => $trade,
826             uuid => $uuid,
827             player => $offering_player,
828             };
829 10         42 $self->trade_add($trade_offer);
830 10         62 return [{TO => { player => $offering_player, uuid => $uuid, resources => $details}}];
831             }
832              
833 5 50   5 0 18 sub trade_bank ($self, $args)
  5 50       14  
  5         9  
  5         9  
834 5         5 {
835 5         10 my $player_number = $args->{player};
836 5         10 my $details = $args->{resources};
837              
838 5 50 33     20 die "Offer is not for current player!\n"
839             unless grep($self->player->number == $_, keys %$details)
840             && $player_number == $self->player->number;
841              
842 5         14 my $trade = Catan::Game::Trade->new($self->bank, $self->players, $details);
843 5         17 my $resources = $trade->execute;
844 5         45 return [{TB => {resources => $resources} }];
845             }
846              
847 10 50   10 0 32 sub trade_add ($self, $trade)
  10 50       26  
  10         20  
  10         19  
848 10         11 {
849 10         39 $self->{trades}{$trade->{uuid}} = $trade;
850             }
851              
852 64 50   64 0 159 sub trades_clear ($self)
  64 50       159  
  64         210  
853 64         78 {
854 64         160 $self->{trades} = {};
855             }
856              
857 59     59 0 276 sub trades { $_[0]->{trades} }
858              
859 9 50   9 0 35 sub trade_accept ($self, $args)
  9 50       35  
  9         22  
  9         14  
860 9         12 {
861 9         23 my $player_number = $args->{player};
862 9   50     29 my $uuid = $args->{uuid} || '';
863              
864             die "Player $player_number does not have an active trade with uuid: $uuid\n"
865             unless $player_number
866             && $uuid && exists $self->trades->{$uuid}
867             && $self->trades->{$uuid}{player} != $player_number
868 9 50 33     76 && $self->trades->{$uuid}{trade}->resources($player_number);
      33        
      33        
      33        
869              
870 9         24 my @actions = ();
871 9         47 push @actions, { TA => {player => $player_number, uuid => $uuid} };
872 9         30 my $trade = $self->trades->{$uuid}{trade};
873 9         31 my $details = $trade->execute;
874 9         41 push @actions, { TR => { resources => $details }};
875 9         24 delete $self->trades->{$uuid};
876 9         77 return \@actions;
877             }
878              
879 1 50   1 0 6 sub trade_cancel ($self, $args)
  1 50       4  
  1         3  
  1         2  
880 1         2 {
881 1         2 my $player_number = $args->{player};
882 1   50     6 my $uuid = $args->{uuid} || '';
883              
884             die "Player $player_number does not have an active trade with uuid: $uuid\n"
885             unless $player_number
886             && $uuid && exists $self->trades->{$uuid}
887             && $self->trades->{$uuid}{player} == $player_number
888 1 50 33     15 && $self->trades->{$uuid}{trade}->resources($player_number);
      33        
      33        
      33        
889              
890 1         5 delete $self->trades->{$uuid};
891 1         7 return [{TC => {player => $player_number, uuid => $uuid}}];
892             }
893              
894 1 50   1 0 6 sub starter_map ($self)
  1 50       5  
  1         3  
895 1         1 {
896 1         7 $self->action('MD', Catan::Map->_starter);
897             }
898              
899 1 50   1 0 5 sub random_map ($self)
  1 50       4  
  1         2  
900 1         2 {
901 1         9 $self->action('MD', Catan::Map->_random);
902             }
903              
904 3 50   3 0 11 sub map_define ($self, $map)
  3 50       21  
  3         6  
  3         8  
905 3         4 {
906 3 50       14 die "map can only be defined during setup phase\n" unless $self->phase eq 'Setup';
907 3         21 $self->{map} = Catan::Map->new({type => 'custom', map => $map});
908 3         481 return [{MD => $map}];
909             }
910              
911 245     245 0 1108 sub robber { $_[0]->{robber} }
912 584     584 0 2063 sub players{ $_[0]->{players} }
913 774     774 0 3056 sub player { $_[0]->{players}[$_[0]->{turn_index}] }
914 392     392 0 1211 sub bank { $_[0]->{bank} }
915 127     127 0 705 sub map { $_[0]->{map} }
916 697     697 0 3642 sub phase { $phases[$_[0]->{phase_index}] }
917 27     27 0 1002 sub round { $_[0]->{round} }
918 144     144 0 422 sub turn { $_[0]->{turn_index} + 1 }
919              
920 12 50   12 0 47 sub player_by_number ($self, $number)
  12 50       27  
  12         17  
  12         16  
921 12         14 {
922 12 50       30 die "player_by_number requires a number argument!\n" unless $number;
923 12         19 my @player = grep ($number == $_->number, @{$self->players});
  12         30  
924 12 50       34 die "No players with nnumber $number found!\n" unless @player;
925 12         27 return $player[0];
926             }
927              
928 189 50   189 0 488 sub is_players_turn ($self, $number)
  189 50       433  
  189         276  
  189         230  
929 189         236 {
930 189 50       410 die "is_players_turn requires a number argument!\n" unless $number;
931 189         447 return $self->player->number == $number;
932             }
933              
934             sub _log
935             {
936 240     240   420 my ($self, $msgs) = @_;
937 240 50 33     1212 die "log requires an arrayref of msgs\n" unless $msgs && ref $msgs eq 'ARRAY';
938 240 50       630 return unless my $fh = $self->{log};
939 240         3632 say $fh encode_json($_) for @$msgs;
940             }
941              
942             1;
943              
944             __END__