2008 Winter Scripting Games

Solution to Advanced Windows PowerShell Event 10: Blackjack!

Event 10 Solution


Windows PowerShell solution to Event 10 in the 2008 Winter Scripting Games.

Solutions are also available for VBScript and Perl.

*

Blackjack!


In the 2007 Scripting Games we tried to save the best event (that is, the most fun, and the most challenging) for last. That’s why, in 2007, the final event required competitors to write a script that could determine whether or not someone possessed ESP (extra-sensory perception). That’s also why the final event for the 2008 Winter Scripting Games required users to write a script that could play a single hand of Blackjack.

Of course, for the Scripting Guys this was also the most fun event to test. Not that reviewing a list of all the prime numbers between 1 and 200 isn’t fun; obviously it is. But to spend the day at work, sitting around and playing Blackjack? That’s hard to beat.

As for the script that allowed us to play Blackjack in the first place, well, here’s the one that we came up with:

$arrUserCards = @()
$arrDealerCards = @()
$arrCards=@()

$arrCards += "Ace of Hearts;11"
$arrCards += "King of Hearts;10"
$arrCards += "Queen of Hearts;10"
$arrCards += "Jack of Hearts;10"
$arrCards += "Ten of Hearts;10"
$arrCards += "Nine of Hearts;9"
$arrCards += "Eight of Hearts;8"
$arrCards += "Seven of Hearts;7"
$arrCards += "Six of Hearts;6"
$arrCards += "Five of Hearts;5"
$arrCards += "Four of Hearts;4"
$arrCards += "Three of Hearts;3"
$arrCards += "Two of Hearts;2"

$arrCards += "Ace of Clubs;11"
$arrCards += "King of Clubs;10"
$arrCards += "Queen of Clubs;10"
$arrCards += "Jack of Clubs;10"
$arrCards += "Ten of Clubs;10"
$arrCards += "Nine of Clubs;9"
$arrCards += "Eight of Clubs;8"
$arrCards += "Seven of Clubs;7"
$arrCards +=  "Six of Clubs;6"
$arrCards +=  "Five of Clubs;5"
$arrCards += "Four of Clubs;4"
$arrCards += "Three of Clubs;3"
$arrCards += "Two of Clubs;2"

$arrCards += "Ace of Diamonds;11"
$arrCards += "King of Diamonds;10"
$arrCards += "Queen of Diamonds;10"
$arrCards += "Jack of Diamonds;10"
$arrCards += "Ten of Diamonds;10"
$arrCards += "Nine of Diamonds;9"
$arrCards += "Eight of Diamonds;8"
$arrCards += "Seven of Diamonds;7"
$arrCards += "Six of Diamonds;6"
$arrCards += "Five of Diamonds;5"
$arrCards += "Four of Diamonds;4"
$arrCards +=  "Three of Diamonds;3"
$arrCards +=  "Two of Diamonds;2"

$arrCards += "Ace of Spades;11"
$arrCards += "King of Spades;10"
$arrCards += "Queen of Spades;10"
$arrCards += "Jack of Spades;10"
$arrCards += "Ten of Spades;10"
$arrCards += "Nine of Spades;9"
$arrCards += "Eight of Spades;8"
$arrCards += "Seven of Spades;7"
$arrCards += "Six of Spades;6"
$arrCards += "Five of Spades;5"
$arrCards += "Four of Spades;4"
$arrCards += "Three of Spades;3"
$arrCards += "Two of Spades;2"

$a = New-Object Random

for ($i = 1; $i -lt 3; $i++)
    {
        while ($x -ne 100)
            {
                $b = $a.next(0,52)
                if ($arrCards[$b] -ne "Dealt")
                    {
                        $arrUserCards += $arrCards[$b]
                        $arrCards[$b] = "Dealt" 
                        break
                     }
             }
    }

for ($i = 1; $i -lt 3; $i++)
    {
        while ($x -ne 100)
            {
                $b = $a.next(0,52)
                if ($arrCards[$b] -ne "Dealt")
                    {
                        $arrDealerCards += $arrCards[$b]
                        $arrCards[$b] = "Dealt" 
                        break
                     }
             }
    }

Write-Host "Your cards: " 

foreach ($strCard in $arrUserCards)
    {
        $arrHand = $strCard.Split(";")
        Write-Host $arrHand[0]  -foreground "yellow"
        $intUserTotal = $intUserTotal + [int] $arrHand[1]
    }

Write-Host
Write-Host "Dealer's card: " 

$x = 1

foreach ($strCard in $arrDealerCards)
    {
        $arrHand = $strCard.Split(";")
        if ($x -eq 1)
            {Write-Host $arrHand[0]  -foreground "green"}
        $x++
        $intDealerTotal = $intDealerTotal + [int] $arrHand[1]
    }

:UserLoop do
    {
        $title = "Your turn"
        $message = "Do you want to hit or stay?"
    
        $hit = New-Object System.Management.Automation.Host.ChoiceDescription "&Hit", `
            "Results in you being dealt another card."

        $stay = New-Object System.Management.Automation.Host.ChoiceDescription "&Stay", `
            "Ends your turn."

        $options = [System.Management.Automation.Host.ChoiceDescription[]]($hit, $stay)
        $result = $host.ui.PromptForChoice($title, $message, $options, 0) 

        if ($result -eq 1)
            {
                break UserLoop
            }

        while ($x -ne 100)
            {
                $b = $a.next(0,52)
                if ($arrCards[$b] -ne "Dealt")
                    {
                        $arrUserCards += $arrCards[$b]
                        $arrCards[$b] = "Dealt" 
                        break
                     }
             }

        Write-Host
        Write-Host "Your cards: " 

        $intUserTotal = 0

        foreach ($strCard in $arrUserCards)
            {
                $arrHand = $strCard.Split(";")
                Write-Host $arrHand[0]  -foreground "yellow"
                $intUserTotal = $intUserTotal + [int] $arrHand[1]
            }

        if ($intUserTotal -gt 21)
             {
                 Write-Host
                 Write-Host "Over 21. Sorry, you lose." -foreground "cyan"
                 exit
             }
    }
while ($x -ne 100)

if ($intDealerTotal -ge $intUserTotal)
    {
        foreach ($strCard in $arrDealerCards)
            {
                $arrHand = $strCard.Split(";")
                Write-Host $arrHand[0]  -foreground "green"
            }

        Write-Host
        Write-Host "The dealer has $intDealerTotal. Sorry, you lose." -foreground "cyan"
        exit
    }

:DealerLoop do
    {

        while ($z -ne 100)
            {
                $b = $a.next(0,52)
                if ($arrCards[$b] -ne "Dealt")
                    {
                        $arrDealerCards += $arrCards[$b]
                        $arrCards[$b] = "Dealt" 
                        break
                     }
             }

        Write-Host

        $intDealerTotal = 0

        foreach ($strCard in $arrDealerCards)
            {
                $arrHand = $strCard.Split(";")
                Write-Host $arrHand[0]  -foreground "green"
                $intDealerTotal = $intDealerTotal + [int] $arrHand[1]
            }

        if ($intDealerTotal -gt 21)
             {
                 Write-Host
                 Write-Host "The dealer is over 21. You win." -foreground "cyan"
                 exit
             }

        if ($intDealerTotal -ge $intUserTotal)
            {
                Write-Host
                Write-Host "The dealer has $intDealerTotal. Sorry, you lose." -foreground "cyan"
                exit
            }
    }
while ($x -ne 100)

So how does our Blackjack script actually work? Well, it starts out by creating three empty arrays: one to hold our deck of cards($arrCards); one to hold the cards dealt to the user ($arrUserCards); and one to hold the cards dealt to the computer/dealer (@arrDealerCards). That’s what these three lines of code are for:

$arrUserCards = @()
$arrDealerCards = @()
$arrCards=@()

After setting up our arrays, we next run through 52 lines of code that place all the cards into the array $arrCards. And yes, we could have done a for loop to cut down on the lines of code, but we thought it would be easier for people to understand what was going on if we showed all 52 cards being put into the array. Each line of code looks something like this:

$arrCards += "Ace of Hearts;11"

In the preceding line of code, we’re simply assigning the Ace of Hearts to the array. In addition, we’re including the value of the card (11), separating it from the card name by using a semicolon.

Fascinating, isn’t it?

That brings us to this line of code:

$a = New-Object Random

This line creates an instance of the System.Random object. System.Random (which is used to generate random numbers) is actually a .NET Framework class that, like all .NET classes, is accessible to Windows PowerShell. We’re going to use the System.Random class when we deal our cards. To do so, we’ll pick a random number between 0 and 51 (the first card in our deck has an index number of 0, the last card has an index number of 51). Once we have that random number, we’ll then grab the array item corresponding to that number.

Now it’s time to deal the first two cards to the user:

for ($i = 1; $i -lt 3; $i++)
    {
        while ($x -ne 100)
            {
                $b = $a.next(0,52)
                if ($arrCards[$b] -ne "Dealt")
                    {
                        $arrUserCards += $arrCards[$b]
                        $arrCards[$b] = "Dealt" 
                        break
                     }
             }

    }

Here we’ve set up a simple little For Next loop, one that runs from 1 to 2. Inside the loop, we set up another loop, a while loop that runs until we’ve dealt a card to the user. Inside that loop we use this line of code to generate a random number between 0 and 51:

$b = $a.next(0,52)

Once we have that number we check the array to see if that card has already been dealt; we’ll be able to tell that because the value of that array item would be the string Dealt. If the card has already been dealt, we simply go back to the top of the loop and try again. If the card hasn’t been dealt, we execute these three lines of code:

$arrUserCards += $arrCards[$b]
$arrCards[$b] = "Dealt" 
break

In line 1 we assign the card (that is, the value of the item in the array $arrCards) to the array holding the user’s cards; in line 2, we set the value of the item in $arrCards to the string Dealt. (That way, we’ll know that this card is no longer available.) We then call the break statement to exit the while loop.

After we’ve dealt two cards to the user, we repeat the process and deal two cards to the dealer. Once the cards have been dealt, we use this block of code to reveal the user’s two cards:

Write-Host "Your cards: " 

foreach ($strCard in $arrUserCards)
    {
        $arrHand = $strCard.Split(";")
        Write-Host $arrHand[0]  -foreground "yellow"
        $intUserTotal = $intUserTotal + [int] $arrHand[1]
    }

All we’re doing here is looping through the items in the array $arrUserCards. For each item in the array we use the Split function to split the card information into an array. (Remember, each array item contains both the card name and the card value.) We display the card name using this line of code:

Write-Host $arrHand[0]  -foreground "yellow"

Then we use this line of code to add the card value to the user’s point total:

$intUserTotal = $intUserTotal + [int] $arrHand[1]

After that we employ a similar strategy to handle the dealer’s cards; the only difference here is that, per standard blackjack rules, we reveal only one of the dealer’s cards.

Thus far, our game has played out something like this:

PS C:\scripts> c:\scripts\games\test.ps1
Your cards:
Queen of Diamonds
Seven of Spades

Dealer's card:
Ten of Clubs

Now it’s time to do some real gambling. To begin with, we set up a Do loop (named UserLoop) designed to run as long as the variable $x is not equal to 100. (Which, by design, it never will be.) Our loop statement looks like this:

:UserLoop do

Inside the loop, we use the following block of code to display a menu that prompts the user to hit (be dealt another card) or stay (play the hand as-is):

$title = "Your turn"
$message = "Do you want to hit or stay?"
    
$hit = New-Object System.Management.Automation.Host.ChoiceDescription "&Hit", `
    "Results in you being dealt another card."

$stay = New-Object System.Management.Automation.Host.ChoiceDescription "&Stay", `
    "Ends your turn."

$options = [System.Management.Automation.Host.ChoiceDescription[]]($hit, $stay)
$result = $host.ui.PromptForChoice($title, $message, $options, 0)

Note. We won’t discuss the code for setting up a simple menu in his solution. For more information, see this Windows PowerShell Tip of the Week.

If the user chooses to stay, we exit the loop; that’s because the user doesn’t want any more cards. This block of code checks to see if the user entered an S (for “stay”) and, if so, exits the loop:

if ($result -eq 1)
    {
        break UserLoop
    }

If the user chooses H (hit) we deal that user another card. That leads us into this block of code:

$intUserTotal = 0

    foreach ($strCard in $arrUserCards)
        {
            $arrHand = $strCard.Split(";")
            Write-Host $arrHand[0]  -foreground "yellow"
            $intUserTotal = $intUserTotal + [int] $arrHand[1]
        }

What we’re doing here is recalculating the user’s point total, adding up the values of all the cards in his or her hand. There’s one catch, however: if that point total exceeds 21 then the user has gone “bust” and automatically loses; in that case, we execute the following if block, which reports the fact that the user has gone over 21, then quits the script:

if ($intUserTotal -gt 21)
    {
         Write-Host
         Write-Host "Over 21. Sorry, you lose." -foreground "cyan"
         exit
     }

If the user’s point total isn’t over 21 then it’s back to the top of the loop, where we repeat the entire process, once again asking the user if he or she would like another card.

Onscreen, the action looks something like this:

PS C:\scripts> c:\scripts\games\test.ps1
Your cards:
Queen of Diamonds
Seven of Spades

Dealer's card:
Ten of Clubs

Your turn
Do you want to hit or stay?
[H] Hit  [S] Stay  [?] Help (default is "H"): s

Once the user decides to stay then it’s the dealer’s turn to play. After we echo the user’s total score, we use this block of code to check the point total for the dealer:

if ($intDealerTotal -ge $intUserTotal)
    {
        foreach ($strCard in $arrDealerCards)
            {
                $arrHand = $strCard.Split(";")
                Write-Host $arrHand[0]  -foreground "green"
            }

        Write-Host
        Write-Host "The dealer has $intDealerTotal. Sorry, you lose." -foreground "cyan"
        exit
    }

Suppose the user has 17 and the dealer already has 18 (an 8 of Clubs to go along with the King of Diamonds). If the dealer’s point total exceeds the user’s point total then the dealer has already won; therefore, we echo back this lamentable fact, then quit the script.

But what happens if the dealer doesn’t have more points than the user? In that case, the dealer needs to be dealt another card. With that in mind, we set up another Do loop, and deal a card. We update the dealer’s point total, then check to see if the dealer has gone over 21:

if ($intDealerTotal -gt 21)
    {
        Write-Host
        Write-Host "The dealer is over 21. You win." -foreground "cyan"
        exit
    }

If that’s the case, then the dealer automatically loses and the script ends. If the dealer did not go over 21, we check to see if the dealer’s point total is greater than the user’s point total:

if ($intDealerTotal -ge $intUserTotal)
    {
        Write-Host
        Write-Host "The dealer has $intDealerTotal. Sorry, you lose." -foreground "cyan"
        exit
    }

If so, the dealer is declared the winner and the game is over. If not, we go back to the top of loop, deal another card, and then check the point totals again. This continues until we have a winner.

An entire game might look something like this:

PS C:\scripts> c:\scripts\games\test.ps1
Your cards:
Queen of Diamonds
Seven of Spades

Dealer's card:
Ten of Clubs

Your turn
Do you want to hit or stay?
[H] Hit  [S] Stay  [?] Help (default is "H"): s
Ten of Clubs
Seven of Clubs

The dealer has 17. Sorry, you lose.

Now you know why we didn’t include any actual betting in the game.


Top of pageTop of page