File Coverage

lib/Settlers/Game.pm
Criterion Covered Total %
statement 624 659 94.6
branch 242 416 58.1
condition 108 199 54.2
subroutine 69 72 95.8
pod 0 52 0.0
total 1043 1398 74.6


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