One of my favourite ways of improving my coding is by working on Katas. Katas are small coding problems that are used to practise a technique. The concept derives from Japanese martial arts where students continuously repeat a certain sequence to refine their technique. There is an early Playful Python article that goes into this concept more

How do you become better at programming?
You get better at programming just like how you get better at anything else – by practicing. The question is: how to practice in an effective way?

The point of a software kata is not to arrive at a "correct" solution, but as a playground to experiment with different approaches. Since we just learned about the multi-value monad in the previous article, how about we use it to solve the Blackjack scoring kata?

The Kata

One of the key aspects of the game of Blackjack is to determine the score for a hand of cards.

  • Cards from 2 to 10 are worth the points equal to their face value
  • The cards Jack J, Queen Q and King K are worth 10 points each
  • The Ace Ace can be worth either 1 point, or 11 points. Usually the higher the score is better, but if your score crosses 21 then you lose (its called going "bust"). So you want to select the points for the A in such a way that it is as high as possible without crossing 21

Here are some examples of hands and their scores:

  • 2, 5, 7, 2 - 16 points, just add up all the numbers
  • 2, 5, 7, 10 - bust
  • 2, 5, 7, A - 15 points. Here if we take A as 11 then the hand will bust. So we take A as a 1
  • 2, A, A - 14 points. We take the first A as 11 points. Taking the second A as 11 will bust, so we consider the second A as 1 point
  • A, A, A - 13 points

Generally in blackjack you can only consider one A as 11 as any more will definitely bust. However if we keep the bust threshold at a higher value, say 25, then there could be situations where more than one A could be counted as 11.

So here is the problem statement

Given a hand of cards, and a bust threshold, determine the score of the hand

We are going to solve this problem in two ways: one using functions (this article), and the second one using classes (next article). We will use the multi-value monad in both approaches.

The Multi-Value Monad

For reference, here is the code we derived last time for implementing the multi-value monad.

from itertools import chain

class MultiValue:
    def __init__(self, values):
        self.values = set(values)
        
    def map(self, fn):
        out = {fn(val) for val in self.values}
        return MultiValue(out)
        
    def flatmap(self, fn):
        out = (fn(val).values for val in self.values)
        return MultiValue(chain(*out))

Now let us see the two approaches, starting with the functional approach

The Functional Approach

Okay, so we have a hand of cards in a list like this: [2, 5, 7, 'A']. We need to loop through these and sum them up.

The tricky part is the ace which could be counted as a 1 or 11. Given that it has two values, it makes sense to represent card values as MultiValue. Lets start with a function to get the value of a card.

def get_card_value(card):
    if card == "A":
        return MultiValue({1, 11})
    elif card in ("J", "Q", "K"):
        return MultiValue({10})
    return MultiValue({card})

Adding the values

Next, we need to be able to add up the card values, so we need a function to take two MultiValue and add them up, like this: MultiValue({1, 2}) + MultiValue({10, 20})

Lets build up to that step by step. We start with a function that adds two int

def add(a: int, b: int) -> int:
    return a + b

That function is quite straightforward. I've added type hints to make it clear what data types are involved. Actually, we don't even need to write this function ourselves. The operator package in python already has it.

from operator import add

Adding a MultiValue and an int

Now lets move on to a function to add a MultiValue with an int. We can do that using the map method on the MultiValue.

The catch is that the map method needs a function that takes one argument. Our add function takes two parameters. We can fix this by partially applying the first parameter. Here is how it works

from functools import partial

plus_2 = partial(add, 2)
print(plus_2(10)) # will add 2 to give 12
print(plus_2(6)) # 8

partial allows us to take add and set the first argument ahead of time. The return value plus_2 is a function that takes the remaining argument.

And since plus_2 only takes one argument, it can be applied via map. Here is an example

mv = MultiValue({4, 10})
plus_2 = partial(add, 2)
mv.map(plus_2) # MultiValue({6, 12})

and the generalised function

def add_mv_and_int(mv: MultiValue, b: int) -> MultiValue:
    return mv.map(partial(add, b))

Note the type signature. This function returns a MultiValue as an output

Adding two MultiValues

Now we have to add two multivalues. Here is an example: MultiValue({1, 2}) + MultiValue({10, 20}) which will equal MultiValue({11, 21, 12, 22})

To do this, we apply the add_mv_and_int function. Since that function returns a MultiValue as an output, we need to use flatmap here instead of map. Also, add_mv_and_int takes two parameters, so we need to use partial as before. Here is an example

a = MultiValue({1, 2})
b = MultiValue({10, 20})
a.flatmap(partial(add_mv_and_int, b)) # MultiValue({11, 21, 12, 22})

and the generic function

def add_multivalue(a: MultiValue, b: MultiValue) -> MultiValue:
    return a.flatmap(partial(add_mv_and_int, b))

Solving the problem

Alright now that we have a function for adding two MultiValue, how do we add all the values? Well, we can start with an identity value (MultiValue({0})) and adding it with the first number, take that sum and add it with the second, then take that sum and add it with the third and so on.

This kind of operation is called a reduce and Python has it in the functools module.

from functools import reduce

def hand_value(hand):
    values = [get_card_value(card) for card in hand]
    total = reduce(add_multivalue, values, MultiValue({0}))

Now that we have all the possible totals it is simple to complete the function. Just filter out the values that don't bust and find the maximum one. That is the score of the hand.

Here is what the final code looks like

from functools import partial
from operator import add
from functools import reduce

def add_mv_and_int(mv: MultiValue, b: int) -> MultiValue:
    return mv.map(partial(add, b))

def add_multivalue(a: MultiValue, b: MultiValue) -> MultiValue:
    return a.flatmap(partial(add_mv_and_int, b))

def get_card_value(card):
    if card == "A":
        return MultiValue({1, 11})
    elif card in ("J", "Q", "K"):
        return MultiValue({10})
    return MultiValue({card})

def hand_value(hand):
    if len(hand) == 0:
        return 0
    values = [get_card_value(card) for card in hand]
    total = reduce(add_multivalue, values, MultiValue({0}))
    no_bust = {value for value in total.values if value <= 21}
    try:
        return max(no_bust)
    except ValueError:
        return None # all possible scores are bust

The code in hand_value reads almost like pseudocode

  1. If the hand is empty the score is zero
  2. Get the values of the cards
  3. Add them all up
  4. Get the possibilities that do not bust
  5. Find the highest of the possibilities. If no possibilities, return None

We don't need to handle the complexity that some variables are multi-valued, making the code clean and easy to read.

Did you like this article?

If you liked this article, consider subscribing to this site. Subscribing is free.

Why subscribe? Here are three reasons:

  1. You will get every new article as an email in your inbox, so you never miss an article
  2. You will be able to comment on all the posts, ask questions, etc
  3. Once in a while, I will be posting conference talk slides, longer form articles (such as this one), and other content as subscriber-only

Tagged in: