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.