File Coverage

blib/lib/Games/Hanabi.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1 1     1   25687 use strict;
  1         2  
  1         47  
2 1     1   7 use warnings;
  1         1  
  1         71  
3              
4             package Games::Hanabi;
5             # ABSTRACT: 'hanabi' card game
6             $Games::Risk::VERSION = '0.001';
7              
8 1     1   5 use List::Util qw(shuffle);
  1         6  
  1         92  
9 1     1   4 use Carp;
  1         0  
  1         46  
10 1     1   184 use Clone qw(clone);
  0            
  0            
11             use Data::Dumper;
12              
13             my @colors = qw(R G B Y W);
14             my @numbers = qw(1 2 3 4 5);
15             my %revealed_cache;
16              
17             # Needs players and variants
18             sub new {
19             my ( $class, %params ) = @_;
20             $params{players} //= 2;
21             $params{derive} //= 1;
22              
23             # R = red
24             # G = Green
25             # B = Blue
26             # W = White
27             # Y = Yellow
28             # M = Multicolor
29             my @cards = qw(
30             R1 R1 R1 R2 R2 R3 R3 R4 R4 R5
31             G1 G1 G1 G2 G2 G3 G3 G4 G4 G5
32             B1 B1 B1 B2 B2 B3 B3 B4 B4 B5
33             Y1 Y1 Y1 Y2 Y2 Y3 Y3 Y4 Y4 Y5
34             W1 W1 W1 W2 W2 W3 W3 W4 W4 W5
35             );
36             my @deck;
37             my ( %starting_count, %public_count );
38              
39             for my $card (@cards) {
40             my ( $color, $number ) = split //, $card;
41             push @deck, { color => $color, number => $number };
42             $starting_count{$color}{$number}++;
43             $public_count{$color}{$number} = 0;
44             }
45             @deck = shuffle @deck;
46              
47             my $self = {
48             players => $params{players},
49             derive => $params{derive},
50             deck => \@deck,
51             starting_deck => clone( \@deck ),
52             starting_count => \%starting_count,
53             public_count => \%public_count,
54             turn => int( rand( $params{players} ) ),
55             hints => 8,
56             bombs => 0,
57             debug => $params{debug},
58             score => 0,
59             discards => [],
60             top_card => {
61             'R' => 0,
62             'G' => 0,
63             'B' => 0,
64             'Y' => 0,
65             'W' => 0,
66             },
67             };
68              
69             bless $self, $class;
70              
71             # Draw starting hands
72             my $starting_cards = 4;
73             if ( $params{players} < 4 ) {
74             $starting_cards = 5;
75             }
76             for my $i ( 0 .. $params{players} - 1 ) {
77             for ( 1 .. $starting_cards ) {
78             $self->_draw($i);
79             }
80             }
81              
82             return $self;
83             }
84              
85             # Draw a new card and add it to the player's hand
86             sub _draw {
87             my $self = shift;
88             my ($player) = @_;
89             return if @{ $self->{deck} } == 0; # empty deck
90             my $card = shift @{ $self->{deck} };
91             $card->{known_information} =
92             { number_score => 0, color_score => 0, score => 0 };
93             push @{ $self->{hands}[$player] }, $card;
94              
95             # Deck just got empty, begin the end game
96             if ( scalar @{ $self->{deck} } == 0 ) {
97             $self->{countdown} = $self->{players};
98             }
99             return;
100             }
101              
102             # The game state consists of what each player is holding,
103             # what cards are in play, the deck, and what information is known about
104             # the cards players are holding
105             sub get_game_state {
106             my $self = shift;
107             return {
108             players => $self->{players},
109             deck => $self->{deck},
110             discards => $self->{discards},
111             hands => $self->{hands},
112             turn => $self->{turn},
113             hints => $self->{hints},
114             bombs => $self->{bombs},
115             top_card => $self->{top_card},
116             score => $self->{score},
117             countdown => $self->{countdown},
118             };
119             }
120              
121             # Return an array of valid moves.
122             # actions: play, discard, hint
123             # For play / discard, the 'card' value will be an int representing the index of the card in hand
124             # For hint, also pass 'player' index, and the 'value' which is either a number of color letter.
125             sub get_valid_moves {
126             my $self = shift;
127             return if defined $self->{countdown} && $self->{countdown} == 0;
128             return if $self->{bombs} == 3;
129             return if $self->{score} == 25;
130             my @moves;
131             my $current_player = $self->{turn};
132              
133             # Play or discard a card
134             for my $i ( 0 .. scalar @{ $self->{hands}[$current_player] } - 1 ) {
135             push @moves, { action => 'play', index => $i };
136             push @moves, { action => 'discard', index => $i };
137             }
138              
139             # Give a hint
140             if ( $self->{hints} ) {
141             for my $player ( 0 .. $self->{players} - 1 ) {
142             next if $player == $current_player;
143             my %colors;
144             my %numbers;
145             for my $i ( 0 .. scalar @{ $self->{hands}[$player] } - 1 ) {
146             my $card = $self->{hands}[$player][$i];
147             $colors{ $card->{color} } = 1;
148             $numbers{ $card->{number} } = 1;
149             }
150             for my $c ( sort keys %colors ) {
151             push @moves,
152             { action => 'hint', player => $player, hint => $c };
153             }
154             for my $n ( sort keys %numbers ) {
155             push @moves,
156             { action => 'hint', player => $player, hint => $n };
157             }
158             }
159             }
160              
161             return @moves;
162             }
163              
164             # Perform an action. Return 1 for game on, 0 for game over.
165             sub take_action {
166             my $self = shift;
167             my ($move) = @_;
168             my $current_player = $self->{turn};
169             if ( $move->{action} eq 'discard' ) {
170              
171             #print "Player: $current_player / index : $move->{index} \n";
172             my $card = $self->{hands}[$current_player][ $move->{index} ];
173             print "Player $current_player is discarding "
174             . $card->{color}
175             . $card->{number} . "\n"
176             if $self->{debug};
177             push @{ $self->{discards} }, $card;
178             splice @{ $self->{hands}[$current_player] }, $move->{index}, 1;
179             $self->{public_count}{ $card->{color} }{ $card->{number} }++;
180             if ( $self->{hints} < 8 ) {
181             $self->{hints}++;
182             }
183             $self->_draw($current_player);
184             }
185             elsif ( $move->{action} eq 'play' ) {
186             my $card = $self->{hands}[$current_player][ $move->{index} ];
187             print "Player $current_player is playing "
188             . $card->{color}
189             . $card->{number} . "\n"
190             if $self->{debug};
191             splice @{ $self->{hands}[$current_player] }, $move->{index}, 1;
192             $self->{public_count}{ $card->{color} }{ $card->{number} }++;
193              
194             if ( $self->is_valid_play($card) ) {
195             print "It worked!\n" if $self->{debug};
196             $self->{top_card}{ $card->{color} }++;
197             if ( $self->{top_card}{ $card->{color} } == 5 ) {
198             $self->{hints}++;
199             }
200             $self->{score}++;
201             }
202             else {
203             print "If did not work...\n" if $self->{debug};
204             push @{ $self->{discards} }, $card;
205             $self->{bombs}++;
206             if ( $self->{bombs} == 3 ) {
207             return 0; # game over
208             }
209             }
210             $self->_draw($current_player);
211             }
212             elsif ( $move->{action} eq 'hint' ) {
213             $self->{hints}--;
214             croak "Used up a hint when there were none to use\n"
215             if $self->{hints} < 0;
216             my $player = $move->{player};
217             print
218             "Player $current_player is giving a hint of $move->{hint} to player $player\n"
219             if $self->{debug};
220             for my $i ( 0 .. scalar @{ $self->{hands}[$player] } - 1 ) {
221             my $card = $self->{hands}[$player][$i];
222             if ( $move->{hint} =~ /\d/ ) {
223             if ( $card->{number} == $move->{hint} ) {
224             print "Found a match!\n" if $self->{debug};
225             for my $number (@numbers) {
226             if (
227             !defined $card->{known_information}{number}{$number}
228             )
229             {
230             $card->{known_information}{number_score}++;
231             $card->{known_information}{number}{$number} = 0;
232             }
233             }
234             $card->{known_information}{number}{ $move->{hint} } = 1;
235             $card->{known_information}{number_score} = 10;
236             }
237             else {
238             if ( !defined $card->{known_information}{number}
239             { $move->{hint} } )
240             {
241             $card->{known_information}{number_score}++;
242             $card->{known_information}{number}{ $move->{hint} } = 0;
243             }
244             }
245             }
246             elsif ( $move->{hint} =~ /[a-z]/i ) {
247             if ( $card->{color} eq $move->{hint} ) {
248             print "Found a match!\n" if $self->{debug};
249             for my $color (@colors) {
250             if (
251             !defined $card->{known_information}{color}{$color} )
252             {
253             $card->{known_information}{color_score}++;
254             $card->{known_information}{color}{$color} = 0;
255             }
256              
257             }
258             $card->{known_information}{color}{ $move->{hint} } = 1;
259             $card->{known_information}{color_score} = 10;
260             }
261             else {
262             if ( !defined $card->{known_information}{color}
263             { $move->{hint} } )
264             {
265             $card->{known_information}{color_score}++;
266             $card->{known_information}{color}{ $move->{hint} } = 0;
267             }
268             }
269             }
270             $card->{known_information}{score} =
271             $card->{known_information}{color_score} +
272             $card->{known_information}{number_score};
273             }
274             }
275             else {
276             croak "Unknown action: $move->{action}";
277             }
278              
279             # Advance the turn counter
280             $self->{turn} = ( $self->{turn} + 1 ) % $self->{players};
281             if ( defined $self->{countdown} ) {
282             $self->{countdown}--;
283             if ( $self->{countdown} == 0 ) {
284             return 0; # game over
285             }
286             }
287              
288             if ( $self->{derive} ) {
289             $self->derive_information();
290             }
291             return 1;
292             }
293              
294             sub is_valid_play {
295             my $self = shift;
296             my ($card) = @_;
297             if ( $card->{number} == $self->{top_card}{ $card->{color} } + 1 ) {
298             return 1;
299             }
300             return;
301             }
302              
303             # Is the play valid from the perspective of the player?
304             sub is_valid_known_play {
305             my $self = shift;
306             my ($card) = @_;
307             return $self->is_valid_play($card) if $self->is_card_known($card);
308              
309             # If we know just the number, see if that number if valid for all possible cards
310             if ( $self->is_number_known($card) ) {
311              
312             #print "number is known for $card->{color}$card->{number}\n";
313             for my $color (@colors) {
314             if ( not defined $card->{known_information}{color}{$color} ) {
315             if ( $card->{number} != $self->{top_card}{$color} + 1 ) {
316             return;
317             }
318             }
319             }
320             }
321             else {
322             return;
323             }
324             return 1;
325             }
326              
327             sub is_junk {
328             my $self = shift;
329             my ( $card, $known ) = @_;
330             return 1 if $card->{known_information}{is_junk};
331              
332             # A card is junk if it is known, and already played
333             if ( $known || $self->is_card_known($card) ) {
334             if ( $card->{number} <= $self->{top_card}{ $card->{color} } ) {
335             $card->{known_information}{is_junk} = 1;
336             return 1;
337             }
338              
339             # or dead.
340             elsif ( $card->{number} > $self->max_score_for_color( $card->{color} ) )
341             {
342             $card->{known_information}{is_junk} = 1;
343             return 1;
344             }
345              
346             # or we have 2 copies
347             elsif (
348             scalar(
349             grep {
350             $_->{number} == $card->{number}
351             && $_->{color} eq $card->{color}
352             } @{ $self->{hands}[ $self->{turn} ] }
353             ) > 1
354             )
355             {
356             $card->{known_information}{is_junk} = 1;
357             return 1;
358             }
359             }
360              
361             # A card is junk if a color is dead and the card has that color
362             elsif ( my $color = $self->is_color_known($card) ) {
363             if ( $self->{top_card}{$color} == $self->max_score_for_color($color) ) {
364             $card->{known_information}{is_junk} = 1;
365             return 1;
366             }
367             }
368              
369             # A card is junk if a number is dead and the card has that number
370             elsif ( my $number = $self->is_number_known($card) ) {
371             for my $color ( keys %{ $self->{top_card} } ) {
372             if ( $self->{top_card}{$color} < $number ) {
373             return;
374             }
375             }
376             $card->{known_information}{is_junk} = 1;
377             return 1;
378             }
379              
380             return;
381             }
382              
383             # What's the best score this color can get?
384             sub max_score_for_color {
385             my $self = shift;
386             my ($color) = @_;
387             my $max_score = $self->{top_card}{$color};
388             for my $i ( $max_score + 1 .. 5 ) {
389             my $matches = grep { $_->{color} eq $color && $_->{number} == $i }
390             @{ $self->{discards} };
391             my $starting = grep { $_->{color} eq $color && $_->{number} == $i }
392             @{ $self->{starting_deck} };
393             if ( $matches < $starting ) { $max_score++; }
394             }
395             return $max_score;
396             }
397              
398             # Determine information about cards in our hand based on what else we can see
399             sub derive_information {
400             my $self = shift;
401             my ($retain_cache) = @_;
402             %revealed_cache = () if !$retain_cache;
403             my $information_gained = 0;
404              
405             for my $player ( 0 .. $self->{players} - 1 ) {
406             for my $i ( 0 .. @{ $self->{hands}[$player] } - 1 ) {
407             my $card = $self->{hands}[$player][$i];
408              
409             my $color_known = $self->is_color_known($card);
410             my $number_known = $self->is_number_known($card);
411              
412             # If we know the number, try to figure out the color
413             if ( $number_known && !$color_known ) {
414              
415             # Eliminate any colors that are fully revealed
416             for my $color (@colors) {
417             if ( not defined $card->{known_information}{color}{$color} )
418             {
419             if (
420             $self->revealed_count( $player,
421             { color => $color, number => $number_known } )
422             == $self->{starting_count}{$color}{$number_known}
423             )
424             {
425             $card->{known_information}{color}{$color} = 0;
426             $card->{known_information}{color_score}++;
427             $information_gained++;
428             }
429             }
430             }
431             }
432              
433             # If we know the color try to figure out the number
434             if ( $color_known && !$number_known ) {
435              
436             # Eliminate any numbers that are fully revealed
437             for my $number (@numbers) {
438             if (
439             not defined $card->{known_information}{number}{$number}
440             )
441             {
442             if (
443             $self->revealed_count( $player,
444             { color => $color_known, number => $number } )
445             == $self->{starting_count}{$color_known}{$number}
446             )
447             {
448             $card->{known_information}{number}{$number} = 0;
449             $card->{known_information}{number_score}++;
450             $information_gained++;
451             }
452             }
453             }
454              
455             }
456              
457             # If we know it's NOT every other color, then we know its color
458             if ( !$color_known ) {
459             my $negative_colors = 0;
460             my $positive_color;
461             for my $color (@colors) {
462             if ( defined $card->{known_information}{color}{$color}
463             && !$card->{known_information}{color}{$color} )
464             {
465             $negative_colors++;
466             }
467             elsif ( !defined $card->{known_information}{color}{$color} )
468             {
469             $positive_color = $color;
470             }
471             }
472             if ( $negative_colors == 4 ) {
473             $card->{known_information}{color}{$positive_color} = 1;
474             $card->{known_information}{color_score} = 10;
475             $information_gained++;
476             }
477             }
478              
479             # If we know it's NOT every other number, then we know its number
480             if ( !$number_known ) {
481             my $negative_numbers = 0;
482             my $positive_number;
483             for my $number (@numbers) {
484             if ( defined $card->{known_information}{number}{$number}
485             && !$card->{known_information}{number}{$number} )
486             {
487             $negative_numbers++;
488             }
489             elsif (
490             !defined $card->{known_information}{number}{$number} )
491             {
492             $positive_number = $number;
493             }
494             else {
495             $positive_number = $number;
496             }
497             }
498             if ( $negative_numbers == 4 ) {
499             $card->{known_information}{number}{$positive_number} = 1;
500             $card->{known_information}{number_score} = 10;
501             $information_gained++;
502             }
503             }
504             }
505              
506             # Is there a dead number?
507              
508             for my $number (@numbers) {
509             my $total = 0;
510             my $total_revealed = 0;
511             for my $color (@colors) {
512             $total_revealed += $self->revealed_count( $player,
513             { number => $number, color => $color } );
514             $total += $self->{starting_count}{$color}{$number};
515             }
516             if ( $total == $total_revealed ) {
517             for my $i ( 0 .. @{ $self->{hands}[$player] } - 1 ) {
518             my $card = $self->{hands}[$player][$i];
519             if ( !defined $card->{known_information}{number}{$number} )
520             {
521             $card->{known_information}{number}{$number} = 0;
522             $card->{known_information}{number_score}++;
523             $information_gained++;
524             }
525             }
526             }
527             }
528              
529             # Is there a dead color?
530             for my $color (@colors) {
531             my $total = 0;
532             my $total_revealed = 0;
533             for my $number (@numbers) {
534             $total_revealed += $self->revealed_count( $player,
535             { number => $number, color => $color } );
536             $total += $self->{starting_count}{$color}{$number};
537             }
538             if ( $total == $total_revealed ) {
539             for my $i ( 0 .. @{ $self->{hands}[$player] } - 1 ) {
540             my $card = $self->{hands}[$player][$i];
541             if ( !defined $card->{known_information}{color}{$color} ) {
542             $card->{known_information}{color}{$color} = 0;
543             $card->{known_information}{color_score}++;
544             $information_gained++;
545             }
546             }
547             }
548             }
549              
550             }
551              
552             if ($information_gained) {
553              
554             # Recompute the total scores and derive again
555             for my $player ( 0 .. $self->{players} - 1 ) {
556             for my $i ( 0 .. @{ $self->{hands}[$player] } - 1 ) {
557             my $card = $self->{hands}[$player][$i];
558             $card->{known_information}{score} =
559             $card->{known_information}{number_score} +
560             $card->{known_information}{color_score};
561             }
562             }
563              
564             $self->derive_information(1);
565             }
566             return;
567             }
568              
569             # How may copies of $card does $player know about?
570             sub revealed_count {
571             my $self = shift;
572             my ( $player, $card ) = @_;
573             return $revealed_cache{$player}{ $card->{color} }{ $card->{number} }
574             if $revealed_cache{$player}{ $card->{color} }{ $card->{number} };
575              
576             # If all copies of a card are revealed, stop looking
577             my $color = $card->{color};
578             my $number = $card->{number};
579             my $count = $self->{public_count}{$color}{$number};
580             if ( $self->{starting_count}{$color}{$number} == $count ) {
581             $revealed_cache{$player}{ $card->{color} }{ $card->{number} } = $count;
582             return $count;
583             }
584              
585             # Look for cards in players' hands... even our own
586             for my $p ( 0 .. $self->{players} - 1 ) {
587             if ( $p == $player ) {
588             $count += grep {
589             $_->{known_information}{color}{$color}
590             && $_->{known_information}{number}{$number}
591             } @{ $self->{hands}[$player] };
592             }
593             else {
594             $count += grep { $_->{color} eq $color && $_->{number} == $number }
595             @{ $self->{hands}[$p] };
596             }
597             }
598             $revealed_cache{$player}{ $card->{color} }{ $card->{number} } = $count;
599             return $count;
600             }
601              
602             sub is_card_known {
603             my $self = shift;
604             my ($card) = @_;
605             return $card
606             if $card->{known_information}{score}
607             && $card->{known_information}{score} == 20;
608             return;
609             }
610              
611             sub is_color_known {
612             my $self = shift;
613             my ($card) = @_;
614             return $card->{color}
615             if $card->{known_information}{color_score}
616             && $card->{known_information}{color_score} == 10;
617             return;
618             }
619              
620             sub is_number_known {
621             my $self = shift;
622             my ($card) = @_;
623             return $card->{number}
624             if $card->{known_information}{number_score}
625             && $card->{known_information}{number_score} == 10;
626             return;
627             }
628              
629             sub print_game_state {
630             my $self = shift;
631              
632             # Print the piles
633             print "-----------------------------------------\n";
634             print "Board: ";
635             for my $color ( keys %{ $self->{top_card} } ) {
636             print $color . $self->{top_card}{$color} . " ";
637             }
638             print "\n";
639              
640             # Print the hands
641             for my $i ( 0 .. $self->{players} - 1 ) {
642             print "Player $i: ";
643             for my $card ( @{ $self->{hands}[$i] } ) {
644             print $card->{color} . $card->{number} . ' ';
645             }
646             print "\n";
647             }
648             print "\n";
649              
650             # Print the discard pile
651             print "Discards: ";
652             for my $card ( @{ $self->{discards} } ) {
653             print $card->{color} . $card->{number} . ' ';
654             }
655             print "\n";
656              
657             # Print the deck size, turns left, and bombs
658             print "Turn: " . $self->{turn};
659             print "\tHints: " . $self->{hints};
660             print "\tScore: " . $self->{score};
661             print "\tDeck: " . scalar @{ $self->{deck} };
662             print "\tBombs: " . $self->{bombs};
663             print "\tTurns Left: " . $self->{countdown} if defined $self->{countdown};
664             print "\n\n";
665             return;
666             }
667              
668             1;
669              
670             __END__