pythonoopcombinationsblackjackplaying-cards

How to find the number of ways to get 21 in Blackjack?


Some assumptions:

  1. One deck of 52 cards is used
  2. Picture cards count as 10
  3. Aces count as 1 or 11
  4. The order is not important (ie. Ace + Queen is the same as Queen + Ace)

I thought I would then just sequentially try all the possible combinations and see which ones add up to 21, but there are way too many ways to mix the cards (52! ways). This approach also does not take into account that order is not important nor does it account for the fact that there are only 4 maximum types of any one card (Spade, Club, Diamond, Heart).

Now I am thinking of the problem like this:

We have 11 "slots". Each of these slots can have 53 possible things inside them: 1 of 52 cards or no card at all. The reason it is 11 slots is because 11 cards is the maximum amount of cards that can be dealt and still add up to 21; more than 11 cards would have to add up to more than 21.

Then the "leftmost" slot would be incremented up by one and all 11 slots would be checked to see if they add up to 21 (0 would represent no card in the slot). If not, the next slot to the right would be incremented, and the next, and so on.

Once the first 4 slots contain the same "card" (after four increments, the first 4 slots would all be 1), the fifth slot could not be that number as well since there are 4 numbers of any type. The fifth slot would then become the next lowest number in the remaining available cards; in the case of four 1s, the fifth slot would become a 2 and so on.

How would you do approach this?


Solution

  • Before worrying about the suits and different cards with value 10 lets figure out how many different value combinations resulting to 21 there are. For example 5, 5, 10, 1 is one such combination. The following function takes in limit which is the target value, start which indicates the lowest value that can be picked and used which is the list of picked values:

    def combinations(limit, start, used):
        # Base case
        if limit == 0:
            return 1
    
        # Start iteration from lowest card to picked so far
        # so that we're only going to pick cards 3 & 7 in order 3,7
        res = 0
        for i in range(start, min(12, limit + 1)):
            # Aces are at index 1 no matter if value 11 or 1 is used
            index = i if i != 11 else 1
    
            # There are 16 cards with value of 10 (T, J, Q, K) and 4 with every
            # other value
            available = 16 if index == 10 else 4
            if used[index] < available:
                # Mark the card used and go through combinations starting from
                # current card and limit lowered by the value
                used[index] += 1
                res += combinations(limit - i, i, used)
                used[index] -= 1
    
        return res
    
    print combinations(21, 1, [0] * 11) # 416
    

    Since we're interested about different card combinations instead of different value combinations the base case in above should be modified to return number of different card combinations that can be used to generate a value combination. Luckily that's quite easy task, Binomial coefficient can be used to figure out how many different combinations of k items can be picked from n items.

    Once the number of different card combinations for each value in used is known they can be just multiplied with each other for the final result. So for the example of 5, 5, 10, 1 value 5 results to bcoef(4, 2) == 6, value 10 to bcoef(16, 1) == 16 and value 1 to bcoef(4, 1) == 4. For all the other values bcoef(x, 0) results to 1. Multiplying those values results to 6 * 16 * 4 == 384 which is then returned:

    import operator
    from math import factorial
    
    def bcoef(n, k):
        return factorial(n) / (factorial(k) * factorial(n - k))
    
    def combinations(limit, start, used):
        if limit == 0:
            combs = (bcoef(4 if i != 10 else 16, x) for i, x in enumerate(used))
            res = reduce(operator.mul, combs, 1)
            return res
    
        res = 0
        for i in range(start, min(12, limit + 1)):
            index = i if i != 11 else 1
            available = 16 if index == 10 else 4
    
            if used[index] < available:
                used[index] += 1
                res += combinations(limit - i, i, used)
                used[index] -= 1
    
        return res
    
    print combinations(21, 1, [0] * 11) # 186184