[QUIZ] Studying Blackjack (#151)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

The majority of the strategy in Blackjack hinges around the dealer's hand. The
reasons are likely obvious to most of you: that's the hand you have to beat and
the dealer plays by fixed rules we can predict.

For those unfamiliar with Blackjack, you only need to know a tiny bit about the
game for the purposes of this exercise. The goal for both the player and the
dealer is to draw cards to make a hand with the highest total possible, without
going over 21. Going over 21 is called "busting" and it means you lose the
hand. Face cards count for ten, aces are one or eleven (whichever is better for
the hand), and all other cards count for their face value. You start with two
cards and, if they happen to be a ten valued card and an ace (a count of 21),
the hand is called a "natural." A natural is an automatic win in most cases.

The dealer begins with one of his two cards face up and one face down. We call
the former the "upcard." The dealer will "hit" or take more cards until he
reaches a count of 17 or higher. After that he will "stand" or leave the hand
where it is. That tells us that there are only seven possible outcomes for the
dealer: get dealt a natural, bust, or hit to a total of 17, 18, 19, 20, or 21.

We start every hand knowing half of what the dealer holds thanks to the upcard.
Believe it or not, you can make pretty reliable guesses about how the hand will
go with just that knowledge.

Write a Ruby program that shows the percent chance of a dealer reaching each
possible outcome based on the upcard showing.

I'll give you some hints to verify your results. Basic Blackjack strategy
teaches that we should assume the dealer "has a ten in the hole" (as the face
down card). It's not always true, of course, but 17 is a common outcome for a
dealer with an upcard of seven. Finally, we call five and six "the dealer's
bust cards" for reasons that will become obvious if you are outputting correct
percentages.

In the casinos Blackjack is often played with more than one deck shuffled
together. One, two, six, and eight deck games are common. You may want to
offer the option to adjust the deck size your program uses. Either way, let's
default to two decks as an average of what a player will face.

Write a Ruby program that shows the percent chance of a dealer reaching each
possible outcome based on the upcard showing

Are we able to post partial / full outcomes within the 48 hours?

Cheers,
Dave

Write a Ruby program that shows the percent chance of a dealer
reaching each possible outcome based on the upcard showing.

I'm sorry for asking this dummy question (and I hope not to spoil it
for
anyone) but all this jargon makes me feel strange and it's early in
the
morning. Do I understand the game right in that:

    1. The dealer gets a card
    2. If the sum of his cards is < 17, repeat step #1
    3. If the sum is >= 17, the result is registered
    4. If the dealer has 6 + Ace, he stops and doesn't try to get a 4
or
       something lower?

The set of possible cards is limited to 2 decks, i.e. 2 * 13 (2..10,
B,
D, K, A) cards by default.

A set of two card decks contains two 2s?

The probabilities for 10, B, D, K should be all the same since they
all
count 10 and removing them from the card set results in the same
probability
to get another card with a value of 10 for each of them?

Here's my solution, with a sample run.

Comments, corrections, criticism welcome.
/dh

$ ./blackjack.rb
Odds for each dealer outcome based on initial upcard (2 deck game)
      17 18 19 20 21 BUST
  A 12.58% 12.82% 12.75% 12.85% 36.07% 12.93%
  2 13.93% 13.33% 13.07% 12.39% 11.92% 35.36%
  3 13.27% 13.06% 12.45% 12.18% 11.53% 37.50%
  4 13.07% 12.02% 12.10% 11.63% 11.31% 39.88%
  5 12.10% 12.28% 11.73% 10.90% 10.73% 42.25%
  6 16.62% 10.62% 10.67% 10.12% 9.75% 42.21%
  7 37.05% 13.82% 7.80% 7.88% 7.34% 26.11%
  8 12.97% 36.12% 12.90% 6.89% 6.96% 24.16%
  9 12.09% 11.20% 35.41% 12.11% 6.10% 23.09%
10 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%
  J 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%
  Q 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%
  K 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%

$ cat blackjack.rb
#!/usr/bin/env ruby

CARDS = %w(A 2 3 4 5 6 7 8 9 10 J Q K)
DECKS = ARGV.size == 1 ? ARGV[0].to_i : 2
SUITS = 4

def hand_value(hand)
   value = 0
   # First calculate values ignoring aces
   hand.each do |c|
     if c=='A'
       next
     elsif 'JQK'.include? c
       value += 10
     else
       value += c.to_i
     end
   end
   # Then add aces as 11 unless they would bust the hand
   hand.each do |c|
     if c=='A'
       if value>10
         value += 1
       else
         value += 11
       end
     end
   end
   value
end

def new_shute
   cards = []
   CARDS.each do |c|
     DECKS.times { SUITS.times { cards << c }}
   end
   cards
end

def odds_of(cards, v)
   count = 0
   cards.each { |c| count += 1 if c==v }
   (1.0 * count) / cards.length
end

# calc the odds of reaching result from a given hand
def calc_odds(hand, result)
   current = hand_value(hand)
   return 1.0 if current == result
   return 0.0 if current >= 17

   # Remove hand cards from full shute
   cards = new_shute
   hand.each {|c| cards.delete_at(cards.index(c))}

   odds = 0.0
   CARDS.each do |c|
     odds_of_card = odds_of(cards, c)
     if odds_of_card > 0.0
       hand.push c
       odds_of_result = calc_odds(hand, result)
       odds += odds_of_card * odds_of_result
       hand.pop
     end
   end

   return odds
end

puts "Odds for each dealer outcome based on initial upcard (#{DECKS} deck game)"

puts " 17 18 19 20 21 BUST"
CARDS.each do |c|
   odds = {}
   bust = 100.0
   (17..21).each do |r|
     odds[r] = calc_odds([c], r) * 100.0
     bust -= odds[r]
   end
   printf "%2s %5.02f%% %5.02f%% %5.02f%% %5.02f%% %5.02f%% %5.02f%%\n", c, odds[17], odds[18], odds[19], odds[20], odds[21], bust
end

Here's my solution. I'm particularly interested in comments on my
initialize method in the Dealer class. Is there a better way to make
the deck object available to the methods in this class ?

$ ruby quiz_151.rb
Upcard Bust 17 18 19 20 21 Natural
c2 34.90% 14.20% 13.38% 13.30% 12.16% 12.06% 0.00%
c3 37.30% 13.68% 12.68% 13.34% 11.56% 11.44% 0.00%
c4 39.88% 14.22% 11.74% 11.82% 11.08% 11.26% 0.00%
c5 41.88% 11.46% 11.94% 12.08% 11.86% 10.78% 0.00%
c6 42.30% 16.54% 11.06% 10.64% 10.00% 9.46% 0.00%
c7 26.82% 35.92% 13.12% 8.70% 7.94% 7.50% 0.00%
c8 23.84% 13.30% 35.72% 13.14% 6.60% 7.40% 0.00%
c9 22.94% 11.68% 12.46% 34.48% 12.24% 6.20% 0.00%
ct 21.32% 11.12% 11.56% 11.18% 34.06% 3.48% 7.28%
cj 20.98% 11.18% 11.28% 10.60% 34.14% 3.82% 8.00%
cq 21.40% 11.24% 11.34% 10.72% 34.36% 3.48% 7.46%
ck 20.54% 10.36% 10.38% 11.32% 35.72% 3.64% 8.04%
ca 13.08% 13.04% 12.96% 13.56% 11.92% 5.48% 29.96%

$ cat quiz_151.rb
#!/usr/bin/env ruby -w

class Deck
  def initialize(number_of_decks)
    @cards = []

    suits = ["h","c","d","s"]
    values = [2,3,4,5,6,7,8,9,"t","j","q","k","a"]

    number_of_decks.times do
      suits.each do |suit|
        values.each do |value|
          @cards << suit + value.to_s
        end
      end
    end
    shuffle
  end

  def shuffle
    @cards = @cards.sort_by {rand}
  end

  def deal
    @cards.pop
  end

  def deal_a(card)
    # Deal a named card from the deck
    @cards.delete_at(@cards.index(card))
  end
end

class Dealer

  def initialize(deck,upcard)
    @hand = []
    @score = 0
    @hand << deck.deal_a(upcard)
    @hand << deck.deal
    @deck = deck
  end

  def bust?
    current_score > 21
  end

  def natural?
    current_score == 21 && @hand.length == 2
  end

  def current_score

    # To deal with multiple aces, sort the current hand so that the
    # aces appear as the last elements in the array.
    values = []
    @hand.each {|card| values << card[1].chr}
    not_aces = values.find_all {|v| /[^a]/=~v}
    aces = values.find_all {|v| /[a]/=~v}

    values = not_aces + aces

    # Calculate the score for this hand
    score = 0
    values.each do |value|
      if /\d/ =~ value then score += value.to_i end
      if /[t,k,j,q]/ =~ value then score += 10 end
      if /[a]/ =~ value then
        if score + 11 > 21
          score += 1
        elsif
          score += 11
        end
      end
    end
    score
  end

  def play
    until self.bust? || current_score >= 17
      card = @deck.deal
      @hand << card
    end

    if self.bust?
      "bust"
    elsif self.natural?
      "natural"
    else
      current_score
    end
  end
end

if __FILE__ == $0
  upcards =
["c2","c3","c4","c5","c6","c7","c8","c9","ct","cj","cq","ck","ca"]
  outcomes = ["bust",17,18,19,20,21,"natural"]

  no_of_games = 5000
  printf("Upcard\tBust\t17\t18\t19\t20\t21\tNatural\n")
  upcards.each do |upcard|
    results = []
    no_of_games.times {results << Dealer.new(Deck.new(8),upcard).play}

    p = []
    outcomes.each do |outcome|
      number = results.find_all {|r| r==outcome}
      p << (number.length.to_f/no_of_games)*100
    end

    printf("%s\t%5.2f%%\t%5.2f%%\t%5.2f%%\t%5.2f%%\t%5.2f%%\t%5.2f%%\t
%5.2f%%\n",
           upcard,p[0],p[1],p[2],p[3],p[4],p[5],p[6])
  end
end

Here is my solution. I originally tried to run through all solutions
but
then adopted the simulation strategy already mentioned by others
before.

If sample size is 55000 (default is 10000), this script runs about as
long (with ruby 1.8) as Denis Hennessy's solution (which uses much
less
memory though). Also, I get constantly less bust counts for an Ace as
upcard which makes me wonder if I did something wrong here. The other
figures appear about the same.

Sample results:

$ ruby quiz151b.rb 55000
    bust natural 17 18 19 20 21
2: 35.43% 0.00% 13.84% 13.45% 12.88% 12.35% 12.05%
3: 37.39% 0.00% 13.56% 12.85% 12.73% 12.03% 11.43%
4: 40.08% 0.00% 12.85% 11.97% 12.11% 11.72% 11.28%
5: 42.21% 0.00% 12.25% 12.19% 12.02% 10.85% 10.48%
6: 41.83% 0.00% 16.71% 10.58% 10.74% 10.45% 9.68%
7: 26.29% 0.00% 36.96% 13.76% 7.76% 7.90% 7.32%
8: 24.48% 0.00% 13.05% 35.85% 12.92% 6.63% 7.06%
9: 23.39% 0.00% 11.97% 11.10% 35.50% 11.94% 6.11%
10: 21.21% 7.85% 11.01% 11.12% 11.52% 33.77% 3.52%
B: 21.15% 7.71% 11.26% 11.11% 11.50% 33.77% 3.49%
D: 21.36% 7.73% 11.25% 11.08% 11.32% 33.74% 3.52%
K: 21.65% 7.77% 11.47% 11.28% 11.17% 33.16% 3.51%

Regards,
Thomas.

#!/usr/bin/env ruby
# Author:: Thomas Link (micathom AT gmail com)
# Created:: 2008-01-05.

class Quiz151b
    LABELS = ['bust', 'natural', *(17..21).to_a]
    NAMES = ['A', *(2..10).to_a] << 'B' << 'D' << 'K'
    CARDS = (1..10).to_a + [10] * 3

    class << self
        def run(sample=10000, decks=2)
            puts ' ' + LABELS.map {|k| '%-7s' % k}.join(' ')
            13.times do |upcard|
                puts Quiz151b.new(upcard, decks).run(sample)
            end
        end
    end

    def initialize(upcard, decks)
        @upcard = upcard
        @cards = CARDS * (4 * decks)
        @hands = []
    end

    def run(sample)
        sample.times {@hands << deal(@upcard)}
        self
    end

    def to_s
        total = @hands.size
        acc = Hash.new(0)
        @hands.each do |sum, hand|
            label = sum > 21 ? 'bust' :
                sum == 21 && hand.size == 2 ? 'natural' :
                sum
            acc[label] += 1
        end
        '%02s: %s' % [
            NAMES[@upcard],
            LABELS.map {|k| '%6.2f%%' % (100.0 * acc[k] /
total)}.join(' ')
        ]
    end

    def deal(idx)
        cards = @cards.dup
        hand = []
        sum = 0
        loop do
            hand << cards.delete_at(idx)
            sum = count(hand)
            return [sum, hand] if sum >= 17
            idx = rand(cards.size)
        end
    end

    def count(hand)
        sum = 0
        tidx = 21 - hand.size - 10
        hand.dup.sort.reverse.each_with_index do |c, i|
            sum += c == 1 && sum <= tidx + i ? 11 : c
        end
        return sum
    end

end

if __FILE__ == $0
    case ARGV[0]
    when '-h', '--help'
        puts "#$0 [DEALS=10000] [DECKS=2]"
    else
        Quiz151b.run(*ARGV.map {|e| e.to_i})
    end
end

···

A: 11.62% 31.09% 12.65% 13.09% 12.88% 13.33% 5.35%

Here is my solution. It's just another simulation.
Ruby 1.9 only.

Usage: ruby1.9 [upcard] [number of decks] [number of games]

Pastie: http://pastie.caboo.se/135733

Code:

class Array
  def score
    sort.inject(0){|s,c| s+c > 21 && c==11 ? s+1 : s+c }
  end
end

unless ARGV[0]
(2..11).each{|n| puts `ruby1.9 #{__FILE__} #{n}`}
exit
end

puts "upcard: #{upcard = ARGV[0].to_i}"
NDECKS = (ARGV[1]||2).to_i
CARDS = (((2..11).to_a+[10]*3)*4*NDECKS).tap{|c| c.delete_at c.index(upcard)}

score_count = [0]*27
cards = []
N=(ARGV[2]||100_000).to_i
N.times{
cards = CARDS.dup.shuffle if cards.size < 17
dealer = [upcard]
dealer << cards.pop while dealer.score < 17
score_count[dealer.score] += 1
}

puts %w[17 18 19 20 21 bust].join(' ')
puts (score_count[17..21] << score_count[22..-1].inject(:+)).map{|x|
'%-4.1f%% ' % (100.0*x / N )}.join

You bet. Feel free.

James Edward Gray II

···

On Jan 4, 2008, at 4:27 PM, Sharon Phillips wrote:

Write a Ruby program that shows the percent chance of a dealer reaching each
possible outcome based on the upcard showing

Are we able to post partial / full outcomes within the 48 hours?

The set of possible cards is limited to 2 decks, i.e. 2 * 13 (2..10,
B,
D, K, A) cards by default.

It's * 4 because there are 4 colours.

Write a Ruby program that shows the percent chance of a dealer
reaching each possible outcome based on the upcard showing.

I'm sorry for asking this dummy question (and I hope not to spoil it
for
anyone) but all this jargon makes me feel strange and it's early in
the
morning.

No worries. I probably didn't explain it well.

Do I understand the game right in that:

   1. The dealer gets a card
   2. If the sum of his cards is < 17, repeat step #1
   3. If the sum is >= 17, the result is registered
   4. If the dealer has 6 + Ace, he stops and doesn't try to get a 4
or
      something lower?

Yes to all of the above.

The forth rule actually varies somewhat in the casinos. Some tables allow a dealer to hit on "soft 17" (a total of 17 involving an ace used as an eleven). I left that out to keep this exercise simple.

The set of possible cards is limited to 2 decks, i.e. 2 * 13 (2..10,
B,
D, K, A) cards by default.

A set of two card decks contains two 2s?

I meant to do the experiment using two full decks shuffled together. That would be a total of 104 cards and include eight twos, threes, etc.

I hope that clears things up.

James Edward Gray II

···

On Jan 5, 2008, at 2:05 AM, tho_mica_l wrote:

Here's my solution, which, like Denis Hennessy's solution, attempts to
calculate the exact results by looking at all potential, meaningful
permutations of a fresh shoe. By *meaningful* permutations, I mean
that once a result is determined (e.g., 20, bust), the order of the
remaining cards doesn't matter, and so doesn't need to be taken into
account.

The results Denis and I calculate differ slightly. I still haven't
figured out why....

Eric

···

----

Interested in hands-on, on-site Ruby training? See http://LearnRuby.com
for information about a well-reviewed class.

====

# 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 .

# mathn provides us with fractional (rational) results for partial
# calculations rather than floating point results, which can be
# subject to rounding errors. Rounding takes place at the point of
# final output.
require 'mathn'

# 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

# 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

# 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]

# 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 = DECK.inject(Hash.new) { |hash, card|
  hash[card.first] = deck_count * card.last; hash }
shoe[:cards_in_shoe] =
  shoe.inject(0) { |sum, card_count| sum + card_count.last }

# 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

# manages the consumption of a specific card from the shoe
def shoe_consume(shoe, card)
  current = shoe[card]
  raise "error, consuming non-existant card" if current <= 0
  shoe[card] = current - 1
  shoe[:cards_in_shoe] -= 1
end

# manages the replacement of a specific card back into the shoe
def shoe_replace(shoe, card)
  shoe[card] += 1
  shoe[:cards_in_shoe] += 1
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|
      count = shoe[card]
      next if count == 0
      card_odds = count / shoe[:cards_in_shoe]

      hand.push(card)
      shoe_consume(shoe, card)

      play_dealer(hand, shoe, odds * card_odds , upcard_result)

      shoe_replace(shoe, card)
      hand.pop
    end
  else
    raise "error, illegal hand action"
  end
end

# MAIN PROGRAM

# calculate results

CARDS.each do |upcard|
  shoe_consume(shoe, upcard)
  play_dealer([upcard], shoe, 1, results[upcard])
  shoe_replace(shoe, 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.each do |result|
  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

Hi All,

Below is my solution. I used simulation strategy, too but I understand one simulation as passing through all cards until set of decks is empty. When ran through 1000 simulations it showed me much more chance (22%) of getting bust while having an ace as an upcard. Remaining results differ but not that much...

Results of three example upcards:

A:
  natural -> 32%
     bust -> 22%
       20 -> 11%
       18 -> 11%
       17 -> 11%
       19 -> 11%
       21 -> 3%

7:
       17 -> 37%
     bust -> 25%
       18 -> 14%
       19 -> 8%
       20 -> 8%
       21 -> 7%
6:
     bust -> 39%
       17 -> 17%
       19 -> 12%
       18 -> 11%
       20 -> 10%
       21 -> 10%

Source code:

#!/usr/bin/env ruby

# Solution to Ruby Quiz #151 (see http://www.rubyquiz.com/quiz151.html)
# by Pawel Radecki (pawel.j.radecki@gmail.com).

COLOURS_IN_DECK = 4
SIMULATIONS_NO = 1000

class Array
     def shuffle
         sort_by { rand }
     end
end

class Card

     attr_reader :face

     @@blackjack_values = { "A" => [1,11] , "K" => 10, "Q" => 10, "J" => 10,
             "10" => 10, "9" => 9, "8" => 8, "7" => 7, "6" => 6, "5" => 5, "4" => 4,
             "3" => 3, "2" => 2}

     @@list = ["A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3", "2" ]

     def initialize(face)
         if @@blackjack_values.keys.include? face
             @face=face
         else
             raise Exception.new("Can't initialize card with face: "+face)
         end
     end

     def blackjack_value
         @@blackjack_values[@face]
     end

     def best_blackjack_value(score)
         if (self.blackjack_value.respond_to? :pop)
             if (score>10)
                 self.blackjack_value[0]
             else
                 self.blackjack_value[1]
             end
         else
             self.blackjack_value
         end
     end

     def self.faces
         @@blackjack_values.keys
     end

     def self.list
         @@list
     end

     def to_s
         return "#{@face}"
     end

     def inspect
         return "#{@face}"
     end
end

#one or more decks
class DeckSet

     #new shuffled deck
     def initialize (decks_no=2)
         @cards = []

         (decks_no*COLOURS_IN_DECK).times do
             Card.faces.shuffle.each {|c| @cards << Card.new(c)}
         end
     end

     def draw
         @cards.pop
     end

     def empty?
         @cards.empty?
     end
end

USAGE = <<ENDUSAGE
Usage:
    black_jack_dealer_chances.rb [-u <upcard>] [-d <decks_no>]
    -u upcard: {#{Card.list.join(", ")}}
    -d number of decks used

    Calculates percentage chances of a black jack dealer reaching each possible outcome.
    Upcard may be given, number of the decks may be configured.

    Example: black_jack_dealer_chances.rb -u "Q" -d 5
ENDUSAGE

if ARGV.length>4
     puts USAGE
     exit
end

upcard = nil
decks_no = 2

if ARGV.include?("-u")
     upcard = ARGV[ARGV.index("-u")+1]
     if (upcard.nil? || !Card.faces.include?(upcard))
         puts USAGE
         exit
     end
     ARGV.delete("-u")
     ARGV.delete(upcard)
end

if ARGV.include?("-d")
     decks_no = ARGV[ARGV.index("-d")+1]
     if (decks_no.nil?)
         puts USAGE
             exit
     end
     ARGV.delete("-d")
     ARGV.delete(decks_no)
end

histogram = Hash.new 0
sum = Hash.new 0
probability = []

SIMULATIONS_NO.times do
     decks = DeckSet.new(decks_no.to_i)
     while (!decks.empty?)
         score = 0; hand = []
         while score<17
             hand << card=decks.draw
             score+=card.best_blackjack_value(score)

             if score==21 && hand.size==2
                 if $DEBUG
                     print "hand: "
                     p hand
                     print "score: "
                     p score
                     puts
                 end
                 sum[hand.first.face]+=1
                 histogram[[hand.first.face,"natural"]]+=1
                 break
             elsif score>21
                 if $DEBUG
                     print "hand: "
                     p hand
                     print "score: "
                     p score
                     puts
                 end
                 sum[hand.first.face]+=1
                 histogram[[hand.first.face,"bust"]]+=1
                 break
             elsif (17..21).include? score
                 if $DEBUG
                     print "hand: "
                     p hand
                     print "score: "
                     p score
                     puts
                 end
                 sum[hand.first.face]+=1
                 histogram[[hand.first.face,score]]+=1
                 break
             elsif decks.empty?
                 break
             end

         end
     end
end

histogram.keys.each { |el| probability << [el,histogram[el].to_f/sum[el.first]].flatten }
probability.sort! { |x,y| x.first != y.first ? Card.list.index(x.first) <=> Card.list.index(y.first) : y.last <=> x.last}

card = nil
probability.each do |el|
     if (upcard==nil || el.first==upcard)
         if card!=el.first
             card=el.first
             puts "#{el.first}:"
         end
             printf("%8s -> %2.0f%% \n", el[1], el.last*100)
     end
end

exit

···

--
Pawel Radecki
m: +48 695 34-64-76
e: pawel.j.radecki@gmail.com
w: http://radeckimarch.blogspot.com/

class Array
def score
   sort.inject(0){|s,c| s+c > 21 && c==11 ? s+1 : s+c }
end
end

That has the some bug Eric found in Dennis's code:

>> [10, 11, 11].score
=> 22

unless ARGV[0]
(2..11).each{|n| puts `ruby1.9 #{__FILE__} #{n}`}
exit
end

Clever trick. I like that.

James Edward Gray II

···

On Jan 6, 2008, at 9:42 AM, Sander Land wrote:

> Are we able to post partial / full outcomes within the 48 hours?
You bet. Feel free.

Here's mine so far, my first ruby quiz !

Upcard Bust 17 18 19 20 21 Natural
c1 26.20% 16.80% 13.00% 14.20% 13.90% 15.90% 0.00%
c2 38.80% 14.60% 11.40% 11.00% 12.30% 11.90% 0.00%
c3 40.50% 14.40% 13.40% 11.20% 12.00% 8.50% 0.00%
c4 38.00% 15.20% 14.30% 11.70% 10.70% 10.10% 0.00%
c5 42.30% 12.80% 12.30% 11.80% 9.20% 11.60% 0.00%
c6 39.10% 23.50% 10.30% 10.00% 8.80% 8.30% 0.00%
c7 25.60% 35.40% 14.00% 10.70% 6.50% 7.80% 0.00%
c8 23.70% 13.00% 33.60% 16.20% 6.20% 7.30% 0.00%
c9 21.90% 10.50% 11.90% 35.90% 12.90% 6.90% 0.00%
ct 19.40% 12.50% 10.90% 11.20% 31.70% 5.10% 9.20%
cj 19.70% 14.40% 10.60% 12.30% 29.90% 5.80% 7.30%
cq 18.90% 13.50% 10.90% 10.30% 32.20% 6.60% 7.60%
ck 19.50% 12.30% 12.80% 11.10% 31.90% 6.00% 6.40%
ca 14.00% 13.40% 14.40% 13.10% 12.60% 4.10% 28.40%

Seems to satisfy the hints given in the quiz, but I'm still not sure
if I deal with the case where the dealer gets dealt two or more aces
correctly.

Chris

Here's my results so far. I don't completely trust them yet since I'm not sure I understand the blackjack rules properly (I'm assuming the dealer has to use an ace as 11 unless they bust).

/dh

$ ./blackjack.rb
Odds for each dealer outcome based on initial upcard (2 deck game)
      17 18 19 20 21 BUST
  A 12.58% 12.82% 12.75% 12.85% 36.07% 12.93%
  2 13.93% 13.33% 13.07% 12.39% 11.92% 35.36%
  3 13.27% 13.06% 12.45% 12.18% 11.53% 37.50%
  4 13.07% 12.02% 12.10% 11.63% 11.31% 39.88%
  5 12.10% 12.28% 11.73% 10.90% 10.73% 42.25%
  6 16.62% 10.62% 10.67% 10.12% 9.75% 42.21%
  7 37.05% 13.82% 7.80% 7.88% 7.34% 26.11%
  8 12.97% 36.12% 12.90% 6.89% 6.96% 24.16%
  9 12.09% 11.20% 35.41% 12.11% 6.10% 23.09%
10 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%
  J 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%
  Q 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%
  K 11.29% 11.22% 11.30% 33.56% 11.31% 21.32%

···

On 4 Jan 2008, at 22:31, James Gray wrote:

On Jan 4, 2008, at 4:27 PM, Sharon Phillips wrote:

Write a Ruby program that shows the percent chance of a dealer reaching each
possible outcome based on the upcard showing

Are we able to post partial / full outcomes within the 48 hours?

You bet. Feel free.

James Edward Gray II

Well, it looks like I over-engineered my solution. After noting that
Denis' updated solution and James Koppel's solution were providing the
same results as my solution, and that they were using floating point
math, I realized that my use of rational math to avoid rounding errors
and gain higher precision wasn't paying off.

So by making a change to one line in my play_dealer method:

< card_odds = count / shoe[:cards_in_shoe]

···

On Jan 6, 11:55 am, "Eric I." <rubytrain...@gmail.com> wrote:

# mathn provides us with fractional (rational) results for partial
# calculations rather than floating point results, which can be
# subject to rounding errors. Rounding takes place at the point of
# final output.
require 'mathn'

---

      card_odds = count.to_f / shoe[:cards_in_shoe]

my output is identical, and the program is over five times faster.
I'm a big fan of the mathn library, but clearly I should be more
careful about using it.

Eric

Thanks. I should have thought of this considering Dennis' results matched mine.
Here's my corrected solution. I renamed the ace to "1" for a more
elegant solution. :slight_smile:

class Array
  def score
    (s=inject(:+)) <= 11 && index(1) ? s+10 : s
  end
end

unless ARGV[0]
(1..10).each{|n| puts `ruby1.9 #{__FILE__} #{n}`}
exit
end

puts "upcard: #{upcard = ARGV[0].to_i}"
NDECKS = (ARGV[1]||2).to_i
CARDS = (((1..10).to_a+[10]*3)*4*NDECKS).tap{|c| c.delete_at c.index(upcard)}

score_count = [0]*27
cards =
N=(ARGV[2]||1_000_000).to_i
N.times{
cards = CARDS.dup.shuffle if cards.size < 17
dealer = [upcard]
dealer << cards.pop while dealer.score < 17
score_count[dealer.score] += 1
}

puts %w[17 18 19 20 21 bust].join(' ')
puts (score_count[17..21] << score_count[22..-1].inject(:+)).map{|x|
'%-4.1f%% ' % (100.0*x / N )}.join

Results match Eric's results now.

upcard: 1
17 18 19 20 21 bust
12.8% 13.1% 13.0% 13.1% 36.3% 11.6%
upcard: 2
17 18 19 20 21 bust
14.0% 13.4% 13.1% 12.3% 11.9% 35.3%
upcard: 3
17 18 19 20 21 bust
13.3% 13.2% 12.5% 12.2% 11.5% 37.3%
upcard: 4
17 18 19 20 21 bust
13.1% 12.1% 12.1% 11.6% 11.3% 39.8%
upcard: 5
17 18 19 20 21 bust
12.2% 12.3% 11.7% 10.8% 10.7% 42.2%
upcard: 6
17 18 19 20 21 bust
16.6% 10.7% 10.7% 10.1% 9.8 % 42.1%
upcard: 7
17 18 19 20 21 bust
36.9% 13.9% 7.8 % 7.9 % 7.4 % 26.0%
upcard: 8
17 18 19 20 21 bust
12.9% 36.0% 12.9% 6.9 % 7.0 % 24.3%
upcard: 9
17 18 19 20 21 bust
12.1% 11.2% 35.3% 12.1% 6.1 % 23.1%
upcard: 10
17 18 19 20 21 bust
11.2% 11.2% 11.3% 33.6% 11.3% 21.3%

···

On Jan 9, 2008 9:10 PM, James Gray <james@grayproductions.net> wrote:

On Jan 6, 2008, at 9:42 AM, Sander Land wrote:

> class Array
> def score
> sort.inject(0){|s,c| s+c > 21 && c==11 ? s+1 : s+c }
> end
> end

That has the some bug Eric found in Dennis's code:

>> [10, 11, 11].score
=> 22

Here are my results. I combined the 10 and the face cards in one row
since they're essentially equivalent. And the data was generated by
doing 10,000 deals per upcard.

upcard bust 17 18 19 20 21
natural
------ ------- ------- ------- ------- ------- -------

···

-------
     2 | 35.95% 13.32% 13.17% 13.34% 12.44% 11.78%
0.00%
     3 | 37.85% 13.41% 12.93% 11.69% 12.51% 11.61%
0.00%
     4 | 40.14% 13.24% 11.86% 12.03% 11.48% 11.25%
0.00%
     5 | 41.80% 12.82% 12.32% 11.41% 11.34% 10.31%
0.00%
     6 | 41.76% 17.65% 10.59% 10.36% 10.04% 9.60%
0.00%
     7 | 26.77% 36.23% 13.53% 8.29% 7.88% 7.30%
0.00%
     8 | 24.96% 12.84% 35.69% 12.73% 7.04% 6.74%
0.00%
     9 | 22.98% 12.27% 10.69% 35.47% 12.22% 6.37%
0.00%
    10 | 21.61% 11.21% 11.68% 11.23% 32.97% 3.55%
7.75%
   ace | 11.60% 12.33% 13.03% 13.06% 13.43% 5.27%
31.28%

I do wonder, though, whether the deck is biased somehow when the
dealer deals him/herself since all the players are dealt beforehand.
For example, it seems a player is more likely to consume a sequence of
low cards through consecutive hits than a sequence of high cards. So
depending on the number of players at the table, the cards the dealer
is likely to get may change somewhat. And perhaps the dealer's odds
change further the deeper into the shoe the table gets. Does anyone
know?

Eric

====

Are you interested in on-site Ruby training that uses well-designed,
real-world, hands-on exercises? http://LearnRuby.com

Are we able to post partial / full outcomes within the 48 hours?

You bet. Feel free.

Here's mine so far, my first ruby quiz !

Welcome to the quiz. Glad to have you.

…but I'm still not sure if I deal with the case where the dealer gets dealt two or more aces correctly.

If a dealer has two aces, the first one would be 11 and the second one a 1 for a total of twelve. Any aces added into the mix would be another one. The original aces would drop to a one if keeping it at 11 takes the dealer over 21.

James Edward Gray II

···

On Jan 4, 2008, at 6:35 PM, Chris wrote:

Hi Chris,

You have an extra card in your hand (a '1' as well as an ace).

/dh

···

On 5 Jan 2008, at 00:35, Chris wrote:

Are we able to post partial / full outcomes within the 48 hours?

You bet. Feel free.

Here's mine so far, my first ruby quiz !

Upcard Bust 17 18 19 20 21 Natural
c1 26.20% 16.80% 13.00% 14.20% 13.90% 15.90% 0.00%
c2 38.80% 14.60% 11.40% 11.00% 12.30% 11.90% 0.00%
c3 40.50% 14.40% 13.40% 11.20% 12.00% 8.50% 0.00%
c4 38.00% 15.20% 14.30% 11.70% 10.70% 10.10% 0.00%
c5 42.30% 12.80% 12.30% 11.80% 9.20% 11.60% 0.00%
c6 39.10% 23.50% 10.30% 10.00% 8.80% 8.30% 0.00%
c7 25.60% 35.40% 14.00% 10.70% 6.50% 7.80% 0.00%
c8 23.70% 13.00% 33.60% 16.20% 6.20% 7.30% 0.00%
c9 21.90% 10.50% 11.90% 35.90% 12.90% 6.90% 0.00%
ct 19.40% 12.50% 10.90% 11.20% 31.70% 5.10% 9.20%
cj 19.70% 14.40% 10.60% 12.30% 29.90% 5.80% 7.30%
cq 18.90% 13.50% 10.90% 10.30% 32.20% 6.60% 7.60%
ck 19.50% 12.30% 12.80% 11.10% 31.90% 6.00% 6.40%
ca 14.00% 13.40% 14.40% 13.10% 12.60% 4.10% 28.40%

Seems to satisfy the hints given in the quiz, but I'm still not sure
if I deal with the case where the dealer gets dealt two or more aces
correctly.

Chris