Simple Game in Perl 6
Here's a little game which was a sub-game in the 1980's Commodore 64 game Legend of Blacksilver. It's a simple gambling game where you are dealt five cards in a row from a standard 52-card deck. The cards are then turned up one at a time. After each card is turned up, you have to guess whether the next card will be higher or lower in rank than the last one. Aces are high, and you lose all ties. If you guess all four right, you win.
[Update: There's a serious bug in this version, as pointed out by Brad in the comments, but I'm leaving it in the original so the comments make sense, since I'm writing these for learning purposes. A fixed version can be found at my GitLab account.]
#!/usr/bin/env perl6
use v6;
my %v = ( 2 => 2, 3 => 3, 4 => 4, 5 => 5, 6 => 6, ### 1
7 => 7, 8 => 8, 9 => 9, T => 10,
J => 11, Q => 12, K => 13, A => 14 );
my @deck := %v.keys X~ <♠ ♡ ♢ ♣>; ### 2
my $card = @deck.pick; ### 3
my $show = "$card ";
for ^4 { ### 4
say $show;
my $l; repeat { ### 5
$l = prompt 'Hi or Lo? ';
} until $l ~~ m:i/ ( h | l ) /;
my $new = @deck.pick;
$show ~= "$new ";
my $nv = %v{ $new.substr(0,1)}; ### 6
my $cv = %v{$card.substr(0,1)};
if $nv == $cv or
( $nv < $cv and $l ~~ m/:i h/ ) or ### 7
( $nv > $cv and $l !~~ m/:i h/ ) {
say $show;
say "Sorry, you lose!";
exit;
}
$card = $new;
}
say $show;
say 'You win!';
I inserted commented numbers on lines with interesting features.
(1) I create a hash of card ranks to values, to make it easy to tell whether a card is higher or lower than another.
(2) There's a lot of Perl 6 goodness here. First, I bind the array with :=
instead of assigning it with =
. I don't completely understand this yet, but I think that binds it like a Perl 5 reference, rather than copying the values. The point of that, I think, is that it means the compiler doesn't have to execute the right side until more values are needed in @deck
. I'm not sure that actually gains me anything in this case, since all the values are probably needed in the next line, but it seems like a good habit.
The X
is the "cross" operator. It takes two lists as arguments, and returns all possible combinations between them. Following it with ~
, the Perl 6 string concatenation operator, tells it to concatenate each combination as a string. So that combines each key from the hash with each of the four card suit symbols, resulting in 52 unique combos.
%v.keys
shows the way that functions can run as methods now with the dot (.) operator. I could say keys %v
as I would have in Perl 5, but I think this makes things like precedence clearer.
The <>
operator is no longer for reading lines from a file descriptor. Now it's shorthand for the qw// quoting operator, so it returns the four card suits (Unicode characters) as a list.
The game doesn't actually care about the suits, by the way, so I could have left them out, but I wanted to test Unicode in my editor and terminals.
(3) The .pick
function randomly picks one item from the array, and will not repeat items on subsequent picks. No more need to calculate random numbers and splice elements out of a separate array!
(4) for
loops have several new features, but this one doesn't use most of them. A couple things to note: you don't need parentheses around the arguments, and if you do use them, you must have whitespace between for
and the opening parenthesis. I recommend not using them. Also, the ^n
syntax means "integers from zero to n-1". It's a handy shortcut to say "loop this many times." If I needed the numbers 1-4 within the loop, I could still use the for 1..4 {
method I'd use in Perl 5, and get the number in $_
.
(5) The do/until
loop is now repeat/until
. Not much more to say about that, but check out the regex at the end of it. There are big changes in regexes in Perl 6, and that's one area where you can't do much until you learn some of them, because even fairly basic Perl 5 regexes probably won't work like you expect.
For one thing, whitespace is ignored by default, as if you used the /x modifier. For another thing, you don't put modifiers at the end anymore, and some of them have changed. Now the /i modifier goes at the beginning, as :i
, and can go either inside or outside the delimiters. Other than that, the regex isn't too different: it just makes sure the response contains either h
or l
, case-insensitive.
One last thing there: there's now a built-in prompt
routine that outputs a string and gets a response, so that replaces the old print 'question? '; my $answer = <STDIN>;
combo.
(6) There's not anything drastically new here, but note again how substr()
can be used postfix, where in Perl 5 it would have been substr($new,0,1)
.
One thing, though: you can't use whitespace in a few places where you could in Perl 5. One of those places is between objects and postfix functions and braces. So to line up those two lines, I had to put a space before $new
; putting it after $new
before or after the dot would have broken it. There's something called "unspace" you can use to get around that, but here it was simplest to put the space where I did.
(7) There's nothing really special here; it's just checking to see whether you selected 'h' as your guess, and checks that against the cards. One thing to notice, which I forgot to mention in #5: the matching operators have changed from =~
to ~~
and from !=
to !~~
.
I think that's everything that's really new, so I'll stop there. There are a couple things about it that bug me, as if there's some refactoring that could be done, so I may come back to it later. If you have any questions or suggestions, please leave a comment.
Array.pick()
doesn't do what you think it does, it never modifies the array you call it on. With no arguments it returns one item, with a positive number it returns a list of that many elements with no repeats, withWhatever
or*
it returns the whole list in randomized order.What you should have done is is have
my @deck := (...).pick(*);
, and then replacemy $card = @deck.pick;
withmy $card = @deck.shift;
. (same thing with$new
)%v.keys
calls thekeys
method, if you want to show how to use a function/subroutine as a method you would call it as%v.&keys
.Thanks for correcting that; I didn't say it very well. As you say, .pick() doesn't modify the array. That's why, if I were doing it in Perl 5, I probably would have copied @deck to a separate variable and spliced the cards out of it, to get the same non-repeating behavior without changing the original (though in this case, I don't use the deck more than once, so I guess it doesn't matter).
I hadn't thought of randomizing the deck from the start as you say with .pick(*) and then shifting cards off one at a time, but I can see how that would be more efficient, since there's probably some overhead in each .pick call, so calling it once is better than five times. Thanks!
I'll try to be more accurate about calling methods methods and not subroutines. I'm still getting used to everything being an object even though I didn't create any.
It wouldn't be more efficient, it would actually be correct.
$new
would effentially be the same as your card.Oh, you mean the lack of repetition is only guaranteed within a single call to .pick(), not multiple calls! Yes, that's definitely a problem. Thanks for straightening me out on that.