Procedural BlackJack game
So the assignment is to create the blackjack game. The rules of BlackJack.
Blank page ready I started off by writing some pseudo code for the high level principles of the game. Already I cannot imagine starting to write a program without this as it really gets you into the zone of thinking about how your program is going to operate without cluttering my mind worrying about the actual code.
My high-level pseudo code:
# Initialize deck of cards
# Deal two random cards each to player(s) and dealer
# Player 'Hits' or 'Stays' or has BJ
# continue until 'Stay' or 'Bust'
# Dealer plays
# Result
# play again?
Once I’d written this foundation I went into a second level of detail to better understand each step fully and ended up with this:
# Initialize deck of cards
# Deal two random cards each to player(s) and dealer
# Remove the dealt cards from the deck as they are dealt
# Player 'Hits' or 'Stays' or has BJ
# if player has BJ play moves to Dealer
# if player hits then another card is dealt
# if total exceeds 21 player busts comp wins, else if player has > 16 && < 20 player can 'Hit' or 'Stay', else 'Hit'
# Remove dealt cards from deck array
# continue until 'Stay' or 'Bust'
# Dealer plays if Player 'Stayed' or has BJ
# if dealer cards == BJ && players cards == BJ its a draw
# else if dealers cards < 16 dealer 'Hits'
# else if dealers cards > 16 && < 19 random ('Hit' or 'Stay')
# else if dealers cards >= 19 && <= 21 'Stay'
# else bust
# Result compares
# play again?
With the logic written I dived into writing the methods I’ll need for my program to operate. I figured I needed a method that calculates the running total of the players cards, a method for each of the players for hitting or staying, a hit method that adds a new card to the players hand. A few potential tricky elements began to enter my head such as how I’m going to handle the Aces that can be worth 1 or 11. Some thinking time is needed. Anyway this is what I have so far:
1 # multidimensional array holding two identical decks. Two decks to prevent card counting.
2 decks = [
3 ['Ah ', '2h ', '3h ', '4h ', '5h ', '6h ', '7h ', '8h ', '9h ', '10h', 'Jh ', 'Qh ', 'Kh ','Ad ', '2d ', '3d ', '4d ', '5d ', '6d ', '7d ', '8d ', '9d ', '10d', 'Jd ', 'Qd ', 'Kd ', 'As ', '2s ', '3s ', '4s ', '5s ', '6s ', '7s ', '8s ', '9s ', '10s', 'Js ', 'Qs ', 'Ks ', 'Ac ', '2c ', '3c ', '4c ', '5c ', '6c ', '7c ', '8c ', '9c ', '10c', 'Jc ', 'Qc ', 'Kc '],
4 ['Ah ', '2h ', '3h ', '4h ', '5h ', '6h ', '7h ', '8h ', '9h ', '10h', 'Jh ', 'Qh ', 'Kh ','Ad ', '2d ', '3d ', '4d ', '5d ', '6d ', '7d ', '8d ', '9d ', '10d', 'Jd ', 'Qd ', 'Kd ', 'As ', '2s ', '3s ', '4s ', '5s ', '6s ', '7s ', '8s ', '9s ', '10s', 'Js ', 'Qs ', 'Ks ', 'Ac ', '2c ', '3c ', '4c ', '5c ', '6c ', '7c ', '8c ', '9c ', '10c', 'Jc ', 'Qc ', 'Kc ']
5 ]
6
7 # two empty arrays to store the players hands
8 player = []
9 dealer = []
10
11
12 # deals cards
13 2.times do
14 player.push(decks.sample.delete(decks.sample.sample))
15 dealer.push(decks.sample.delete(decks.sample.sample))
16 end
17
18 # calculate total of cards in a hand
19 def calculate_total(array)
20 running_total = 0
21 array.each do |card|
22 if card.slice(0) == 'A'
23 running_total = running_total + 11
24 elsif card.slice(0) == 'J' || card.slice(0) == 'Q' || card.slice(0) == 'K' || card.slice(0..1) == '10'
25 running_total = running_total + 10
26 else
27 running_total = running_total + card.slice(0).to_i
28 end
29 end
30 running_total
31 end
32
33 # player hit or stay method
34 def player_hit_or_stay(running_total, decks, player)
35 if running_total == 21
36 puts "BLACKJACK!!!!"
37 exit
38 elsif running_total <= 20
39 puts 'Hit(h) or Stay(s)?'
40 answer = gets.chomp.downcase
41 while answer != 'h' && answer != 's'
42 puts 'Press "h" to hit or press "s" to stay'
43 answer = gets.chomp.downcase
44 end
45 if answer == 'h'
46 hit(decks.sample, player)
47 p player
48 end
49 end
50 end
51
52 # dealer hit or stay method
53 def dealer_hit_or_stay(running_total)
54 if running_total == 21
55 puts 'DEALER HAS BLACKJACK!'
56 elsif running_total < 17
57 puts 'Dealer Hits'
58 elsif running_total >= 17 && running_total <= 18
59 puts 'Dealer randomally selects h or s'
60 elsif running_total >= 19 && running_total <= 20
61 puts 'Dealer Stays!'
62 end
63 end
64
65 # deal new card method
66 def hit(deck, hand)
67 hand.push(deck.delete(deck.sample))
68 calculate_total(hand)
69 end
The pry-byebug gem
I was looking for a way to stop my programs at certain points and then step by step move on from the break point. Someone on the #ruby irc chanel linked me to the pry-byebug gem. It does exactly what I needed which is very useful for debugging.
Back to my game..
Dealing with aces
So after a few hours short break I returned to my program to tackle the Ace in the pack issue. Basically if the player has an ace in their hand then I want the program to output the two scores, for instance if you had an ace and a 4 you should get a message along the lines of:
You have 5 or 15
If you had an ace, 7 and an 8 you should get the message:
You have 16
In the second example it should only output one total as the other (11+7+8) exceeds 21 and therefor is unplayable.
So I need to track two totals in case an ace appears at any stage. I can then write some code that will check for an ace and if it exists add 1 to the first total and add 11 to the second.
So I reworked my calculate_total method to this:
1 # calc total
2 def calculate_total(array)
3 running_totals = {:total1 => 0, :total2 => 0}
4 array.each do |card|
5 if card.slice(0) == 'A'
6 running_totals[:total1] = running_totals[:total1] + 1
7 running_totals[:total2] = running_totals[:total2] + 11
8 ace_in_pack = true
9 elsif card.slice(0) == 'J' || card.slice(0) == 'Q' || card.slice(0) == 'K' || card.slice(0..1) == '10'
10 running_totals[:total1] = running_totals[:total1] + 10
11 running_totals[:total2] = running_totals[:total2] + 10
12 else
13 running_totals[:total1] = running_totals[:total1] + card.slice(0).to_i
14 running_totals[:total2] = running_totals[:total2] + card.slice(0).to_i
15 end
16 end
17 running_totals
18 end
The first version of this was just to get the method working in a fashion but now the method has two running totals to handle instances where an Ace is present and there are two possible totals. So my method handles this pretty well by checking if the card is an Ace and if it is increment total1 by 1 and total2 by 11. Excellent!!
Here are some outcomes:
7h, 5s
Your current score is 12
Hit(h) or Stay(s)
6d, 2c
Your current score is 8
Hit(h) or Stay(s)
Ah, 4s
Your current score is 5 or 15
Hit(h) or Stay(s)
Ah, As
Your current score is 2
Hit(h) or Stay(s)
Great st…. hang on what’s going on in the last example? Shouldn’t that say you have 2 or 12?
The reason it breaks is because for the first iteration of array.each it adds 1 to total1 and 11 to total2 and on the second iteration it again adds 1 to total1 and 11 to total2 giving a total of 2 for total1 and 22 for total2 and therefore busting total2.
If I’m dealt two aces I should have:
“You have 2 or 12
What my program needs to do in this rare but highly possible scenario is check to see if there is already an ace in the pack and if there is then add 1 to both total1 and total2 not add another 11 to total2.
So I created a new variable local to the calculate_total method that tracks if the is an ace in the pack. Here is the newly written method:
1 # calc total
2 def calculate_total(array)
3 running_totals = {:total1 => 0, :total2 => 0}
4 ace_in_pack = false
5 array.each do |card|
6 if card.slice(0) == 'A'
7 if ace_in_pack == true
8 running_totals[:total1] = running_totals[:total1] + 1
9 running_totals[:total2] = running_totals[:total2] + 1
10 elsif ace_in_pack == false
11 running_totals[:total1] = running_totals[:total1] + 1
12 running_totals[:total2] = running_totals[:total2] + 11
13 ace_in_pack = true
14 end
15 elsif card.slice(0) == 'J' || card.slice(0) == 'Q' || card.slice(0) == 'K' || card.slice(0..1) == '10'
16 running_totals[:total1] = running_totals[:total1] + 10
17 running_totals[:total2] = running_totals[:total2] + 10
18 else
19 running_totals[:total1] = running_totals[:total1] + card.slice(0).to_i
20 running_totals[:total2] = running_totals[:total2] + card.slice(0).to_i
21 end
22 end
23 running_totals
24 end
Now that’s far slicker. Line 4 is where i’ve declared the ace_in_pack variable to an initial value of false and now in the event of an Ace being present I have added a new conditional to check to see if there is already an Ace in the array. If there is then each total increments by 1 and if there isn’t an Ace present then we carry on as normal adding 1 to total1 and 11 to total2 and then reset ace_in_pack to true.
I spent a few days away from my program partly due to time constraints and partly due to allowing myself some thinking time. I came back to it today a finished the program and I’m pretty happy with it but I’ve learnt that orgainization is so important when designing and executing my programs.
I like to always know where I am and exactly what every line of my program is doing and I found that after a while writing this game I was losing myself in the code and forgetting what part does what and how one method interacts with another. I got there in the end and as a version 1 I suppose it’s a nice foundation but I should probably refactor it and adopt the DRY principle in a few places.
My final code: Well before I refactor it I guess.
1 # Initialize deck of cards
2 # Deal two random cards each to player(s) and dealer
3 # Remove the dealt cards from the deck as they are dealt
4 # Player 'Hits' or 'Stays' or has BJ
5 # if player has BJ play moves to Dealer
6 # if player hits then another card is dealt
7 # if total exceeds 21 player busts comp wins, else if player has > 16 && < 20 player can 'Hit' or 'Stay', else 'Hit'
8 # Remove dealt cards from deck array
9 # continue until 'Stay' or 'Bust'
10 # Dealer plays if Player 'Stayed' or has BJ
11 # if dealer cards == BJ && players cards == BJ its a draw
12 # else if dealers cards < 16 dealer 'Hits'
13 # else if dealers cards > 16 && < 19 random ('Hit' or 'Stay')
14 # else if dealers cards >= 19 && <= 21 'Stay'
15 # else bust
16 # Result compares
17 # play again?
18
19 # 2...9 = face value , 10,J,Q,K = 10, A = 1 or 11
20 # 4 different suits
21 # 52 cards in total
22
23
24
25 require('pry')
26
27 # M M MMMM MMMMM M M MMMMM MMMM MMMMM
28 # MM MM M M M M M M M M M
29 # M M M MMM M MMMMM M M M M MMM
30 # M M M M M M M M M M M
31 # M M MMMM M M M MMMMM MMMM MMMMM
32
33 # deals cards
34 def deal(player, dealer, decks)
35 system 'clear'
36 2.times do
37 player.push(decks.delete(decks.sample))
38 dealer.push(decks.delete(decks.sample))
39 end
40 display_cards(player)
41 end
42
43 # displays the cards and tell player their score
44 def display_cards(hands)
45 i = 0
46 puts 'PLAYERS CARDS'
47 hands.each do |card|
48 puts " _____ "
49 puts "| |"
50 puts "| #{hands[i]} |"
51 puts "| |"
52 puts " ----- "
53 i = i + 1
54 end
55 end
56
57 # Dealer shows hand
58 def dealer_shows(dealers_hand, players_hand, running_total1, running_total2)
59 system 'clear'
60 player_final_score(running_total1, running_total2)
61 display_cards(players_hand)
62 puts
63 puts
64 puts "DEALERS CARDS"
65 i = 0
66 dealers_hand.each do |card|
67 puts " _____ "
68 puts "| |"
69 puts "| #{dealers_hand[i]} |"
70 puts "| |"
71 puts " ----- "
72 i = i + 1
73 end
74 end
75
76 def player_final_score(running_total1,running_total2)
77 if running_total1 <= 21 && running_total2 <= 21 && running_total2 > running_total1
78 return running_total2
79 else
80 return running_total1
81 end
82 end
83
84 # say score
85 def say_score(hands, person)
86 total1 = calculate_total(hands)[:total1]
87 total2 = calculate_total(hands)[:total2]
88 if total1 == 21 || total2 == 21
89 puts
90 elsif total1 == total2
91 puts "#{person} has #{total1}"
92 elsif total1 <= 21 && total2 <= 21
93 puts "#{person} has #{total1} or #{total2}"
94 elsif total2 > 21
95 puts "#{person} has #{total1}"
96 end
97 end
98
99 # calc total
100 def calculate_total(array)
101 running_totals = {:total1 => 0, :total2 => 0}
102 ace_in_pack = false
103 array.each do |card|
104 if card.slice(0) == 'A'
105 if ace_in_pack == true
106 running_totals[:total1] = running_totals[:total1] + 1
107 running_totals[:total2] = running_totals[:total2] + 1
108 elsif ace_in_pack == false
109 running_totals[:total1] = running_totals[:total1] + 1
110 running_totals[:total2] = running_totals[:total2] + 11
111 ace_in_pack = true
112 end
113 elsif card.slice(0) == 'J' || card.slice(0) == 'Q' || card.slice(0) == 'K' || card.slice(0..1) == '10'
114 running_totals[:total1] = running_totals[:total1] + 10
115 running_totals[:total2] = running_totals[:total2] + 10
116 else
117 running_totals[:total1] = running_totals[:total1] + card.slice(0).to_i
118 running_totals[:total2] = running_totals[:total2] + card.slice(0).to_i
119 end
120
121 end
122 running_totals
123 end
124
125 # player hit or stay
126 def player_hit_or_stay(running_total1, running_total2, decks, player, dealer)
127 say_score(player, "Player")
128 if running_total1 > 21 && running_total2 > 21
129 puts "YOU BUST!"
130 puts "Dealer wins"
131 sleep 1.5
132 puts "GAME OVER :("
133 elsif running_total1 == 21 || running_total2 == 21
134 puts "BLACKJACK!!!!"
135 running_total1 = calculate_total(player)[:total1]
136 running_total2 = calculate_total(player)[:total2]
137 player_score = player_final_score(running_total1, running_total2)
138 running_total1 = calculate_total(dealer)[:total1]
139 running_total2 = calculate_total(dealer)[:total2]
140 dealer_score = player_final_score(running_total1, running_total2)
141 puts say_score(dealer, "Dealer")
142 sleep 1
143 result(player_score, dealer_score)
144 elsif running_total1 <= 20 || running_total2 <= 20
145 puts 'Hit(h) or Stay(s)?'
146 answer = gets.chomp.downcase
147 while answer != 'h' && answer != 's'
148 puts 'Press "h" to hit or press "s" to stay'
149 answer = gets.chomp.downcase
150 end
151 if answer == 'h'
152 sleep 1
153 hit(decks, player)
154 system 'clear'
155 display_cards(player)
156 running_total1 = calculate_total(player)[:total1]
157 running_total2 = calculate_total(player)[:total2]
158 player_hit_or_stay(running_total1, running_total2, decks, player, dealer)
159 elsif answer == 's'
160 dealer_shows(dealer, player, running_total1, running_total2)
161 puts "You stayed on #{player_final_score(running_total1, running_total2)}"
162 player_score = player_final_score(running_total1, running_total2)
163 sleep 1
164 running_total1 = calculate_total(dealer)[:total1]
165 running_total2 = calculate_total(dealer)[:total2]
166 puts say_score(dealer, "Dealer")
167 sleep 1
168 dealer_hit_or_stay(decks,running_total1,running_total2, player, dealer, player_score)
169 end
170 end
171 end
172
173 # dealer hit or stay
174 def dealer_hit_or_stay(decks, running_total1,running_total2, player, dealer, player_score)
175 if running_total1 > 21 && running_total2 > 21
176 puts "Dealer BUST, You WIN!!!!!"
177 sleep 0.5
178 puts "Game Over :)"
179 elsif running_total1 == 21 || running_total2 == 21
180 puts 'DEALER HAS BLACKJACK!'
181 running_total1 = calculate_total(dealer)[:total1]
182 running_total2 = calculate_total(dealer)[:total2]
183 dealer_score = player_final_score(running_total1, running_total2)
184 result(player_score, dealer_score)
185 elsif running_total1 < 17 && running_total2 < 17
186 puts 'Dealer Hits'
187 hit(decks, dealer)
188 sleep 2
189 dealer_shows(dealer, player, running_total1, running_total2)
190 puts "You stayed on #{player_score}"
191 say_score(dealer, "Dealer")
192 running_total1 = calculate_total(dealer)[:total1]
193 running_total2 = calculate_total(dealer)[:total2]
194 dealer_hit_or_stay(decks, running_total1,running_total2, player, dealer, player_score)
195 elsif running_total1 >= 17 && running_total1 <= 18
196 gamble = ['yes', 'no']
197 if gamble.sample == 'yes'
198 puts 'Dealer Hits'
199 hit(decks, dealer)
200 sleep 2
201 dealer_shows(dealer, player, running_total1, running_total2)
202 running_total1 = calculate_total(dealer)[:total1]
203 running_total2 = calculate_total(dealer)[:total2]
204 dealer_hit_or_stay(decks, running_total1,running_total2, player, dealer, player_score)
205 elsif gamble.sample == 'no'
206 puts 'Dealer Stays'
207 sleep 2
208 running_total1 = calculate_total(dealer)[:total1]
209 running_total2 = calculate_total(dealer)[:total2]
210 dealer_score = player_final_score(running_total1, running_total2)
211 result(player_score, dealer_score)
212 end
213 # if s result(player_score, dealer_score)
214 elsif running_total1 >= 19 && running_total1 <= 20 || running_total2 >= 19 && running_total2 <= 20
215 puts 'Dealer Stays!'
216 running_total1 = calculate_total(dealer)[:total1]
217 running_total2 = calculate_total(dealer)[:total2]
218 dealer_score = player_final_score(running_total1, running_total2)
219 result(player_score, dealer_score)
220 end
221 end
222
223 # deal new card
224 def hit(deck, hand)
225 hand.push(deck.delete(deck.sample))
226 calculate_total(hand)
227 end
228
229 # result calc
230 def result(player_score, dealer_score)
231 if player_score == dealer_score
232 puts "Both player got #{player_score}"
233 puts "It's a DRAW!"
234 elsif player_score > dealer_score
235 puts "#{player_score} beats #{dealer_score}"
236 puts "YOU WIN!!!"
237 elsif dealer_score > player_score
238 puts "#{dealer_score} beats #{player_score}"
239 puts "Dealer wins :("
240 end
241 end
242
243
244 # GGGGG GGGGG G G GGGGG
245 # G G G GG GG G
246 # G GG GGGGG G G G GGG
247 # G G G G G G G
248 # GGGGG G G G G GGGGG
249
250
251 # play again
252 play_again = 'y'
253 game_streak = 0
254 results = { :wins => 0, :losses => 0, :draws => 0 } # store history
255
256
257 while play_again == 'y'
258 # initialize decks
259 decks = ['Ah ', '2h ', '3h ', '4h ', '5h ', '6h ', '7h ', '8h ', '9h ', '10h', 'Jh ', 'Qh ', 'Kh ','Ad ', '2d ', '3d ', '4d ', '5d ', '6d ', '7d ', '8d ', '9d ', '10d', 'Jd ', 'Qd ', 'Kd ', 'As ', '2s ', '3s ', '4s ', '5s ', '6s ', '7s ', '8s ', '9s ', '10s', 'Js ', 'Qs ', 'Ks ', 'Ac ', '2c ', '3c ', '4c ', '5c ', '6c ', '7c ', '8c ', '9c ', '10c', 'Jc ', 'Qc ', 'Kc ', 'Ah ', '2h ', '3h ', '4h ', '5h ', '6h ', '7h ', '8h ', '9h ', '10h', 'Jh ', 'Qh ', 'Kh ','Ad ', '2d ', '3d ', '4d ', '5d ', '6d ', '7d ', '8d ', '9d ', '10d', 'Jd ', 'Qd ', 'Kd ', 'As ', '2s ', '3s ', '4s ', '5s ', '6s ', '7s ', '8s ', '9s ', '10s', 'Js ', 'Qs ', 'Ks ', 'Ac ', '2c ', '3c ', '4c ', '5c ', '6c ', '7c ', '8c ', '9c ', '10c', 'Jc ', 'Qc ', 'Kc ']
260
261 # empty arrays to hold all players cards
262 player = []
263 dealer = []
264
265 # Deal two card to each player
266 deal(player, dealer, decks)
267
268 # player takes turn
269 total1 = calculate_total(player)[:total1]
270 total2 = calculate_total(player)[:total2]
271 player_hit_or_stay(total1,total2, decks, player, dealer)
272 total1 = calculate_total(player)[:total1]
273 total2 = calculate_total(player)[:total2]
274 p = player_final_score(total1, total2)
275 total1 = calculate_total(dealer)[:total1]
276 total2 = calculate_total(dealer)[:total2]
277 d = player_final_score(total1, total2)
278
279
280 sleep 0.5
281
282 if d > 21
283 results[:wins] = results[:wins] + 1
284 elsif p <= 21 && p > d
285 results[:wins] = results[:wins] + 1
286 elsif p == d
287 results[:draws] = results[:draws] + 1
288 elsif d <= 21 && d > p
289 results[:losses] = results[:losses] + 1
290 elsif p > 21
291 results[:losses] = results[:losses] + 1
292 end
293
294 game_streak = game_streak + 1
295 per_wins = (results[:wins].to_f / game_streak.to_f * 100).round(2)
296 puts "_____________________________________"
297 puts "_____________________________________"
298 puts "Current game streak is #{game_streak}"
299 puts "You have won #{results[:wins]}, lost #{results[:losses]} and drawn #{results[:draws]}"
300 puts "You have won #{per_wins}% of the games you have played"
301 puts "_____________________________________"
302 puts "_____________________________________"
303
304 # play again?
305 puts "Play again? (y/n)"
306 play_again = gets.chomp
307
308 end
I built in some pretty neat features too like making the dealer not follow a scripted method of hit or staying. I also allowed Ace to be worth 1 and 11 and giving the player an option to play both totals. Finally whilst in the game loop the game stores your game streak, wins, losses and draws total and outputs the percentage of wins you have.