Studying Blackjack
This is a solution to Ruby Quiz #151. The quiz involves determining the odds of all possible dealer outcomes, given a specific dealer upcard. It assumes the dealer is playing with a fresh shoe, w/o other players playing.
Rather than using randomized simulation, this solution tries to calculate the exact odds by looking at all possible permutations of a fresh shoe.
The “dealer” plays by typical dealer rules — stand on 17 or above, hit otherwise.
It's interesting to note that originally this solution used the mathn library to perform rational math rather than floating point math. I did this thinking I'd get more accurate results by avoiding floating point rounding errors. It turns out, however, that that wasn't necessary. Floating point math gave me the same result, and the program ran more than five times faster!
# A solution to RubyQuiz #151 by LearnRuby.com .
#
# For the game of casino Blackjack, determines the odds of all
# possible dealer outcomes, given a specific dealer upcard. Assumes
# the dealer is playing with a fresh shoe, w/o other players playing.
#
# See http://www.rubyquiz.com/quiz151.html for details.
#
# The latest version of this solution can also be found at
# http://learnruby.com/examples/ruby-quiz-151.shtml .
# CONFIGURABLE PARAMETERS
# deck count is first command line argument or default of 2
deck_count = ARGV.size == 1 && ARGV[0].to_i || 2
# CONSTANTS
# The unique cards (10 and face cards are not distinguished).
CARDS = (2..10).to_a << :ace
# The possible results are 17--21 plus bust and natural. The order is
# given in a what might be considered worst to best order.
POSSIBLE_RESULTS = [:bust] + (17..21).to_a + [:natural]
# CLASSES
class Shoe
# A deck is a hash keyed by the card, and the value is how many of
# that card there are. There are four of all cards except the
# 10-value cards, and there are sixteen of those.
DECK = CARDS.inject(Hash.new) { |hash, card| hash[card] = 4; hash }
DECK[10] = 16
def initialize(deck_count = 2)
@cards = DECK.inject(Hash.new) { |hash, card|
hash[card.first] = deck_count * card.last; hash }
@card_count = @cards.inject(0) { |sum, card_count|
sum + card_count.last }
end
def count(card)
@cards[card]
end
def any?(card)
count(card) > 0
end
def odds_of(card)
@cards[card].to_f / @card_count
end
def consume(card)
current = @cards[card]
raise "error, consuming non-existant card" if current <= 0
@cards[card] = current - 1
@card_count -= 1
end
def replace(card)
@cards[card] += 1
@card_count += 1
end
end
# SET UP VARIABLES
# The shoe is a Hash that contains one or more decks and an embedded
# count of how many cards there are in the shoe (keyed by
# :cards_in_shoe)
shoe = Shoe.new(deck_count)
# The results for a given upcard is a hash keyed by the result and
# with values equal to the odds that that result is acheived.
results_for_upcard =
POSSIBLE_RESULTS.inject(Hash.new) { |hash, r| hash[r] = 0; hash }
# The final results is a hash keyed by every possible upcard, and with
# a value equal to results_for_upcard.
results = CARDS.inject(Hash.new) { |hash, card|
hash[card] = results_for_upcard.dup; hash }
# METHODS
# returns the value of a hand
def value(hand)
ace_count = 0
hand_value = 0
hand.each do |card|
if card == :ace
ace_count += 1
hand_value += 11
else
hand_value += card
end
end
# flip aces from being worth 11 to being worth 1 until we get <= 21
# or we run out of aces
while hand_value > 21 && ace_count > 0
hand_value -= 10
ace_count -= 1
end
hand_value
end
# the dealer decides what to do -- stands on 17 or above, hits
# otherwise
def decide(hand)
value(hand) >= 17 && :stand || :hit
end
# computes the result of a hand, returning a numeric value, :natural,
# or :bust
def result(hand)
v = value(hand)
case v
when 21 : hand.size == 2 && :natural || 21
when 17..20 : v
when 0..16 : raise "error, illegal resulting hand value"
else :bust
end
end
# plays the dealer's hand, tracking all possible permutations and
# putting the results into the results hash
def play_dealer(hand, shoe, odds, upcard_result)
case decide(hand)
when :stand
upcard_result[result(hand)] += odds
when :hit
CARDS.each do |card|
next unless shoe.any?(card)
card_odds = shoe.odds_of(card)
hand.push(card)
shoe.consume(card)
play_dealer(hand, shoe, odds * card_odds , upcard_result)
shoe.replace(card)
hand.pop
end
else
raise "error, illegal hand action"
end
end
# MAIN PROGRAM
# calculate results
CARDS.each do |upcard|
shoe.consume(upcard)
play_dealer([upcard], shoe, 1, results[upcard])
shoe.replace(upcard)
end
# display results header
puts "Note: results are computed using a fresh %d-deck shoe.\n\n" %
deck_count
printf "upcard "
POSSIBLE_RESULTS.each do |result|
printf "%9s", result.to_s
end
puts
printf "-" * 6 + " "
POSSIBLE_RESULTS.length.times do
print " " + "-" * 7
end
puts
# display numeric results
CARDS.each do |upcard|
printf "%6s |", upcard
POSSIBLE_RESULTS.each do |result|
printf "%8.2f%%", 100.0 * results[upcard][result]
end
puts
end