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 ?
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"]
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.
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 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 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]
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...
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
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
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
> On Jan 5, 7:38 pm, James Gray <ja...@grayproductions.net> 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... :)
>> James Edward Gray 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.
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.
On Jan 6, 12:02 am, "Eric I." <rubytrain...@gmail.com> 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.
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....
> On Jan 6, 12:02 am, "Eric I." <rubytrain...@gmail.com> 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.
> > 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.
> 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.
> On Jan 6, 2008, at 3:03 PM, Denis Hennessy 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 Gray 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
>> On Jan 6, 2008, at 3:03 PM, Denis Hennessy 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 Gray 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)
.. 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 Hennessy <de...@hennessynet.com> 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.
My solution works by expanding a nested "probability hash." Each card is a key in this probability hash which contains another probability hash equivalent to what would happen if the dealer were to next gain that card. For example, the information on the situation in which the dealer starts with a 2 up and an ace down and then hits a jack would be contained in $probs[2][:A][:J]. In addition, each hash contains the following:
1)The hand of the dealer . E.g.: $probs[1][:A][:K][:hand]==[1,:A,:K] 2)The remaining deck, as represented by a card=>number of instances of that card remaining hash. 3)The probability of this situation occurring. This is easily calculated as the probability of the parent situation occurring times the probability of a the new card being drawn.
The program starts with the base case of full decks, empty hand, and probability of 1, and then expands down the sub-situations down to the point of 0-probability, bust, or dealer's hand being above 17. The probabilities of all the sub-situations of an upcard are then summed and outputted.
Interestingly, while most of my table is within rounding error of Dennis's, the results for the aces are remarkably different.
class Array def count(obj) select{|el|el==obj}.size end
def sum inject(0){|s,n|s+n} end end
def sum_hand(hand) sum = hand.map{|c|CARDS[c]}.compact.sum sum += hand.count(:A)*HIGH_ACE hand.count(:A).times{sum -= (HIGH_ACE-LOW_ACE) if sum > MAX_HAND} sum <= MAX_HAND ? sum : nil end
def expand_prob_hash(prob_hash) return if 0 == prob_hash[:prob] or nil == sum_hand(prob_hash[:hand]) or HIT_THRESHOLD<=sum_hand(prob_hash[:hand]) CARDS.keys.each do |c| mod_deck = prob_hash[:deck].clone mod_deck[c] -= 1 prob_hash[c] = {:hand=>prob_hash[:hand]+[c], :deck=>mod_deck, :prob=>prob_hash[:prob]* prob_hash[:deck][c]/prob_hash[:deck].values.sum} expand_prob_hash(prob_hash[c]) end end
def sum_probs(prob_hash) probs=((HIT_THRESHOLD..MAX_HAND).to_a+[nil]).inject({}){|h,n|h[n]=0.0;h} if prob_hash.has_key? :A CARDS.keys.each do |c| prob_part = sum_probs(prob_hash[c]) prob_part.each_pair {|k,v| probs[k] += v} end elsif 0 == prob_hash[:prob] #do nothing else probs[sum_hand(prob_hash[:hand])] += prob_hash[:prob] end probs end
----- Original Message ---- From: Ruby Quiz <ja...@grayproductions.net> To: ruby-talk ML <ruby-t...@ruby-lang.org> Sent: Friday, January 4, 2008 7:04:45 AM Subject: [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:
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.
___________________________________________________________________________ _________ Never miss a thing. Make Yahoo your home page. http://www.yahoo.com/r/hs
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'
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] ---
> 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.
On Jan 9, 2008 9:10 PM, James Gray <ja...@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
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. :)
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
> On Jan 9, 2008 9:10 PM, James Gray <ja...@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
> 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. :)
Neat. Just know that I wrote tomorrow's summary before you did this. ;)
The solutions took two totally different approaches this week. Some exhaustively searched the possible hands a dealer can have. The search space isn't too big in this case so that's a viable approach that gives exact results.
Others chose to solve the problem with a simulation. This approach is very simple: deal a ton of hands, play them out by the dealer rules, and keep track of the results. If you simulate enough hands, this approach should zero in on the actual numbers.
Let's see if the two approaches agree. Here are the expected results from Eric I.'s exhaustive search:
As you can see, they are very close to each other.
There are advantages to both approaches and I had a hard time picking what to talk about in this summary. If accuracy is your biggest concern, you're probably better off sticking with the exhaustive search. However, simulation gets very close results, is probably a little easier to code up, and would be an option even if the search space was much larger (though the results may be less accurate for that).
Given that, I'm going to show a simulation solution here. If you are more interested in the exhaustive search, Eric I.'s code is well commented and easy to read.
The solution I want to look at below is Sander Land's first submission. It's not perfect and I'll try to point out the problems as they come up, but the code is a very straight-forward simulation with a few good tricks in it. I think that makes it worth a read through.
Sander's solution is written for Ruby 1.9, so it doesn't run without modification on earlier versions. I'll point out the new features as we go and that will give us a chance to see how some of the new stuff gets used.
Here's the start of the code:
class Array def score sort.inject(0){|s,c| s+c > 21 && c==11 ? s+1 : s+c } end end
# ...
This is Sander's code for valuing Blackjack hands. Sander's notion of a hand is simply an Array of 2 through 11 Integers. Blackjack isn't affected by suits, so this code doesn't bother to track them.
This code counts drops an ace an to 1 if counting it as 11 would bust the hand. Note that the hand is first sorted to push aces to the end and make sure they are counted last. Everything else gets the normal card value added on to a running total.
While, this code gets very close, it doesn't work in all cases. Consider a hand containing a ten, an ace, and another ace. The code will count the ten, add eleven to it, because it wouldn't bust the hand, then add one more. This gives a final total of twenty two, instead of the correct count of twelve. At least three solutions made the same error.
One way to fix the code is:
class Array def score sort.each_with_index.inject(0){|s,(c,i)| s+c > 21 - (size - (i + 1)) && c==11 ? s+1 : s+c } end end
This does the same thing, but reduces the hand limit for each card we have left to count once we reach the aces. They are sorted to the end so we know we only have aces left and we could choose to count them as one each. Note that I used a 1.9 feature here of calling each_with_index() without a block to get an Enumerator object. That allows me to inject() over the values with their index.
Here's the next bit of Sander's code:
# ...
unless ARGV[0] (2..11).each{|n| puts `ruby1.9 #{__FILE__} #{n}`} exit end
# ...
I thought this was a great trick. As you are about to see, the rest of the code is written such that it only worries about a single upcard. That simplified the code, but it doesn't let us see all of the results at once. To fix that, Sander just calls his own program once for each upcard, when one isn't provided. It's a recursive process call, so to speak.
The downside here is that this code didn't run for me as written. I call my ruby 1.9 install ruby_dev, so the hardcoded name bit me. A more portable way to get the name would be:
We know we have an upcard if we made it this far, since the code before calls us with one when the user doesn't pass one in. That makes it safe to read in at this point. The number of decks is also read if available, or the default of two is assigned when it's not.
The last bit of this code builds a full deck. It does that by generating the values 2 through 11, adding on three additional 10 values for the face cards, multiplying the whole set by 4 for the suits, and multiplying that standard deck by the count of decks we want to play with.
This gives us the number of requested full decks, but we need to remove the already used upcard. That's what the tap() call does and it's new in Ruby 1.9. delete_at() returns the element deleted, but we need the resulting deck back instead, to store it for future use. That's what tap() is for. You can tap() into a chain of calls to run some code, but you still get the object itself back as the result of the tap() call.
Believe it or not, that's the entire simulation code.
It begins by creating an Array to hold the tally of scores, indexed by hand total. It's interesting to look at the size of this Array, initialized to twenty seven 0's. That makes the highest Array index twenty six, which is the largest hand a dealer will ever bust with. That is, they can draw a face card when hitting a sixteen (a ace would count as one in this case). Indices zero through sixteen won't be touched, since a dealer won't stop on these totals.
The rest of this code creates an Array to hold the cards left, and decides how many rounds to run the simulation for based on user input or a default. The simulation loop then starts, doing just four things: deal and shuffle() (added in Ruby 1.9) a fresh deck if we may not have enough cards to deal another hand (seventeen aces at minimum), initialize a hand with the upcard, deal additional cards until we crest a total of seventeen, and record the final hand total.
The good point about this code is that it reuses the deck for as long as it can. This simulation gets fairly accurate results by trying many possible hands with each trip through the deck.
One downside is that it doesn't distinguish between a total of twenty one and a natural. You could fix this by dedicating some slot in the score Array for that (or switching to a score Hash of Hash.new(0)) and adding a check for this special hand after the hitting code.
With the results tallied, printing is all that remains:
The only real point of interest in this code is another 1.9 idiom. inject() has be enhanced so that it can now take a Symbol argument for the method to call in the block. That greatly simplifies the common summing case, as we see here.
Again the other solutions and approaches to this problem were all interesting. Do take the time to look through them.
My thanks to all the card players that decided to ante up for this quiz.