Studying Blackjack (#151)

On Jan 5, 2008, at 4:02 PM, Rick DeNatale wrote:

In the old-way players could gather information as cards
were removed from play, of course it’s quite a skill to remember the
information and be able to mentally keep track of the effect on the
odds.

This isn’t really true, though it’s a myth both Hollywood and the
casinos try hard to keep up. There are numerous card counting systems
in common use today and, while they vary in difficulty level, several
easy systems are something most people can learn in a matter of hours
(though of course you’ll need practice to play it proficiently in a
casino).

I actually had the idea for this quiz while learning one of the easier
card counting systems. If you can add and subtract one to a single
running total and memorize less than 20 rules, you can learn the
system. Maybe I’ll make that next week’s quiz… :slight_smile:

James Edward G. II

After going the randomized simulations route, I was inspired by the
precise results you achieved by looking at all permutations. So I
decided to try using that technique myself. Here’s what I got:

upcard bust 17 18 19 20 21
natural



 2   |   35.33%   13.94%   13.33%   13.07%   12.40%   11.93%

0.00%
3 | 37.48% 13.28% 13.07% 12.46% 12.18% 11.54%
0.00%
4 | 39.85% 13.07% 12.02% 12.10% 11.64% 11.31%
0.00%
5 | 42.25% 12.10% 12.28% 11.73% 10.90% 10.73%
0.00%
6 | 42.21% 16.62% 10.62% 10.67% 10.12% 9.75%
0.00%
7 | 26.11% 37.05% 13.82% 7.80% 7.88% 7.34%
0.00%
8 | 24.16% 12.97% 36.12% 12.90% 6.89% 6.96%
0.00%
9 | 23.09% 12.09% 11.20% 35.41% 12.11% 6.10%
0.00%
10 | 21.32% 11.29% 11.22% 11.30% 33.56% 3.55%
7.77%
ace | 11.59% 12.85% 13.09% 13.02% 13.12% 5.28%
31.07%

Although very close, my results do differ slightly from yours. For
example you determine that when an ace is the upcard, there’s a 36.07%
chance of getting 21 exactly. I get 36.35% (31.07% natural + 5.28%
“unnatural”). On the other hand we both get a 21.32% chance of
busting when the upcard is a 10 or facecard. It’ll be interesting to
see why that’s the case when we post our code solutions.

Eric

====

Are you interested in on-site Ruby training that’s been highly
reviewed by former students? http://LearnRuby.com

Although very close, my results do differ slightly from yours. […]
It’ll be interesting to see why that’s the case when we post our code solutions.

When actually calculating permutations it could also be interesting
to
have the absolute numbers. Eg how many outcomes are possible when the
upcard
is an Ace? How many lead to a score of 17 etc. IMHO percentages are
more
interesting in the context of a simulation.

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©)}

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

On Jan 5, 7:38 pm, James G. [email protected] wrote:

On Jan 5, 2008, at 4:02 PM, Rick DeNatale wrote:
I actually had the idea for this quiz while learning one of the easier
card counting systems. If you can add and subtract one to a single
running total and memorize less than 20 rules, you can learn the
system. Maybe I’ll make that next week’s quiz… :slight_smile:

James Edward G. II

A card-counting-system evaluator would be interesting as a follow-on
to this weeks quiz. If you were considering different systems, you
would want to know which ones worked better (or at all) and how much
additional advantage you might gain for learning a more complicated
system, etc.

Regards,

Paul

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. It’s just another simulation.
Ruby 1.9 only.

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

Pastie: Parked at Loopia

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)4NDECKS).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

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 H.'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
A: 11.62% 31.09% 12.65% 13.09% 12.88% 13.33% 5.35%
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

Here’s my solution, which, like Denis H.'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 Ruby Quiz - Studying Blackjack (#151) 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

On Jan 6, 2008, at 7:40 AM, Paul N. wrote:

A card-counting-system evaluator would be interesting as a follow-on
to this weeks quiz. If you were considering different systems, you
would want to know which ones worked better (or at all) and how much
additional advantage you might gain for learning a more complicated
system, etc.

This has been well studied. The short story is that most of the
popular systems vary in expectations by pretty small amounts.

Given that, I personally prefer the easier systems. I’ll sacrifice a
few percentage points for easier counting.

James Edward G. II

On Jan 6, 12:02 am, “Eric I.” [email protected] wrote:

Although very close, my results do differ slightly from yours. For
example you determine that when an ace is the upcard, there’s a 36.07%
chance of getting 21 exactly. I get 36.35% (31.07% natural + 5.28%
“unnatural”). On the other hand we both get a 21.32% chance of
busting when the upcard is a 10 or facecard. It’ll be interesting to
see why that’s the case when we post our code solutions.

It took me a while, but after comparing Denis’ code and my own, I
figured out where the differences in results came from, and it was in
how aces are handled when valuing a hand.

I believe that Denis uses an incorrect algorithm. When valuing a
hand, he first sums up the values of all the non-aces and then makes a
second pass handling the aces. For each ace, if valuing it as 11
would not bust the hand, he values it at 11. Otherwise he values it
at 1.

But consider a three-card hand like ace, ace, 10. The first ace is
counted as 11 since that wouldn’t bust the hand. The second ace is
counted as 1, since valuing it as 11 also would bust the hand. But
the hand still busts due to the 10. If both aces were counted as 1,
then the hand would not be a bust (so far, at least), and it would
need for another hit.

When I changed Denis’ hand valuation logic on aces to my own, our
results were identical.

Eric

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 Q. #151 (see Ruby Quiz - Studying Blackjack (#151))

by Pawel R. ([email protected]).

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

But consider a three-card hand like ace, ace, 10. The first ace is
counted as 11 since that wouldn’t bust the hand. The second ace is
counted as 1, since valuing it as 11 also would bust the hand. But
the hand still busts due to the 10. If both aces were counted as 1,
then the hand would not be a bust (so far, at least), and it would
need for another hit.

I’m not sure if that’s what I do with my algorithm, but that’s what I
was trying to do,
by evaluating the score on every deal, and sorting the aces to the end
of the array to
value them. I need to compare other submissions with mine to be sure.

Chris

Hi Eric,

That’s interesting. The understanding I had was that a dealer could
‘re-value’ their hand with each card dealt; that is - if they draw two
aces they’re obviously 11 and 1 and they have to hit again. If they
then draw a card which would cause them to bust, they can treat both
aces as 1.

I don’t know this for definite, except that the description I’ve seen
for what a dealer does did not specify what order the ace appears in.
Maybe someone knows what really happens…

/dh

On Jan 6, 2008, at 3:03 PM, Denis H. wrote:

The understanding I had was that a dealer could ‘re-value’ their
hand with each card dealt; that is - if they draw two aces they’re
obviously 11 and 1 and they have to hit again. If they then draw a
card which would cause them to bust, they can treat both aces as 1.

You have all of that right, but Eric was saying you don’t properly
value hands with multiple aces in them. For example, ace, ace, and
ten should value as 12, not 22.

One way I’ve handled this in the pass was to sort the hand such that
aces are at the end, run through building up a total, and count an ace
as 11 when I passed it if doing so would keep the running count less
than or equal to 21 minus the cards left to count.

Hope that makes sense.

James Edward G. II

On 6 Jan 2008, at 22:40, James G. wrote:

One way I’ve handled this in the pass was to sort the hand such that
aces are at the end, run through building up a total, and count an
ace as 11 when I passed it if doing so would keep the running count
less than or equal to 21 minus the cards left to count.

Hope that makes sense.

James Edward G. II

Actually, I think my code is correct, but I might be snow-blind.
Here’s the relevant function. It values all the non-ace cards first
and then values the aces.

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

/dh

On 6 Jan 2008, at 23:54, Denis H. wrote:

value hands with multiple aces in them. For example, ace, ace, and

Actually, I think my code is correct, but I might be snow-blind.
Here’s the relevant function. It values all the non-ace cards first
and then values the aces.

def hand_value(hand)

… original function …

end

Gah! There was a bug - it valued aces after other cards but didn’t
take into account other aces. Here’s the corrected function which
works similarly to Eric’s code (value aces as 11 and later re-value as
1 as needed):

def hand_value(hand)
value = 0

First calculate values counting aces as 11

hand.each do |c|
if c==‘A’
value += 11
elsif ‘JQK’.include? c
value += 10
else
value += c.to_i
end
end

Then re-value aces as 1 as long as hand is bust

hand.each do |c|
if c==‘A’
if value>21
value -= 10
end
end
end
value
end

On Jan 6, 7:07 pm, Denis H. [email protected] wrote:

Gah! There was a bug - it valued aces after other cards but didn’t
take into account other aces.

Yeah, it was a pretty subtle issue. I read your code many times,
certain that it worked the same as mine. It was only because the
biggest differences in our results was when ace was the upcard that I
focused on the ace-handling code…

Thanks for going after the exact odds! I don’t think I would have
tried it had you not demonstrated that it was possible.

Eric

On Jan 6, 11:55 am, “Eric I.” [email protected] 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’

Well, it looks like I over-engineered my solution. After noting that
Denis’ updated solution and James K.'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]

  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

Hi, here goes my solution to this weeks Ruby Q., I was interested in
exact values so I explored the whole game tree of the dealer.
I also allowed to remove visible cards from the deck, here are some
numbers:
2 decks, all cards
608/109 > ruby sol_r_dober-1.0.rb 2
Card 17 18 19 20 21 NATURAL
BUST
2 13.9367% 13.3348% 13.0743% 12.3997% 11.9254% 0.0000%
35.3291%
3 13.2764% 13.0676% 12.4575% 12.1811% 11.5380% 0.0000%
37.4794%
4 13.0704% 12.0208% 12.1038% 11.6366% 11.3145% 0.0000%
39.8540%
5 12.1005% 12.2831% 11.7318% 10.9022% 10.7320% 0.0000%
42.2505%
6 16.6224% 10.6170% 10.6749% 10.1217% 9.7508% 0.0000%
42.2132%
7 37.0478% 13.8195% 7.8013% 7.8781% 7.3390% 0.0000%
26.1143%
8 12.9697% 36.1182% 12.9025% 6.8857% 6.9610% 0.0000%
24.1630%
9 12.0940% 11.2016% 35.4051% 12.1118% 6.0977% 0.0000%
23.0898%
H 11.2904% 11.2156% 11.3008% 33.5608% 3.5463% 7.7670%
21.3191%
A 12.8467% 13.0890% 13.0179% 13.1159% 5.2754% 31.0680%
11.5872%

3 decks no Aces left, (thus no naturals)
Card 17 18 19 20 21 NATURAL
BUST
2 13.0138% 13.3157% 12.8600% 12.0227% 11.3617% 0.0000%
37.4260%
3 12.5628% 13.0186% 12.2907% 11.8168% 11.0145% 0.0000%
39.2965%
4 12.0887% 12.1174% 12.1185% 11.3440% 10.8491% 0.0000%
41.4825%
5 12.1908% 12.1116% 11.5061% 10.6511% 10.4057% 0.0000%
43.1347%
6 6.8368% 12.2179% 11.5637% 10.9612% 10.4570% 0.0000%
47.9633%
7 39.2553% 6.8531% 8.9188% 8.2753% 7.6780% 0.0000%
29.0194%
8 13.2365% 39.2553% 5.7605% 7.8262% 7.1811% 0.0000%
26.7404%
9 12.2132% 12.5372% 38.3526% 4.7974% 6.8658% 0.0000%
25.2339%
H 11.3942% 12.2132% 12.2841% 36.6997% 3.9018% 0.0000%
23.5070%
A is not left in deck

1 deck, lots of high cards visible, 3 Aces, 8 honors and 3 Nines
Card 17 18 19 20 21 NATURAL
BUST
2 16.0973% 15.5326% 15.0611% 13.9028% 9.9494% 0.0000%
29.4568%
3 15.3853% 15.8948% 14.1015% 13.5737% 12.4078% 0.0000%
28.6369%
4 15.7013% 14.1293% 14.5066% 12.7401% 12.0064% 0.0000%
30.9162%
5 14.5124% 15.1168% 13.9803% 12.1107% 11.3529% 0.0000%
32.9269%
6 13.5209% 14.4244% 13.5992% 12.1245% 11.6677% 0.0000%
34.6633%
7 31.4337% 12.8325% 11.0155% 10.3669% 8.9369% 0.0000%
25.4145%
8 11.6807% 31.1870% 11.7455% 9.9479% 9.2536% 0.0000%
26.1854%
9 17.7379% 8.6150% 29.9423% 10.1906% 8.3671% 0.0000%
25.1472%
H 16.4181% 17.2912% 9.5652% 25.4766% 6.0171% 2.7027%
22.5291%
A 16.0890% 18.4076% 18.0607% 10.4038% 7.0168% 21.6216%
8.4006%

and here is the code

--------------------------- >8 -----------------------
require ‘mathn’
require “enumerator”

class String
def each_char &blk
return enum_for(:each_byte).map{ |b| b.chr } unless blk
enum_for(:each_byte).each do |b| blk.call b.chr end
end
end

class Object
def tap
yield self
self
end

def ivars_set ivar_hash
ivar_hash.each do | ivar_name, ivar_value |
instance_variable_set “@#{ivar_name}”, ivar_value
end
self
end
end

module Probability
def to_p digits
“%#{digits+3}.#{digits}f%%” % ( 100 * to_f )
end
end
[ Fixnum, Rational ].each do | number_class |
number_class.send :include, Probability
end

NATURAL = 5
BUST = 6
BANK_STANDS = 17
BLACKJACK = 21

DataError = Class::new RuntimeError
Cards = %w{ 2 3 4 5 6 7 8 9 h a }

class String
def ace
downcase == “a” ? 1 : 0
end

def value
case self
when “A”, “a”
11
when “H”, “h”
10
when “2”…“9”
to_i
else
nil
end
end
end

class Fixnum
def add_value face_or_value
face_or_value = face_or_value.value if String === face_or_value
self + face_or_value
end
end

unmutable objects representing cards that still can be drawn

class Pack

def initialize *args
@data = Hash[ *args ] # a count of faces as a hash
@total = @data.inject(0){ |sum, (k,v)| sum + v } # total count of
cards
end

def each_with_p
@data.each do |face, count|
next if count.zero?
yield face, probability( face )
end
end

def probability face
Rational @data[face], @total
end

def - face
data, total = @data, @total
self.class.allocate.instance_eval { | new_pack |
@data = data.dup
@data[ face ] -= 1
raise DataError, “Cannot remove #{face}” if @data[ face ] < 0
@total = total - 1
new_pack
}
end
end # class Pack

represents the hand of the dealer, immutable

class Hand
attr_reader :probability
def initialize pack, card
@pack = pack
@cards = [ card ]
@count = card.value
@aces = card.ace
@probability = 1
end

def adjust_prob p
@probability *= p
end

def result
return BUST if @count > BLACKJACK
return NATURAL if @count == BLACKJACK && @cards.size == 2
return nil if @count < BANK_STANDS
@count - BANK_STANDS
end

def + face
count = @count + face.value
aces = @aces + face.ace
loop do
break if count <= BLACKJACK || aces.zero?
count -= 10
aces -= 1
end
self.class.allocate.tap{ | new_hand |
new_hand.ivars_set :cards => ( @cards.dup << face ), :count =>
count,
:aces => aces, :pack => @pack - face,
:probability => @probability
}
end

the workerbee, recursive traversal of the game tree of the

dealers hand.

def compute results
@pack.each_with_p do | face, p |
new_hand = self + face
new_hand.adjust_prob p
r = new_hand.result
if r then
results[ r ] += new_hand.probability
else
new_hand.compute results
end
end
end

end

def output card, results
puts " #{card.upcase} #{results.map{ |r| r.to_p(4) }.join(" “)}”
end

def usage
puts %<usage:
ruby #{$0} [visible cards]
visible cards: One or more strings with single characters indicating
face values as follows, 23456789[hH][aA]

exit -1
end
usage if ARGV.empty? || /^-h|^–help/ === ARGV.first

number_of_decks = ARGV.shift.to_i
visible_cards = ARGV.join.each_char.to_a
pack = Pack.new(
*Cards.map{ |c| [c.downcase,
( c.downcase == “h” ? 4 : 1 ) * 4 * number_of_decks ]
}.flatten )
visible_cards.each do |vc|
pack = pack - vc.downcase
end

puts “Card 17 18 19 20 21
NATURAL BUST”
Cards.each do
| card |
begin
hand = Hand.new pack - card, card
results = Array.new( 7 ){ 0 }
hand.compute results
output card, results
rescue DataError
puts " #{card.upcase} is not left in deck"
end
end
------------------------------------------- 8<