Scripting Games, Advanced Event 10, Blackjack

Disclaimer:  This is modified slightly over the version I submitted.  I got a bit carried away with the description so this is a long post!

First, a screen-shot:-)

When I saw MOW’s teaser a couple of weeks’ back I decided a text-only version of Blackjack was going to be unacceptable!  I didn’t think I’d go as far as drawing cards, but thought it would be good to have a go at writing to the console buffer to display things in a nice tidy fashion:

I wrote this in two stages, firstly getting the regular game to work (but using dummy functions to display the output as simple text on the console); secondly, replacing the display functions to use $Host.UI.RawUI calls, changing the program into a full-screen version.  I haven’t tried this before (that’s the good thing about The Scripting Games!), so there are a few rough edges, noted below.

Here’s the code, split into sections (the full code is at the end of the post).  Firstly some setup and pre-amble:

  1 Clear-Host
  2 $script:pos = $Host.ui.rawui.CursorPosition
  3 
  4 $rect = New-Object System.Management.Automation.Host.Rectangle
  5 $rect.Top = $pos.y
  6 $rect.Right = 70
  7 $rect.Left = 0
  8 $rect.Bottom = $pos.y + 25
  9 $script:buf=$Host.UI.RawUI.GetBufferContents($rect)
 10 
 11 $script:tablecolour='darkgreen'
 12 $script:textcolour='black'
 13 
 14 function setTable($colour=$script:tablecolour) {
 15 	$c=New-Object System.Management.Automation.Host.BufferCell
 16 	$c.BackgroundColor=$colour
 17 	$c.ForegroundColor='black'
 18 	$c.Character=' '
 19 
 20 	for ($i=0; $i -le $script:buf.GetLongLength(0)-1; $i++){
 21 		for ($j=0; $j -le $script:buf.GetLongLength(1)-1; $j++){
 22 			$script:buf[$i,$j]=$c
 23 		}
 24 	}
 25 }

The first section defines a playing area and a function to initialise the area.  I’m sure this is unnecessary/can be done in a better way.  See MOW’s event 10 post for definitive information:-)

The next section defines a function to write a string to the screen buffer ("writebuf", below) with optional foreground and background colours.  I did this by writing individual characters into the buffer (line 5 below) although, in hindsight, I think using $Host.UI.RawUI.NewBufferCellArray would be the better way to go.

"Writebuf" will potentially be called multiple times, making changes to the screen buffer until function "flush" (line 8 below) is called to display the buffer on the screen.

"ClearTable" (line 10 below) is used to wipe out the previous hand’s cards and scores from the screen. 

  1 function writebuf($x,$y,$string,$fore=$script:textcolour,$back=$script:tablecolour) {
  2 	$c=New-Object System.Management.Automation.Host.BufferCell
  3 	$c.BackgroundColor=$back
  4 	$c.ForegroundColor=$fore
  5 	$string.tochararray()|%{$c.Character=$_; $script:buf[$x,$y++]=$c}
  6 }
  7 
  8 function flush {$Host.ui.RawUI.SetBufferContents($script:pos,$script:buf)}
  9 
 10 function clearTable {
 11 	7..18|%{writebuf ($_) 7 (' ').padright(25)}
 12 	7..18|%{writebuf ($_) 36 (' ').padright(25)}
 13 	writebuf 4 18 $(' '.padright(18))
 14 	writebuf 4 51 $(' '.padright(18))
 15 	flush
 16 }
 17 
 18 function status ($string) {writebuf 23 7 $string.padright(60); flush}
 19 
 20 function pause ($milliseconds) {Start-Sleep -Milliseconds $milliseconds}

The next function ("choice", below) display a message and uses $Host.UK.RawUI.ReadKey to get an answer from the player.  The function will keep reading characters until a valid key is pressed. A list of valid keys is passed to the function in $set:

  1 function choice ($message, $set) {
  2 	status $message
  3 	do {$answer=[string]($Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")).character}
  4 	until ($set -match $answer)
  5 	return $answer
  6 }

So, that’s the screen taken care of.  The rest of the code described below plays the Blackjack game calling on the "writebuf" and "status" functions to display the results.  But first, some support functions for the game, starting with the all-important shuffle:

  1 # Start of Blackjack code
  2 
  3 
  4 function shuffle {
  5 	$script:dealt = 0 	# Number of cards dealt so far
  6 	$script:pack = 0..51
  7 	
  8 	# Shuffle the pack.  This is the Fisher-Yates shuffle once more!
  9 	$r = New-Object system.random
 10 	0..51| % {$j = $r.Next($_,51); $x = $pack[$_]; $pack[$_] = $pack[$j]; $pack[$j] = $x}
 11 }

So, "shuffle" creates a new pack/deck of cards.  The script-scoped variable $script:dealt remembers how many cards have been taken from the pack while the $script:pack variable is the pack itself, represented as a 52-element array of numbers between 0 and 51.

Here we see the inevitable Fisher-Yates shuffle again.  I’ve seen lots of other very weird and wonderful shuffle techniques used throughout the scripting games, but this is really the way to randomly mix the elements of an array.  See the Wikipedia article and don’t use any other way OK!

The shuffle function merely provides 52 numbers in a random sequence.  To turn these into playing cards and to assign them to a player’s hand we need some more plumbing.  Function "Deal" does what is required, but before describing that we need to know something about the data structures it manipulates.  First of all, the structure of a card.  A card is described by a custom PowerShell object with two properties – one is the value of the card as according to the rules of the Scripting Guys’ Blackjack (from 2 to 11, or 1 for Aces if required); the other is a string naming the card ("Queen of Clubs" and so on).

The card is created on line 6 of function deal (below).  The name, suit and value is then calculated from the card number (0..51) on lines 7-16; these lines map the 52 unique numbers from the pack to the corresponding four suits of cards we need.  The $card object will end up having $card.name = "Queen of Clubs" and $card.points = 10 (for example).

"Deal" now has to assign this card to a hand.  For the purposes of this script a hand is another custom PowerShell object with the following properties:

  • Points: The value of the entire hand.  In Blackjack this value will not exceed 21 for a valid hand
  • Soft: A boolean value indicating whether there are any Aces in the hand.  If there are Aces they initially have a value of 11 points each; however, if the value of the overall hand should exceed 21 any Aces in the hand can, instead, be counted as only one point.  If the hand currently contains Aces that are still being counted as 11 points then this property will have the value $True
  • Cards: An array of the card objects already described that make up the hand

As an example, here’s how a hand looks at the PowerShell console:

PS> $hand|ft -auto

points  soft cards
------  ---- -----
    13 False {Jack of Clubs, Three of Spades}


PS> $hand.cards|ft -auto

Name            Points
----            ------
Jack of Clubs       10
Three of Spades      3

"Deal" adds the new card to the current hand (line 19, below); calculates the new points total for the hand (line 20) and checks to see if the hand now contains any soft Aces (line 21).  That’s the data structures dealt with (so to speak), but there’s one more task for "Deal" to take care of.

The addition of another card to the current hand may have caused the hand’s value to exceed 21 points.  If this has happened, lines 23-34 check to see if there are any soft Aces in the hand that can have their values changed from 11 points to 1 point.  If there are soft Aces they’re downgraded one at a time until the hand’s value drops below 21.

Here’s the complete "Deal" function:

  1 function deal ([ref] $hand) {
  2 	# Add a card to the specified hand.  First, get the next card from the pack as a custom object
  3 
  4 	if ($script:dealt -ge 51) {Throw "Pack is empty"}
  5 	$c = $script:pack[$script:dealt ++ ]			# Pick next card from pack (if not all dealt)
  6 	$card = [object]|Select Name, Points			# convert card to custom object
  7 	$suit = ('Clubs','Diamonds','Hearts','Spades')[[math]::truncate($c / 13)]		# Calculate suit
  8 	$value = $c % 13 + 1
  9 	$card.name, $card.points = $(switch ($value) {	# Calculate name and points
 10 		1 {"Ace",11}
 11 		11 {"Jack",10}
 12 		12 {"Queen",10}
 13 		13 {"King",10}
 14 		Default {('Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten')[$value - 2],$value}
 15 	})
 16 	$card.name=$card.name + " of $suit"
 17 	
 18 	# Now add the custom card object to the hand...
 19 	$hand.value.cards+=$card
 20 	$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
 21 	$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})
 22 	
 23 	# If hand is now worth more than 21 points, minimize by checking for soft Aces
 24 	while ($hand.value.points -gt 21 -and $hand.value.soft) {
 25 		# Check for soft aces
 26 		foreach ($card in $hand.value.cards) {
 27 			if ($card.points -eq 11) {
 28 				$card.points=1			# Convert soft ace to hard
 29 				break;				# Just change one Ace at a time
 30 			}
 31 		}
 32 		$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
 33 		$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})	# Check if any more soft Aces
 34 	}
 35 }

There’s one final subtlety with the "Deal" function.  We want to be able to deal cards to the player or the dealer.  The obvious way to do this is to pass the player’s hand or the dealer’s hand to the "Deal" function and have it manipulate the appropriate set of cards.  In order to do this we have to use a rather obscure (at least in PowerShell) function – passing parameters by reference.  See the last paragraph of this post on the PowerShell Blog.  I can’t find any other references to this technique in a PowerShell article (or in Bruce’s book!) – so it really is obscure!

Pass by reference is indicated on the parameter declaration by using the [ref] type.  This must also be specified on the line calling the function (e.g. deal ([ref] $playerhand), see code below).  In addition to using the [ref] type to pass the parameter it is necessary, within the function, to refer to the contents of passed variable by using "$parametername.value".  See, for example, line 19 above, where we use "$hand.value.cards+=…" rather than the more likely "$hand.cards+=…".

Only one more function before we actually start to play (!), "display-hands", below, is mostly mechanical – just writing text at given parts of the screen.  A couple of bits of logic suppress the initial display of the dealer’s second card and prevent the dealer’s score from being shown until the player has finished (or busted).

  1 function display-hands {
  2 	$playerhand.cards|%{$y=7}{writebuf ($y++) 7 $_.Name.padright(25)}
  3 	$dealerhand.cards|%{$y=7}{writebuf ($y++) 36 $_.Name.padright(25)}
  4 	if ($playerhand.soft) {$soft=', Soft'} else {$soft=''}
  5 	$points="($($playerhand.points) points$soft)"
  6 	writebuf 4 18 $points.padright(18)
  7 	if (!$dealerdown -and $dealerhand.cards.count -gt 1) {
  8 		writebuf 8 36 ('?'.padright(25))
  9 	} 
 10 	if ($dealerdown) {
 11 		if ($dealerhand.soft) {$soft=', Soft'} else {$soft=''}
 12 		$points="($($dealerhand.points) points$soft)"
 13 		writebuf 4 51 $points.padright(18)
 14 	}
 15 	flush
 16 }

Finally, we use all these functions to play the game!  Display some headers, initialise the hands, shuffle the pack and deal the first two cards each:

  1 setTable 
  2 writebuf 1 22 ' Welcome to Blackjack! ' 'red' 'black'
  3 
  4 writebuf 4 7 'Your Cards'
  5 writebuf 5 7 '~~~~~~~~~~'
  6 
  7 writebuf 4 36 "Dealer's Cards"
  8 writebuf 5 36 '~~~~~~~~~~~~~~'
  9 
 10 
 11 do {
 12 	# Play a new hand...
 13 
 14 	$dealerhand=,[object]|select points, soft, cards
 15 	$dealerhand.cards=@()
 16 	$playerhand=,[object]|select points, soft, cards
 17 	$playerhand.cards=@()
 18 
 19 	$dealerdown=$FALSE		# True when dealer reveals second card in hand
 20 	clearTable
 21 	status "Shuffling Cards..."; shuffle; pause 1000
 22 	status "Dealing Cards..."; pause 800
 23 
 24 	# Deal first two cards
 25 	1..2| % {
 26 		deal ([ref]$playerhand); display-hands; pause 600
 27 		deal ([ref]$dealerhand); display-hands; pause 600
 28 	}
 29 

After all that, the code that takes care of playing the player’s hand is trivial, thankfully.  Every time the payer chooses "Hit" we deal a new card into the player’s hand (line 7, below) and update the display:

  1 $next =
  2 # Play player’s hand…
  3 while (($playerhand.points -lt 21) -and ($next -ne ‘s’)) {
  4 $next = Read "You have $($playerhand.points) points.  Stay (S) or Hit (H)?" ‘SHQ’
  5 if ($next -eq ‘Q’) {Clear-Host; exit}
  6 if ($next -eq ‘H’) {
  7 deal ([ref]$playerhand)
  8 status ("You draw the $($playerhand.cards[-1].name)")
  9 pause 1000
10 display-hands
11 }
12 }

The player has finished; that’s either because they stuck (on less than 21) or got 21 or more points, check these possibilities and then move on to the dealer’s hand, played on lines 12-17 below:

  1 	# Check if result, otherwise play dealer's hand...
  2 	switch ($playerhand.points) {
  3 		21 {status '21, You Win!'; break}
  4 		{$_ -gt 21} {status 'Over 21.  Sorry, you lose'; break}
  5 		default {
  6 			# Play dealer's hand...
  7 			$dealerdown=$TRUE
  8 			status "You stick on $($playerhand.points) points"
  9 			pause 1400
 10 			display-hands
 11 			pause 1200
 12 			while ($dealerhand.points -lt $playerhand.points) {
 13 				deal ([ref]$dealerhand)
 14 				status ("The Dealer draws the $($dealerhand.cards[-1].name)")
 15 				pause 2000
 16 				display-hands
 17 			}
 18 			status "The Dealer has $($dealerhand.points) points"
 19 			pause 1600
 20 			switch ($dealerhand.points) {
 21 				{$_ -gt 21} {status 'You Win!'; break}
 22 				{$_ -ge $playerhand.points} {status 'The Dealer Wins'}
 23 			}
 24 		}
 25 	}
 26 
 27 	pause 1600
 28 	$next = choice 'Play again (Y/N)?' 'YNQ'
 29 } until ($next -ne 'y')
 30 
 31 Clear-Host

 

 

Now, when I get bored I must convert the display functions to use MOW’s fantastic cards!

Here’s the whole program:

 

 

  1 # Blackjack, Winter Scripting Games 2008, Advanced Event 10, Chris Warwick, Get-UKPSUG
  2 
  3 # Screen Buffer Code and auxiliary functions
  4 
  5 Clear-Host
  6 $script:pos = $Host.ui.rawui.CursorPosition
  7 
  8 $rect = New-Object System.Management.Automation.Host.Rectangle
  9 $rect.Top = $pos.y
 10 $rect.Right = 70
 11 $rect.Left = 0
 12 $rect.Bottom = $pos.y + 25
 13 $script:buf=$Host.UI.RawUI.GetBufferContents($rect)
 14 
 15 $script:tablecolour='darkgreen'
 16 $script:textcolour='black'
 17 
 18 function setTable($colour=$script:tablecolour) {
 19 	$c=New-Object System.Management.Automation.Host.BufferCell
 20 	$c.BackgroundColor=$colour
 21 	$c.ForegroundColor='black'
 22 	$c.Character=' '
 23 
 24 	for ($i=0; $i -le $script:buf.GetLongLength(0)-1; $i++){
 25 		for ($j=0; $j -le $script:buf.GetLongLength(1)-1; $j++){
 26 			$script:buf[$i,$j]=$c
 27 		}
 28 	}
 29 }
 30 
 31 function clearTable {
 32 	7..18|%{writebuf ($_) 7 (' ').padright(25)}
 33 	7..18|%{writebuf ($_) 36 (' ').padright(25)}
 34 	writebuf 4 18 $(' '.padright(18))
 35 	writebuf 4 51 $(' '.padright(18))
 36 	flush
 37 }
 38 
 39 function writebuf($x,$y,$string,$fore=$script:textcolour,$back=$script:tablecolour) {
 40 	$c=New-Object System.Management.Automation.Host.BufferCell
 41 	$c.BackgroundColor=$back
 42 	$c.ForegroundColor=$fore
 43 	$string.tochararray()|%{$c.Character=$_; $script:buf[$x,$y++]=$c}
 44 }
 45 
 46 function flush {$Host.ui.RawUI.SetBufferContents($script:pos,$script:buf)}
 47 
 48 function status ($string) {writebuf 23 7 $string.padright(60); flush}
 49 
 50 function pause ($milliseconds) {Start-Sleep -Milliseconds $milliseconds}
 51 
 52 function choice ($message, $set) {
 53 	status $message
 54 	do {$answer=[string]($Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")).character}
 55 	until ($set -match $answer)
 56 	return $answer
 57 }
 58 
 59 # Start of Blackjack code
 60 
 61 
 62 function shuffle {
 63 	$script:dealt = 0 	# Number of cards dealt so far
 64 	$script:pack = 0..51
 65 	
 66 	# Shuffle the pack.  This is the Fisher-Yates shuffle once more!
 67 	$r = New-Object system.random
 68 	0..51| % {$j = $r.Next($_,51); $x = $pack[$_]; $pack[$_] = $pack[$j]; $pack[$j] = $x}
 69 }
 70 
 71 function deal ([ref] $hand) {
 72 	# Add a card to the specified hand.  First, get the next card from the pack as a custom object
 73 
 74 	if ($script:dealt -ge 51) {Throw "Pack is empty"}
 75 	$c = $script:pack[$script:dealt ++ ]			# Pick next card from pack (if not all dealt)
 76 	$card = [object]|Select Name, Points			# convert card to custom object
 77 	$suit = ('Clubs','Diamonds','Hearts','Spades')[[math]::truncate($c / 13)]		# Calculate suit
 78 	$value = $c % 13 + 1
 79 	$card.name, $card.points = $(switch ($value) {		# Calculate name and points
 80 		1 {"Ace",11}
 81 		11 {"Jack",10}
 82 		12 {"Queen",10}
 83 		13 {"King",10}
 84 		Default {('Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten')[$value - 2],$value}
 85 	})
 86 	$card.name=$card.name + " of $suit"
 87 	
 88 	# Now add the custom card object to the hand...
 89 	$hand.value.cards+=$card
 90 	$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
 91 	$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})
 92 	
 93 	# If hand is now worth more than 21 points, minimize by checking for soft Aces
 94 	while ($hand.value.points -gt 21 -and $hand.value.soft) {
 95 		# Check for soft aces
 96 		foreach ($card in $hand.value.cards) {
 97 			if ($card.points -eq 11) {
 98 				$card.points=1		# Convert soft ace to hard
 99 				break;			# Just change one Ace at a time
100 			}
101 		}
102 		$hand.value.points=($hand.value.cards|Measure-Object -Sum points).sum
103 		$hand.value.soft=[bool]($hand.value.cards|?{$_.points -eq 11})	# Check if any more soft Aces
104 	}
105 }
106 
107 function display-hands {
108 	$playerhand.cards|%{$y=7}{writebuf ($y++) 7 $_.Name.padright(25)}
109 	$dealerhand.cards|%{$y=7}{writebuf ($y++) 36 $_.Name.padright(25)}
110 	if ($playerhand.soft) {$soft=', Soft'} else {$soft=''}
111 	$points="($($playerhand.points) points$soft)"
112 	writebuf 4 18 $points.padright(18)
113 	if (!$dealerdown -and $dealerhand.cards.count -gt 1) {
114 		writebuf 8 36 ('?'.padright(25))
115 	} 
116 	if ($dealerdown) {
117 		if ($dealerhand.soft) {$soft=', Soft'} else {$soft=''}
118 		$points="($($dealerhand.points) points$soft)"
119 		writebuf 4 51 $points.padright(18)
120 	}
121 	flush
122 }
123 
124 
125 
126 setTable 
127 writebuf 1 22 ' Welcome to Blackjack! ' 'red' 'black'
128 
129 writebuf 4 7 'Your Cards'
130 writebuf 5 7 '~~~~~~~~~~'
131 
132 writebuf 4 36 "Dealer's Cards"
133 writebuf 5 36 '~~~~~~~~~~~~~~'
134 
135 
136 do {
137 	# Play a new hand...
138 
139 	$dealerhand=,[object]|select points, soft, cards
140 	$dealerhand.cards=@()
141 	$playerhand=,[object]|select points, soft, cards
142 	$playerhand.cards=@()
143 
144 	$dealerdown=$FALSE		# True when dealer reveals second card in hand
145 	clearTable
146 	status "Shuffling Cards..."; shuffle; pause 1000
147 	status "Dealing Cards..."; pause 800
148 
149 	# Deal first two cards
150 	1..2| % {
151 		deal ([ref]$playerhand); display-hands; pause 600
152 		deal ([ref]$dealerhand); display-hands; pause 600
153 	}
154 
155 	$next = ''
156 	# Play player's hand...
157 	while (($playerhand.points -lt 21) -and ($next -ne 's')) {
158 		$next = Read "You have $($playerhand.points) points.  Stay (S) or Hit (H)?" 'SHQ'
159 		if ($next -eq 'Q') {Clear-Host; exit}
160 		if ($next -eq 'H') {
161 			deal ([ref]$playerhand)
162 			status ("You draw the $($playerhand.cards[-1].name)")
163 			pause 1000
164 			display-hands
165 		}
166 	}
167 
168 	# Check if result, otherwise play dealer's hand...
169 	switch ($playerhand.points) {
170 		21 {status '21, You Win!'; break}
171 		{$_ -gt 21} {status 'Over 21.  Sorry, you lose'; break}
172 		default {
173 			# Play dealer's hand...
174 			$dealerdown=$TRUE
175 			status "You stick on $($playerhand.points) points"
176 			pause 1400
177 			display-hands
178 			pause 1200
179 			while ($dealerhand.points -lt $playerhand.points) {
180 				deal ([ref]$dealerhand)
181 				status ("The Dealer draws the $($dealerhand.cards[-1].name)")
182 				pause 2000
183 				display-hands
184 			}
185 			status "The Dealer has $($dealerhand.points) points"
186 			pause 1600
187 			switch ($dealerhand.points) {
188 				{$_ -gt 21} {status 'You Win!'; break}
189 				{$_ -ge $playerhand.points} {status 'The Dealer Wins'}
190 			}
191 		}
192 	}
193 
194 	pause 1600
195 	$next = choice 'Play again (Y/N)?' 'YNQ'
196 } until ($next -ne 'y')
197 
198 Clear-Host
 
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s