Blackjack
October 17, 2014
Blackjack is a casino game of chance, played by a player and a dealer. Both player and dealer are initially dealt two cards from a standard 52-card deck. If the player’s initial hand consists of an ace and a ten or face card, the player wins, unless the dealer also has an ace and a ten or face card, in which case the game is a tie. Otherwise, the player draws cards until he decides to stop; if at any time the sum of the pips on the cards (aces count either 1 or 11, face cards count 10) exceeds 21, the player is busted, and loses. Once the player is finished, the dealer draws cards until he has 17 or more pips, or goes bust, at which time the game ends. If neither has gone bust, the hand with the most pips wins. There are many variant rules, but we’ll keep things simple with the rules described above.
Your task is to simulate Blackjack and determine the winning percentage for the player. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.
I should stop, but here’s a J solution:
A sample run is
I don’t have time to work this up into a full solution right now, but it should be possible to calculate the exact percentages as follows:
Build a graph of the possible game states; a game state is a target value (initially 16), a score so far, and a deck (or maybe a shoe) that is just a bag of integers 1-10. Initially, for the player, the target is 17, the score is 0 and the deck is whatever we want to start with, eg. 1-9 map to 4, 10 maps to 16 for a single pack.
Each transition takes one element out of the bag and adds it to the score, a final state is one where the score is greater that the target, at which we stop (with the player bust or not). Each non-final state has a probability associated with each outgoing transition (just the probability of selecting that particular bag element) and we can propagate backwards to get the probabilities of the various final states.
There are a few thousand possible finishing states where the player doesn’t go bust and we need to use each one as a starting point for a simulation of the dealer, but this shouldn’t be completely intractable.
Here’s some Python then. Use a dictionary to keep track of states (just store the remaining deck as a tuple in here – dictionary keys must be immutable). Divide up the probabilities as we go and combine afterwards for an overall win/lose/draw value, we seem to be able to do this without too much problem with rounding error. Starting with a single pack, we end up with 1336 state in which the player hasn’t busted & must be simulated for the dealer. Total runtime on my i3 laptop is 25 secs and the final probabilities are: lose: 48.9%, win: 41.2%, draw: 9.8%.
#!/usr/bin/python
# Create a new deck with ith count decremented
def makenewdeck(deck,i):
newdeck = list(deck)
newdeck[i] -= 1
return tuple(newdeck)
class State:
def __init__ (self,score,ncards,aces,deck,prob):
self.score = score; self.aces = aces
self.deck = deck; self.prob = prob;
self.ncards = ncards; self.ndeck = sum(deck)
def __str__ (self):
return “[%d %d %d %g %s]”%(self.score,self.aces,
self.ncards,self.prob,self.deck)
def finalstate(self):
return self.score > 16
def getscore(self):
if self.score > 21: return None
else: return self.score
def blackjack(self):
return self.ncards == 2 and self.score == 21
def play(initdeck):
initstate = State(0,0,0,initdeck,1.0)
states = [initstate]
deckindex = { initdeck: 0 }
i = 0
while(i != len(states)):
s = states[i]; i += 1
if not s.finalstate():
for j in range(0,len(s.deck)):
if s.deck[j] > 0:
newdeck = makenewdeck(s.deck,j)
newprob = s.deck[j]*s.prob/s.ndeck
n = deckindex.get(newdeck)
if n != None:
# Seen this state already
states[n].prob += newprob
else:
newscore = s.score
newaces = s.aces
if (j == 0):
# Aces initially valued at 11
newscore += 11; newaces += 1
else:
newscore += j+1
if newscore > 21 and newaces > 0:
# If we busted with an ace left, count it as 1
newscore -= 10; newaces -= 1
newstate = State(newscore,s.ncards+1,
newaces,newdeck,newprob)
n = len(states)
states.append(newstate)
deckindex[newdeck] = n
return states
initdeck = (4,4,4,4,4,4,4,4,4,16)
pstates = play(initdeck)
dealerprob = 0; playerprob = 0; drawprob = 0 # Final probabilities
for i in range(0,len(pstates)):
pstate = pstates[i];
if pstate.finalstate():
pprob = pstate.prob
pscore = pstate.getscore() # player score
if pscore == None:
dealerprob += pprob # Player is bust
else:
dstates = play(pstate.deck)
# Accumulate subsidiary probabilities here
# to reduce rounding errors
dealerprob2 = 0; playerprob2 = 0; drawprob2 = 0
for j in range (0,len(dstates)):
dstate = dstates[j]
if dstate.finalstate():
dprob = dstate.prob
dscore = dstate.getscore() # dealer score
if dscore == None or dscore < pscore:
playerprob2 += dprob
elif dscore == pscore:
if pstate.blackjack() and not dstate.blackjack():
playerprob2 += dprob
else:
drawprob2 += dprob
else:
dealerprob2 += dprob
# Add in the subsidiary probabilities
dealerprob += pprob*dealerprob2
playerprob += pprob*playerprob2
drawprob += pprob*drawprob2
print (dealerprob,playerprob,drawprob,dealerprob+playerprob+drawprob)
And with proper indentation (it would be nice if comments were editable):
;; Interactive console Black jack
(defn cartesian-product [l & ls]
(reduce
(fn [a b] (for [x a y b] (conj x y)))
(map vector l) ls))
;; The deck of cards. The suits are not being used in the game but it’s more
;; fun if they’re printed anyway
(def deck (cartesian-product ‘(\u2665 \u2660 \u2666 \u2663) (range 13)))
(defn print-card [[color val]]
(let [v (if (== val 0) ‘A
(if ( a 9) 10 (+ a 1)))))
(defn bust? [v] (> v 21))
;; Find all the possible sums of values of the cards and choose the appropriate one
;; return 50 if bust
(defn hand-value [cards]
(let [possible-values (apply cartesian-product (map card-value cards))
possible-sums (map #(apply + %) possible-values)]
(if (bust? (apply min possible-sums)) 50
(apply max (filter #( v 16))
;; Interactive player’s logic
(defn players-logic [hand value]
(if (= 21 value) true
(do
(println (map print-card hand) “\n” value ” – More?”)
(= “n” (read-line)))))
;; The two game phases:
(defn dealers-game [hand pile value]
(let [[d-hand pile d-value] (deal-hand pile dealers-logic)]
(println “Dealer’s hand” (map print-card d-hand))
(if (or (bust? d-value) (> value d-value)) :win :lose)))
(defn game [pile]
(let [[hand pile value] (deal-hand pile players-logic)]
(println “Your hand” (map print-card hand))
(cond (= 21 value) :win
(bust? value) :lose
:else (dealers-game hand pile value))))
;; Let’s play!
(loop [won 0 lost 0]
(let [result (game (shuffle deck))
win (if (= :win result) 1 0)
won (+ win won)
lost (- (+ 1 lost) win)]
(println ({:win “You win” :lose “Dealer wins”} result))
(if ((fn [] (println “Play another game?”) (= “n” (read-line))))
(do
(println “You won” (int (* 100 (/ won (+ won lost)))) “% of the games”)
(vector won lost))
(recur won lost))))
my previous post is about clojure.
The commenting system on this blog goes bust! ;-)