File Coverage

blib/lib/Games/Euchre.pm
Criterion Covered Total %
statement 18 203 8.8
branch 0 44 0.0
condition 0 25 0.0
subroutine 6 31 19.3
pod 25 25 100.0
total 49 328 14.9


line stmt bran cond sub pod time code
1             package Games::Euchre;
2              
3             =head1 NAME
4              
5             Games::Euchre - Euchre card game for humans and computers
6              
7             =head1 SYNOPSIS
8              
9             Simply run my game wrapper:
10              
11             % euchre.pl
12              
13             or write your own:
14              
15             use Games::Euchre;
16             use Games::Euchre::AI::Simple;
17             use Games::Euchre::AI::Human;
18              
19             my $game = Games::Euchre->new();
20             foreach my $i (1..3) {
21             $game->setAI($i, Games::Euchre::AI::Simple->new());
22             }
23             $game->setAI(4, Games::Euchre::AI::Human->new());
24             $game->playGame();
25             my @scores = sort {$b <=> $a} $game->getScores();
26             print("The winner is " . $game->getWinner()->getName() . " with a score of " .
27             "$scores[0] to $scores[1]\n");
28              
29             =head1 DESCRIPTION
30              
31             This software implements the card game of Euchre. The game is played
32             with four players composing two teams. Any of the four players can be
33             human or computer players, but more than one human is not well
34             supported yet.
35              
36             The Games::Euchre::AI module implements a simple framework for adding
37             new classes of human interfaces or computer opponents. I recomment
38             that AI writers use Games::Euchre::AI::Simple (a REALLY dumb computer
39             opponent) as starting point.
40              
41             Aside from the ::AI class and its descendents, this package also
42             implements the following classes: Games::Euchre::Team,
43             Games::Euchre::Player and Games::Euchre::Trick.
44              
45             =cut
46              
47             require 5.005_62;
48 1     1   841 use strict;
  1         2  
  1         36  
49 1     1   6 use warnings;
  1         1  
  1         31  
50 1     1   1034 use Games::Cards;
  1         6134  
  1         30  
51 1     1   610 use Games::Euchre::Team;
  1         3  
  1         24  
52 1     1   553 use Games::Euchre::Player;
  1         3  
  1         25  
53 1     1   553 use Games::Euchre::Trick;
  1         3  
  1         2679  
54              
55             our $VERSION = '1.02';
56              
57             =head1 CLASS METHODS
58              
59             =over 4
60              
61             =item new
62              
63             Create and initialize a new Euchre game.
64              
65             =cut
66              
67             sub new {
68 0     0 1   my $pkg = shift;
69              
70 0           my %values = (
71             9 => 9,
72             10 => 10,
73             "J" => 11,
74             "Q" => 12,
75             "K" => 13,
76             "A" => 14,
77             );
78            
79 0           my $self = bless({
80             game => Games::Cards::Game->new({cards_in_suit => \%values}),
81             notrump => undef,
82             hangdealer => undef,
83             winningScore => 10,
84             trump => undef,
85             deck => undef,
86             blind => undef,
87             dealer => undef,
88             bidder => undef,
89             players => [],
90             teams => [],
91             }, $pkg);
92 0           $self->resetGame();
93 0           return $self;
94             }
95              
96             =back
97              
98             =head1 INSTANCE METHODS
99              
100             =head2 Pre-Game methods
101              
102             =over 4
103              
104             =cut
105              
106             =item enableHangDealer
107              
108             Turns on the hang-the-dealer game option. It is off by default. If
109             on, this means that the dealer may not pass in the second bidding
110             round. Otherwise, the deal passes to the next player and bidding
111             begins anew.
112              
113             =cut
114              
115             sub enableHangDealer {
116 0     0 1   my $self = shift;
117 0           $self->{hangdealer} = 1;
118 0           return $self;
119             }
120              
121             =item enableNoTrump
122              
123             Turns on the no-trump game option. It is off by default. If on, this
124             means that in the second round of bidding, players may declare "No
125             trump".
126              
127             =cut
128              
129             sub enableNoTrump {
130 0     0 1   my $self = shift;
131 0           $self->{notrump} = 1;
132 0           return $self;
133             }
134              
135             =item setAI INDEX AI_OBJECT
136              
137             Tells the game to use the specified AI instance to control the player
138             of the given index. The index must be an integer between 1 and 4.
139             The AI instance must inherit from Games::Euchre::AI.
140              
141             =cut
142              
143             sub setAI {
144 0     0 1   my $self = shift;
145 0           my $index = shift; # one-based
146 0           my $ai = shift;
147 0 0 0       die "Bad index" unless ($index && $index =~ /^[1-4]$/);
148 0 0 0       die "Invalid AI instance" unless ($ai && ref($ai) &&
      0        
149             $ai->isa("Games::Euchre::AI"));
150 0           $self->{players}->[$index-1]->setAI($ai);
151 0           return $self;
152             }
153              
154             =back
155              
156             =head2 Game Methods
157              
158             =over 4
159              
160             =item resetGame
161              
162             Clear all of the state for the current game and get ready for the next one.
163              
164             =cut
165              
166             sub resetGame {
167 0     0 1   my $self = shift;
168              
169 0           $self->{dealer} = 0;
170 0   0       $self->{players} ||= [];
171 0   0       $self->{teams} ||= [];
172              
173 0           foreach my $i (1 .. 4) {
174 0   0       $self->{players}->[$i-1]
175             ||= Games::Euchre::Player->new($self, $i, "Player $i");
176              
177             # Refresh the old AI, if any
178 0           my $player = $self->{players}->[$i-1];
179 0           my $ai = $player->getAI();
180 0 0         if ($ai) {
181 0 0         if ($ai->persist()) {
182 0           $ai->reset();
183             } else {
184 0           my $pkg = ref($ai);
185 0           $player->setAI($pkg->new());
186             }
187             }
188             }
189              
190 0           foreach my $i (1 .. 2) {
191 0 0         if (!$self->{teams}->[$i-1]) {
192 0           my $team = Games::Euchre::Team->new($self, $i, "Team $i",
193             $self->{players}->[$i-1],
194             $self->{players}->[$i+1]);
195 0           foreach my $player ($team->getPlayers()) {
196 0           $player->setTeam($team);
197             }
198 0           $self->{teams}->[$i-1] = $team;
199             }
200             }
201              
202 0           foreach my $player ($self->getPlayers()) {
203 0           $player->resetGame();
204             }
205 0           foreach my $team ($self->getTeams()) {
206 0           $team->resetGame();
207             }
208 0           return $self->resetHand();
209             }
210              
211             =item resetHand
212              
213             Clear all of the state for the current hand and get ready for the next one.
214              
215             =cut
216              
217             sub resetHand {
218 0     0 1   my $self = shift;
219              
220 0           $self->{bidder} = undef;
221 0           $self->{trump} = undef;
222 0           $self->{othertrump} = undef;
223 0           $self->{deck} = Games::Cards::Deck->new($self->{game}, "Deck");
224 0           $self->{deck}->shuffle();
225 0           $self->{blind} = Games::Cards::Stack->new($self->{game}, "blind");
226 0           $self->{deck}->give_cards($self->{blind}, 4);
227              
228 0           foreach my $team ($self->getTeams()) {
229 0           $team->resetHand();
230             }
231 0           foreach my $player ($self->getPlayers()) {
232 0           $player->resetHand();
233 0           $self->{deck}->give_cards($player->getHand(), 5);
234 0           $player->getHand()->sort_by_value();
235             }
236             }
237              
238             =item playGame
239              
240             Start a game.
241              
242             =cut
243              
244             sub playGame {
245 0     0 1   my $self = shift;
246 0           $self->resetGame();
247 0           while (!$self->getWinner()) {
248 0           $self->playHand();
249             # Announce the end of the hand to AI players
250 0           $self->announceEndOfHand()
251             }
252             # Announce the end of the game to AI players
253 0           $self->announceEndOfGame();
254             }
255              
256             =item playHand
257              
258             Start a hand. Called from playGame().
259              
260             =cut
261              
262             sub playHand {
263 0     0 1   my $self = shift;
264 0           $self->resetHand();
265 0 0         if (!$self->getBid()) {
266 0           $self->announceEndOfBidding();
267 0           $self->nextDealer();
268 0           return $self;
269             }
270 0           $self->announceEndOfBidding();
271 0           my $lead = ($self->getPlayers())[$self->{dealer} + 1 % 4];
272 0           foreach my $trickNum (1 .. 5) {
273 0           my $trick = $self->getNewTrick($lead, $trickNum);
274 0           for (1 .. $self->getPlayers()) {
275 0           $trick->play();
276             }
277 0           $trick->recordTrick();
278 0           my $winner = $trick->getWinner();
279 0           $self->announceEndOfTrick($trick);
280 0           $lead = $winner;
281             }
282 0           $self->scoreHand();
283              
284 0           $self->nextDealer();
285 0           return $self;
286             }
287              
288             =item getBid
289              
290             Called from playHand().
291              
292             =cut
293              
294             sub getBid {
295 0     0 1   my $self = shift;
296            
297             #print "Blind shows " . $self->{blind}->top_card()->truename() . "\n"
298             # if ($self->{debug});
299 0           my @players = $self->getPlayers();
300 0           my $lastbid = 2*@players;
301 0           for (my $turn = 1; $turn <= $lastbid; $turn++) {
302 0           my $index = ($self->{dealer} + $turn) % @players;
303 0           my $trump = $players[$index]->bid($turn);
304 0 0         if (defined $trump) {
305 0           $self->{bidder} = $index;
306 0           $players[$index]->setBid();
307 0 0         if ($trump =~ s/A//) {
308 0           $players[$index]->setAlone();
309             }
310 0           $self->setTrump($trump);
311 0 0         if ($turn <= @players) {
312             # First round, dealer trades for turned card
313 0           $players[$self->{dealer}]->pickItUp();
314             }
315             #print($players[$index]->getName() . " called $trump for trump" .
316             # ($players[$index]->wentAlone() ? ", alone" : "") . "\n")
317             # if ($self->{debug});
318 0           last;
319             }
320             }
321 0 0         if (!defined $self->{trump}) {
322 0           return undef; # signal that all players passed
323             }
324 0           return $self;
325             }
326              
327             =item nextDealer
328              
329             Called from playHand().
330              
331             =cut
332              
333             sub nextDealer {
334 0     0 1   my $self = shift;
335 0           $self->{dealer}++;
336 0           $self->{dealer} %= $self->getPlayers();
337 0           return $self;
338             }
339              
340             =item setTrump TRUMPSUIT
341              
342             Records the trump suit for this hand. Also computes the suit of the
343             left jack for convenience. No-trump is handled correctly. Called
344             from getBid().
345              
346             =cut
347              
348             sub setTrump {
349 0     0 1   my $self = shift;
350 0           my $trump = shift;
351 0           $self->{trump} = $trump;
352 0           my %othertrump = (
353             D => "H",
354             C => "S",
355             H => "D",
356             S => "C",
357             N => "",
358             );
359 0           $self->{othertrump} = $othertrump{$trump};
360 0           return $self;
361             }
362              
363             =item getNewTrick LEADPLAYER TURNNUMBER
364              
365             Instantiate and return a new Games::Euchre::Trick object. Called from playHand().
366              
367             =cut
368              
369             sub getNewTrick {
370 0     0 1   my $self = shift;
371 0           my $lead = shift;
372 0           my $number = shift; # 1-based
373 0           return Games::Euchre::Trick->new($self, $lead, "Trick $number", $number);
374             }
375              
376             =item scoreHand
377              
378             At the end of a hand, update the scores for the teams.
379              
380             =cut
381              
382             sub scoreHand {
383 0     0 1   my $self = shift;
384 0           my @scores = $self->computeHandScores();
385 0           foreach my $team ($self->getTeams()) {
386 0           $team->addScore(shift @scores);
387             }
388 0           return $self;
389             }
390              
391             =item computeHandScores
392              
393             At the end of a hand, compute how many points each team deserves to
394             gain for the tricks they won. Returns an array of these score
395             increments. This method does not record any changes at all. Called
396             by scoreHand().
397              
398             =cut
399              
400             sub computeHandScores {
401 0     0 1   my $self = shift;
402              
403 0           my %scoreMap = ('win' => 1, 'euchre' => 2,
404             'all' => 2, 'alone' => 4);
405 0 0         return map {$scoreMap{$_} || 0} $self->computeWinTypes();
  0            
406             }
407              
408             =item computeWinTypes
409              
410             At the end of a hand, compute what type of result each team deserves
411             to gain for the tricks they won: one of 'win', 'all', 'alone', or
412             'euchre'. Returns an array of these win types increments. This
413             method does not record any changes at all. Called by
414             computeHandScores().
415              
416             =cut
417              
418             sub computeWinTypes {
419 0     0 1   my $self = shift;
420              
421 0           my @winTypes = ();
422 0           foreach my $team ($self->getTeams()) {
423 0           my $tricks = $team->getTricks();
424 0           my $win = "";
425 0 0         if ($tricks >= 3) {
426 0           $win = "win";
427 0 0         if (!$team->isBidder()) {
    0          
428 0           $win = "euchre";
429             } elsif ($tricks >= 5) {
430 0           $win = "all";
431 0 0         if ($team->wentAlone()) {
432 0           $win = "alone";
433             }
434             }
435             }
436 0           push @winTypes, $win;
437             }
438 0           return @winTypes;
439             }
440              
441             =item announceEndOfBidding
442              
443             Tell AIs the results of the bidding.
444              
445             =cut
446              
447             sub announceEndOfBidding {
448 0     0 1   my $self = shift;
449            
450 0           foreach my $player ($self->getPlayers()) {
451 0 0         if ($player->getAI()) {
452 0 0         my $state = {
453             name => $player->getName(),
454             names => {$self->getPlayerNames()},
455             number => $player->getNumber(),
456             trump => $self->{trump},
457             dealer => $self->{dealer},
458             bidder => defined $self->{bidder} ? $self->{bidder}+1 : undef,
459             weBid => $player->getTeam()->isBidder(),
460             usAlone => $player->getTeam()->wentAlone(),
461             themAlone => $player->getTeam()->getOtherTeam()->wentAlone(),
462             debug => $self->{debug},
463             };
464 0           $player->getAI()->endOfBidding($state);
465             }
466             }
467             }
468              
469             =item announceEndOfTrick TRICK
470              
471             Tell AIs the results of the trick.
472              
473             =cut
474              
475             sub announceEndOfTrick {
476 0     0 1   my $self = shift;
477 0           my $trick = shift;
478              
479 0           my $winner = $trick->getWinner();
480 0           my $winCard = $trick->getPlayerIndex($winner);
481 0           foreach my $player ($self->getPlayers()) {
482 0 0         if ($player->getAI()) {
483 0           my $state = {
484             name => $player->getName(),
485             names => {$self->getPlayerNames()},
486             number => $player->getNumber(),
487 0           played => [map {$_->truename()} $trick->getCards()],
488 0           playedBy => [map {$_->getNumber()} $trick->getPlayers()],
489             myCard => $trick->getPlayerIndex($player),
490             winCard => $winCard,
491             winner => $winner->getNumber(),
492             debug => $self->{debug},
493             };
494 0           $player->getAI()->endOfTrick($state);
495             }
496             }
497             }
498              
499             =item announceEndOfHand
500              
501             Tell AIs the results of the hand.
502              
503             =cut
504              
505             sub announceEndOfHand {
506 0     0 1   my $self = shift;
507            
508             # Retrieve the wintype of the winning team
509 0           my ($winType) = grep {$_} $self->computeWinTypes();
  0            
510 0           foreach my $player ($self->getPlayers()) {
511 0 0         if ($player->getAI()) {
512 0           my $state = {
513             name => $player->getName(),
514             names => {$self->getPlayerNames()},
515             number => $player->getNumber(),
516             ourTricks => $player->getTeam()->getTricks(),
517             theirTricks => $player->getTeam()->getOtherTeam()->getTricks(),
518             winType => $winType,
519             ourScore => $player->getTeam()->getScore(),
520             theirScore => $player->getTeam()->getOtherTeam()->getScore(),
521             winScore => $self->{winningScore},
522             debug => $self->{debug},
523             };
524 0           $player->getAI()->endOfHand($state);
525             }
526             }
527             }
528              
529             =item announceEndOfGame
530              
531             Tell AIs the results of the game.
532              
533             =cut
534              
535             sub announceEndOfGame {
536 0     0 1   my $self = shift;
537              
538 0           foreach my $player ($self->getPlayers()) {
539 0 0         if ($player->getAI()) {
540 0           my $state = {
541             name => $player->getName(),
542             names => {$self->getPlayerNames()},
543             number => $player->getNumber(),
544             ourScore => $player->getTeam()->getScore(),
545             theirScore => $player->getTeam()->getOtherTeam()->getScore(),
546             debug => $self->{debug},
547             };
548 0           $player->getAI()->endOfHand($state);
549             }
550             }
551             }
552              
553             =back
554              
555             =head2 Utility/Access Methods
556              
557             =over 4
558              
559             =item getWinner
560              
561             Returns the Team object who has won the game, or undef if nobody has won yet.
562              
563             =cut
564              
565             sub getWinner {
566 0     0 1   my $self = shift;
567 0           foreach my $team ($self->getTeams()) {
568 0 0         return $team if ($team->getScore() >= $self->{winningScore});
569             }
570 0           return undef;
571             }
572              
573             =item getTeams
574              
575             Returns an array of two Team objects.
576              
577             =cut
578              
579             sub getTeams {
580 0     0 1   my $self = shift;
581 0           return @{$self->{teams}};
  0            
582             }
583              
584             =item getPlayers
585              
586             Returns an array of four Player objects.
587              
588             =cut
589              
590             sub getPlayers {
591 0     0 1   my $self = shift;
592 0           return @{$self->{players}};
  0            
593             }
594              
595             =item getPlayerNames
596              
597             Returns a hash relating player numbers to player names for all four
598             players.
599              
600             =cut
601              
602             sub getPlayerNames {
603 0     0 1   my $self = shift;
604 0           return map {$_->getNumber(), $_->getName()} $self->getPlayers();
  0            
605             }
606              
607             =item getScores
608              
609             Returns an array of current scores for the two teams. The order of
610             the returned scores is the same as the order of the returned teams in
611             the getTeams() method.
612              
613             =cut
614              
615             sub getScores {
616 0     0 1   my $self = shift;
617 0           return map {$_->getScore()} $self->getTeams();
  0            
618             }
619              
620             =item getCardSuit CARD
621              
622             Returns the suit of the given card. The left jack is reported to be
623             of the trump suit, if a trump has been declared. [This latter
624             convenience is the whole point of having this function at all and not
625             just calling CARD->suit().]
626              
627             =cut
628              
629             sub getCardSuit {
630 0     0 1   my $self = shift;
631 0           my $card = shift;
632 0           my $cardsuit = $card->suit();
633 0 0 0       if ($self->{trump} && $self->{othertrump} &&
      0        
      0        
634             $cardsuit eq $self->{othertrump} && $card->name() eq "J") {
635 0           $cardsuit = $self->{trump};
636             }
637 0           return $cardsuit;
638             }
639              
640             1;
641             __END__