The final challenge was blackjack. The instructions said that we
could choose to implement that aces always equal 11, but that seems
insufficiently interesting: I wanted to use junctions!
To start with, a simple definition of a deck of cards. The challenge
required humanly named cards: so I started with a list of pairs, and
then crossed this list against the list of suites. Finally, "@cards =
@deck.pick( @deck.elems )" results in a random shuffle.
my @values = (
ace => 1|11,
two => 2,
three => 3,
four => 4,
five => 5,
six => 6,
seven => 7,
eight => 8,
nine => 9,
ten => 10,
jack => 10,
queen => 10,
king => 10,
);
my @suites = < spades clubs diamonds hearts >;
my @deck = ( @values X @suites ).map: {
my ($name, $value) = $^a.kv;
"$name of $^b" => $value
};
my @cards = @deck.pick( @deck.elems );
So that's my deck of cards. I first deal the initial hands:
my @dealer;
my @player;
@dealer.push( @cards.shift );
@player.push( @cards.shift );
@dealer.push( @cards.shift );
say "DEALER:";
say @dealer[0].key; #only display the first card of dealer's hand
say "";
say "PLAYER:";
.key.say for @player;
Now we come to the main player loop. Card values are junctions, so I
don't need to worry about calculating all possible values of hands that
include aces. However, I do need to remember to not test for "bust"
using a "greater-than" test. Because of the logic of "any" junctions,
I need to check only for "less than 21" is not bust (and == 21 is WIN):
my $choice = "hit";
my $player_value = [+] @player.map: { .value };
while ($choice ~~ /h/) {
my $card = @cards.shift;
@player.push( $card );
say $card.key;
$player_value += $card.value;
say "current value is { $player_value.perl }";
if $player_value == 21 {
say "congradulations, you win!";
$choice = "s";
}
elsif $player_value < 21 {
say "hit (h) or stay (s)";
$choice = "stay" unless $player_value < 16;
say $choice;
#TODO: read STDIN
}
else {
say "Sorry, you bust!";
$choice = "s";
}
}
Note that my version of Rakudo doesn't implement "last" properly yet,
so I arranged my code to not need it.
Now the dealer gets to play (but only if the game is still live). A
problem here was to figure out what "player value" to use. If a player
hand is (ace, ace, eight), then possible values are 10, 20, and 30. I
need to make sure that "20" is used for all tests against the dealers
value. I couldn't figure out an analytic way to achieve this, so I brute
forced it: "max (4 .. 21).grep: { $_ == $player_value }". (I start the
list at 4 because that's the lowest possible blackjack hand).
Checking for a dealer win has the same problem: I can't use "less-than"
or "greater-than" tests because of possible junctional values (e.g. "ace,
six, ten" is both not-bust, and greater than any possible play hand).
say "";
if $player_value < 21 && not $player_value == 21 {
$player_value = max (4 .. 21).grep: { $_ == $player_value };
say "DEALER:";
.key.say for @dealer;
my $dealer_value = [+] @dealer.map: { .value };
my $done = 0;
while ! $done {
say "dealer value: {$dealer_value.perl}";
if $dealer_value == any( $player_value ^.. 21) {
say "you loose!";
$done = 1;
}
elsif $dealer_value < 21 {
my $card = @cards.shift;
@dealer.push( $card );
say $card.key;
$dealer_value += $card.value;
}
else {
say "dealer bust: you win!";
$done = 1;
}
}
}
And so that's it. Junctions work, but I can't help feeling that we need
some more language features to help with the "collapsing" of a junction
to its appropriate value -- and that includes a "partial collapse" that,
for example, excludes all values greater than 21 after I've checked that
there'd be at least one value left over. Hmm, perhaps that could be done
with a subset type...