Can You Win at Non-Traditional Blackjack?
This Week's Fiddler
We first want to find all the integer partitions of 21 which repeats no parts and uses no part larger than 10. We can do this manually or with some code:
from functools import cache
@cache
def list_partitions(n: int, max_part: int) -> list[tuple[int, ...]]:
if n < 0 or max_part < 0:
return []
if n == 0:
return [()]
ans = []
for k in range(max_part, 0, -1):
ans.extend(
[
(k, *partition)
for partition in list_partitions(n - k, min(max_part, k - 1))
]
)
return ans
print(list_partitions(21, 10))
[(10, 9, 2), (10, 8, 3), (10, 8, 2, 1), (10, 7, 4), (10, 7, 3, 1), (10, 6, 5), (10, 6, 4, 1), (10, 6, 3, 2), (10, 5, 4, 2), (10, 5, 3, 2, 1), (9, 8, 4), (9, 8, 3, 1), (9, 7, 5), (9, 7, 4, 1), (9, 7, 3, 2), (9, 6, 5, 1), (9, 6, 4, 2), (9, 6, 3, 2, 1), (9, 5, 4, 3), (9, 5, 4, 2, 1), (8, 7, 6), (8, 7, 5, 1), (8, 7, 4, 2), (8, 7, 3, 2, 1), (8, 6, 5, 2), (8, 6, 4, 3), (8, 6, 4, 2, 1), (8, 5, 4, 3, 1), (7, 6, 5, 3), (7, 6, 5, 2, 1), (7, 6, 4, 3, 1), (7, 5, 4, 3, 2), (6, 5, 4, 3, 2, 1)]
What we're interested in is each partition's length. Because we're always going to draw until we either win or bust, the order of our winning hands don't matter. For a -sized partition, we have a chance of getting said particular win condition. Our total probability of winning is thus the sum of all these chances.
There are 7 partitions with 3 cards, 16 partitions with 4 cards, 9 partitions with 5 cards, and 1 partition with 6 cards. Hence, our answer:
Answer: 7/40 (17.5%)
Extra Credit
Note that if we play ultra-conservatively, 10 has to be in our winning hand. If we suppose that 10 isn't in our winning hand, then one of our cards that we're waiting on must be lower than 10. To get exactly 21, this means that our hand total must at some point be 12 or greater. This puts us at risk for busting when drawing a 10. Thus, all winning hands must include a 10.
For the vast majority of winning hands, they follow a structure of drawing 10 last (this is important, as drawing 10 early means other large cards, such as 9, give us a chance of busting). The number of such hands is the number of partitions of 11 we can make. The following code finds all the partitions we need, using the same list_partitions()
function from earlier:
...
def list_partitions(n: int, max_part: int) -> list[tuple[int, ...]]:
...
print(list_partitions(11, 9))
[(9, 2), (8, 3), (8, 2, 1), (7, 4), (7, 3, 1), (6, 5), (6, 4, 1), (6, 3, 2), (5, 4, 2), (5, 3, 2, 1)]
For each partition of size , there are ways of arranging the partition items and ways of arranging the items outside the partition. Note that the 10 card's position must be fixed, lying between the cards in the partition and the cards outside the partition. With total orderings, this makes a chance of for each partition.
There are, however, exactly two winning hands not discussed here: 10-2-9 and 2-10-9. These are the only two hands that work that do not end in 10. It works out because 10 is exhausted earlier in the hand, allowing 9 to be the largest card remaining and letting you go safely to 12 (but no further). Because we must add to 12 with 10 and a different card, 2 is the only other option. Thus, both orderings that satisfy these conditions are valid hands. Each of the hands has a chance of (as there are ways to arrange the remaining 7 cards).
For 4 partitions of size 2, 5 partitions of size 3, and 1 partition of size 4, not forgetting the two extra winning hands, this makes the total chance:
Funnily enough, because the deck size is rather small (there are only possible permutations), we can also just brute-force all the decks and see which ones fit our criteria:
Brute-force code
import itertools
from collections import Counter
from fractions import Fraction
winning_hands = Counter()
total = 0
for deck in itertools.permutations(range(1, 11)):
total += 1
remaining = set(deck)
card_sum = 0
for i, card in enumerate(deck):
remaining.remove(card)
card_sum += card
if card_sum == 21:
winning_hands.update([deck[: i + 1]])
break
if card_sum + max(remaining) > 21:
break
total_wins = winning_hands.total()
print(
f"Probability (brute force): {Fraction(total_wins, total)} ({total_wins / total})"
)
print("Winning hands:")
print(list(winning_hands.keys()))
Probability (brute force): 13/630 (0.020634920634920634)
Winning hands:
[(1, 2, 3, 5, 10), (1, 2, 5, 3, 10), (1, 2, 8, 10), (1, 3, 2, 5, 10), (1, 3, 5, 2, 10), (1, 3, 7, 10), (1, 4, 6, 10), (1, 5, 2, 3, 10), (1, 5, 3, 2, 10), (1, 6, 4, 10), (1, 7, 3, 10), (1, 8, 2, 10), (2, 1, 3, 5, 10), (2, 1, 5, 3, 10), (2, 1, 8, 10), (2, 3, 1, 5, 10), (2, 3, 5, 1, 10), (2, 3, 6, 10), (2, 4, 5, 10), (2, 5, 1, 3, 10), (2, 5, 3, 1, 10), (2, 5, 4, 10), (2, 6, 3, 10), (2, 8, 1, 10), (2, 9, 10), (2, 10, 9), (3, 1, 2, 5, 10), (3, 1, 5, 2, 10), (3, 1, 7, 10), (3, 2, 1, 5, 10), (3, 2, 5, 1, 10), (3, 2, 6, 10), (3, 5, 1, 2, 10), (3, 5, 2, 1, 10), (3, 6, 2, 10), (3, 7, 1, 10), (3, 8, 10), (4, 1, 6, 10), (4, 2, 5, 10), (4, 5, 2, 10), (4, 6, 1, 10), (4, 7, 10), (5, 1, 2, 3, 10), (5, 1, 3, 2, 10), (5, 2, 1, 3, 10), (5, 2, 3, 1, 10), (5, 2, 4, 10), (5, 3, 1, 2, 10), (5, 3, 2, 1, 10), (5, 4, 2, 10), (5, 6, 10), (6, 1, 4, 10), (6, 2, 3, 10), (6, 3, 2, 10), (6, 4, 1, 10), (6, 5, 10), (7, 1, 3, 10), (7, 3, 1, 10), (7, 4, 10), (8, 1, 2, 10), (8, 2, 1, 10), (8, 3, 10), (9, 2, 10), (10, 2, 9)]
This matches what we got earlier, and it lists all the winning hands!
So our chance of winning in one particular round is 13/630 (about 2.06%). What does that make our expected number of rounds until we win? Well that's just a geometric distribution, whose expected value is , where is the probability of success for one round. Thus, our answer is , or about 48.4615 rounds.
Answer: 630/13 (about 48.4615)