Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[QUIZ] Studying Blackjack (#151)

45 views
Skip to first unread message

Ruby Quiz

unread,
Jan 4, 2008, 8:04:45 AM1/4/08
to
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.

Sharon Phillips

unread,
Jan 4, 2008, 5:27:06 PM1/4/08
to
> 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


James Gray

unread,
Jan 4, 2008, 5:31:49 PM1/4/08
to

You bet. Feel free.

James Edward Gray II

Chris

unread,
Jan 4, 2008, 7:34:38 PM1/4/08
to
> > 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


Eric I.

unread,
Jan 4, 2008, 8:50:40 PM1/4/08
to
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

Joe

unread,
Jan 4, 2008, 9:22:26 PM1/4/08
to
I didn't think it was supposed to be run a specific number of times,
but go over all possibilities.

I was thinking something like this: (with 2 decks of cards)
2 2 2 2 2 2 2 2 3 - 19
2 2 2 2 2 2 2 2 4 - 20
2 2 2 2 2 2 2 2 5 - 21
2 2 2 2 2 2 2 2 6 - bust
2 2 2 2 2 2 2 2 7 - bust

also would these be considered different hands or the same?
2 2 2 2 2 2 2 2 3
2 2 2 2 2 2 2 3 2

I may be wrong though. If that's the case someone correct me.

Joe

James Gray

unread,
Jan 4, 2008, 11:20:38 PM1/4/08
to
On Jan 4, 2008, at 8:22 PM, Joe wrote:

> I didn't think it was supposed to be run a specific number of times,
> but go over all possibilities.
>
> I was thinking something like this: (with 2 decks of cards)
> 2 2 2 2 2 2 2 2 3 - 19
> 2 2 2 2 2 2 2 2 4 - 20
> 2 2 2 2 2 2 2 2 5 - 21
> 2 2 2 2 2 2 2 2 6 - bust
> 2 2 2 2 2 2 2 2 7 - bust
>
> also would these be considered different hands or the same?
> 2 2 2 2 2 2 2 2 3
> 2 2 2 2 2 2 2 3 2
>
> I may be wrong though. If that's the case someone correct me.

I think I've heard about two different, but very interesting
strategies to solve this problem. I can't wait to see the code. ;)

James Edward Gray II

James Gray

unread,
Jan 4, 2008, 11:24:55 PM1/4/08
to
On Jan 4, 2008, at 6:35 PM, 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 !

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

James Gray

unread,
Jan 4, 2008, 11:37:19 PM1/4/08
to
On Jan 4, 2008, at 7:55 PM, Eric I. wrote:

> I do wonder, though, whether the deck is biased somehow when the
> dealer deals him/herself since all the players are dealt beforehand.

I believe what you say is true, though I think it's a detail typically
ignored in this kind of strategic analysis.

Google the "cut card effect blackjack" for some interesting (and I
*think* semi-related) reading.

James Edward Gray II

tho_mica_l

unread,
Jan 5, 2008, 3:02:09 AM1/5/08
to
> 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?

tho_mica_l

unread,
Jan 5, 2008, 4:19:04 AM1/5/08
to
> 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.

Denis Hennessy

unread,
Jan 5, 2008, 6:25:11 AM1/5/08
to
[Note: parts of this message were removed to make it a legal post.]

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%

Denis Hennessy

unread,
Jan 5, 2008, 6:37:10 AM1/5/08
to
Hi Chris,

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

/dh

Sander Land

unread,
Jan 5, 2008, 8:45:50 AM1/5/08
to
On Jan 5, 2008 12:25 PM, Denis Hennessy <de...@hennessynet.com> wrote:
> 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).

I think that's correct.


>
> /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%

Those last four lines are identical. Are you calculating the
probabilities and not simulating, or do you just output the last run
four times?

Here are my results, 2 decks and 1_000_000 deals each.

$ ruby1.9 blackjack.rb
upcard: 2
17 18 19 20 21 bust
13.9% 13.4% 13.1% 12.4% 11.9% 35.3%
upcard: 3
17 18 19 20 21 bust
13.3% 13.1% 12.5% 12.2% 11.5% 37.4%
upcard: 4
17 18 19 20 21 bust
13.1% 12.0% 12.1% 11.7% 11.3% 39.8%
upcard: 5
17 18 19 20 21 bust
12.1% 12.3% 11.7% 10.9% 10.7% 42.1%
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.8% 13.9% 7.9 % 8.0 % 7.3 % 26.1%
upcard: 8
17 18 19 20 21 bust
13.0% 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.2% 6.1 % 23.1%
upcard: 10
17 18 19 20 21 bust
11.3% 11.2% 11.3% 33.6% 11.3% 21.3%
upcard: 11
17 18 19 20 21 bust
12.6% 12.8% 12.7% 12.9% 36.0% 13.0%

Chris Lowis

unread,
Jan 5, 2008, 8:49:30 AM1/5/08
to
> You have an extra card in your hand (a '1' as well as an ace).

Thank you, well spotted !

Chris

Chris Lowis

unread,
Jan 5, 2008, 8:57:05 AM1/5/08
to
> You have an extra card in your hand (a '1' as well as an ace).

Here's my updated results using a two-deck deck and 5000 simulated
hands per upcard.

Upcard Bust 17 18 19 20 21 Natural

2 35.14% 13.66% 13.80% 13.18% 12.10% 12.12% 0.00%
3 37.96% 13.02% 12.72% 12.98% 12.10% 11.22% 0.00%
4 39.98% 13.40% 12.70% 11.70% 11.58% 10.64% 0.00%
5 40.70% 12.86% 12.66% 11.12% 11.94% 10.72% 0.00%
6 43.02% 16.74% 10.30% 10.20% 9.78% 9.96% 0.00%
7 26.36% 37.44% 13.18% 8.02% 7.84% 7.16% 0.00%
8 24.52% 12.68% 35.78% 13.42% 6.72% 6.88% 0.00%
9 23.64% 11.52% 11.98% 34.42% 11.88% 6.56% 0.00%
t 21.60% 11.60% 10.90% 11.22% 33.36% 3.98% 7.34%
j 21.30% 11.84% 10.94% 11.68% 32.84% 3.88% 7.52%
q 21.78% 11.26% 10.94% 11.20% 34.08% 3.68% 7.06%
k 20.60% 10.96% 10.34% 11.82% 34.60% 3.54% 8.14%
a 12.48% 13.40% 13.20% 12.82% 12.86% 4.84% 30.40%

Chris

Denis Hennessy

unread,
Jan 5, 2008, 12:38:37 PM1/5/08
to
On 5 Jan 2008, at 13:45, Sander Land wrote:
>> 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%
>
> Those last four lines are identical. Are you calculating the
> probabilities and not simulating, or do you just output the last run
> four times?

I'm calculating (based on every permutation of remaining cards) rather
than simulating. Hence I only do one run. I guess I could have
duplicated the last four lines and saved a few CPU cycles but it runs
reasonably fast anyway so I didn't bother.


>
> Here are my results, 2 decks and 1_000_000 deals each.
>

I'm guessing that you used a simulation approach instead. It's
interesting, and reassuring, that we got the same results.

/dh


Rick DeNatale

unread,
Jan 5, 2008, 5:02:55 PM1/5/08
to
On Jan 4, 2008 11:37 PM, James Gray <ja...@grayproductions.net> wrote:

> Google the "cut card effect blackjack" for some interesting (and I
> *think* semi-related) reading.

Interesting. This seems to have to do with the Casinos doing away
from using a cut card and using a new machine which continuously
reshuffles used cards back into the "deck". The traditional way for
casinos to deal blackjack is to shuffle several decks together, place
them into a shoe, insert a cut card towards the back of the deck, put
used cards at the back of the shoe, and reshuffle the whole deck when
the cut card gets near the front.

Of course, the real reason they do this is to minimize losses to card
counters. 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. The one article I just read on the "cut card effect didn't
mention that but only talked about the effect it has on players who
just use a "basic" strategy based on what's in their current hand, and
what the dealer (and others at the same table) are showing currently.

--
Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

James Gray

unread,
Jan 5, 2008, 7:31:27 PM1/5/08
to
On Jan 5, 2008, at 2:05 AM, tho_mica_l wrote:

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

James Gray

unread,
Jan 5, 2008, 7:32:05 PM1/5/08
to
On Jan 5, 2008, at 5:25 AM, Denis Hennessy wrote:

> (I'm assuming the dealer has to use an ace as 11 unless they bust).

Correct.

James Edward Gray II

James Gray

unread,
Jan 5, 2008, 7:38:23 PM1/5/08
to
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… :)

James Edward Gray II

Eric I.

unread,
Jan 6, 2008, 12:02:34 AM1/6/08
to
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

tho_mica_l

unread,
Jan 6, 2008, 7:27:10 AM1/6/08
to
> 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.

Paul Novak

unread,
Jan 6, 2008, 8:36:04 AM1/6/08
to
> 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.

Regards,

Paul

Denis Hennessy

unread,
Jan 6, 2008, 8:39:27 AM1/6/08
to
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

Chris Lowis

unread,
Jan 6, 2008, 8:53:51 AM1/6/08
to
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

tho_mica_l

unread,
Jan 6, 2008, 10:32:38 AM1/6/08
to
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
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


Sander Land

unread,
Jan 6, 2008, 10:42:47 AM1/6/08
to
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

Eric I.

unread,
Jan 6, 2008, 11:55:54 AM1/6/08
to
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

Pawel Radecki

unread,
Jan 6, 2008, 1:25:41 PM1/6/08
to
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...@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...@gmail.com
w: http://radeckimarch.blogspot.com/

James Gray

unread,
Jan 6, 2008, 1:58:23 PM1/6/08
to

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 Gray II

Eric I.

unread,
Jan 6, 2008, 3:30:25 PM1/6/08
to
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.

Eric

Denis Hennessy

unread,
Jan 6, 2008, 4:03:38 PM1/6/08
to
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

Chris Lowis

unread,
Jan 6, 2008, 5:36:56 PM1/6/08
to
> > 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

James Gray

unread,
Jan 6, 2008, 5:40:02 PM1/6/08
to
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

Denis Hennessy

unread,
Jan 6, 2008, 6:54:04 PM1/6/08
to

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


Denis Hennessy

unread,
Jan 6, 2008, 7:07:32 PM1/6/08
to
On 6 Jan 2008, at 23:54, Denis Hennessy wrote:

> On 6 Jan 2008, at 22:40, James Gray wrote:
>
>> 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

Eric I.

unread,
Jan 6, 2008, 10:03:49 PM1/6/08
to
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.

Eric

James Koppel

unread,
Jan 6, 2008, 10:37:34 PM1/6/08
to
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.

17 18 19 20 21 BUST
2 13.94% 13.33% 13.07% 12.40% 11.93% 35.33%
3 13.28% 13.07% 12.46% 12.18% 11.54% 37.48%
4 13.07% 12.02% 12.10% 11.64% 11.31% 39.85%

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%

A 12.85% 13.09% 13.02% 13.12% 36.34% 11.59%


Here's my program:

CARDS = (2..10).inject({}){|h,n|h[n]=n;h}.merge(
{:J=>10,
:Q=>10,
:K=>10,
:A=>nil})

MAX_HAND = 21
HIT_THRESHOLD = 17
NUM_DECKS = 2
HIGH_ACE = 11
LOW_ACE = 1
CARD_REPS = 4

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

$probs = {:hand=>[],
:deck=>CARDS.keys.inject({}){|h,c|h[c]=CARD_REPS*NUM_DECKS;h},
:prob=>1.0}
expand_prob_hash($probs)

puts " "+((HIT_THRESHOLD..MAX_HAND).to_a+["BUST"]).map{|el|
"%8s"%[el]}.join
[2,3,4,5,6,7,8,9,10,:J,:Q,:K,:A].each do |c|
p = sum_probs($probs[c])
printf "%2s ",c
((HIT_THRESHOLD..MAX_HAND).to_a+[nil]).each{|n|printf "%6.2f%% ",p[n]*100*CARDS.size}
puts
end

----- Original Message ----
From: Ruby Quiz <ja...@grayproductions.net>
To: ruby-talk ML <ruby...@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:

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.


____________________________________________________________________________________
Never miss a thing. Make Yahoo your home page.
http://www.yahoo.com/r/hs


Eric I.

unread,
Jan 7, 2008, 12:09:16 PM1/7/08
to
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.

Eric

James Gray

unread,
Jan 9, 2008, 3:10:00 PM1/9/08
to
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

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

Clever trick. I like that.

James Edward Gray II

Sander Land

unread,
Jan 9, 2008, 6:52:01 PM1/9/08
to
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

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%

James Gray

unread,
Jan 9, 2008, 10:36:54 PM1/9/08
to
On Jan 9, 2008, at 5:52 PM, Sander Land wrote:

> 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. ;)

James Edward Gray II

Ruby Quiz

unread,
Jan 10, 2008, 12:18:47 PM1/10/08
to
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:

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%

Now here are the same results from Chris Lowis's simulation code (with the
number of games increased to 100,000):

Upcard Bust 17 18 19 20 21 Natural
c2 35.02% 14.06% 13.36% 13.12% 12.53% 11.90% 0.00%
c3 37.24% 13.47% 12.94% 12.67% 12.27% 11.42% 0.00%
c4 39.29% 13.25% 12.58% 12.16% 11.59% 11.13% 0.00%
c5 41.78% 12.29% 12.24% 11.78% 11.20% 10.71% 0.00%
c6 42.05% 16.36% 10.75% 10.69% 10.14% 10.02% 0.00%
c7 25.97% 37.00% 13.89% 7.83% 7.75% 7.57% 0.00%
c8 24.52% 12.75% 35.93% 12.75% 6.94% 7.10% 0.00%
c9 22.86% 12.00% 11.97% 35.02% 12.08% 6.07% 0.00%
ct 21.43% 11.04% 11.11% 11.05% 34.37% 3.36% 7.63%
cj 21.39% 11.11% 11.15% 11.17% 34.07% 3.44% 7.67%
cq 21.16% 10.97% 11.30% 11.13% 34.19% 3.47% 7.79%
ck 21.23% 11.27% 11.13% 11.24% 34.07% 3.49% 7.58%
ca 13.13% 12.88% 12.86% 12.71% 12.71% 4.95% 30.77%

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:

unless ARGV[0]
require "rbconfig"
(2..11).each{|n|
puts `#{Config::CONFIG["ruby_install_name"]} #{__FILE__} #{n}`
}
exit
end

Let's more on. Here's the code that reads the upcard and number of decks to
use:

# ...



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

# ...

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.

We're now ready to run the simulation:

# ...



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
}

# ...

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:

# ...



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

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.

Tomorrow we will continue the Blackjack theme..

0 new messages