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