· tutorials · 5 min read

Functional Programming with Python — Building a Simple Slot Machine

Recently I’ve been helping a friend with some Flask apps. Since this involves Python, as a way to brush up on my Python I decided to build a simple, single reel slot machine using a functional programming paradigm. You can view the full gist here, but I’ll also explain some of the logic used when developing from a functional perspective. When the program is run, the output looks like this:

Jackpot!
Number of spins to hit the jackpot: 562
Number of two of a kinds hit: 82

The Basics of Functional Programming

According to Wikipedia, “Functional programming is a programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.” What does that mean for the average developer who’s used to the more common object oriented programming model (OOP)?

Firstly, programs are made “by applying and composing functions”, so in our slot machine, we’ll need to make sure that we use as many pure functions as possible, a concept known as referential transparency. Given the same inputs, a pure function should always return a value and never modify state through the use of class attributes. You want to avoid side effects because these can make functions difficult to test and potentially result in unpredictable behavior if a value changes when it isn’t supposed to.

A second important concept of functional programming is immutability, and so care must be taken to avoid reassigning variables and treat them as bindings instead.

In addition, type checking is handy in languages which support Hindlely-Milner type systems (such as F# and OCaml), but python has support for optional types and so I’ll also be making sure to make use of them here.

Modeling the Slot Machine

The first step in creating the slot machine is to think of the steps we will need to run through, and then think about composing pure functions wherever possible:

  • “Spin” the slot machine to generate a list consisting of three elements to model the reel.
  • Check the output of the spin condition and keep track of the current state.
  • Return a message when a jackpot condition is reached (three numbers of the same value in a reel).

For step one, I created the following method to generate a list which models a spin based on the number of symbols that the machine has. A typical slot machine might display three spinning reels with about 20 symbols, but for simplicity’s sake, lets model a single reel, eg: [7, 7, 7]:

def spin_machine(symbols_amount: int) -> list[int]:
    spin = (list(np.random.randint(low=1, high=symbols_amount+1, size=3)))
    return spin

Although spin_machine resembles a pure function, it is actually impure because given the same inputs it returns a different list each time its called because of .randint. Impure functions are hard to avoid in certain cases as things like saving to a database inherently generates a side effect, but we try to keep them at the boundaries of our programs whenever possible.

The next step is to check the output of the spin. To do so, we’ll first create an enum called SpinState with four possible states:

class SpinState(Enum):
    START = 0
    JACKPOT = 1
    TWO_OF_A_KIND = 2
    LOSS = 3

To check for a win condition, we’ll create a pure function which returns a SpinState given a spin:

def win_condition_check(spin: list[int]) -> SpinState:
    if len(set(spin)) < 2:
        return SpinState.JACKPOT
    elif len(spin) != len(set(spin)):
        return SpinState.TWO_OF_A_KIND
    else:
        return SpinState.LOSS

There is a few things to make note of here. Firstly, this is an example of an if expression and not an if statement, because all paths evaluate to a value (SpinState). In imperative programming we might handle some but not all of the branches, or return different types for different branches, both of which mean that we can’t guarantee the method will resolve to a value.

Now we’ll need a way to simulate the game until the win condition (SpinState.JACKPOT) is reached. In order to do so, we’re going to create a recursive method which uses accumulators to manage state in a functional way:

def game_simulation(spin_result: SpinState, jackpot_count: int, two_of_a_kind_count: int) -> (SpinState, int, int):
    wheel_spin = spin_machine(20)
    spin_result = win_condition_check(wheel_spin)

    if spin_result == SpinState.JACKPOT:
        return spin_result, jackpot_count, two_of_a_kind_count
    elif spin_result == SpinState.TWO_OF_A_KIND:
        return game_simulation(spin_result, jackpot_count + 1, two_of_a_kind_count + 1)
    elif spin_result == SpinState.START:
        return game_simulation(spin_result, jackpot_count, two_of_a_kind_count)
    else:
        new_spin = SpinState.LOSS
        return game_simulation(new_spin, jackpot_count + 1, two_of_a_kind_count)

Inside the game_simulation function, we have another if expression which returns a triple once the jackpot is hit, otherwise it makes a recursive function call while incrementing our jackpot_count and two_of_a_kind_count accumulators. Once the jackpot is hit, we can get the values we want from the triple. Let’s try the slot machine in the entrypoint now:

if __name__ == '__main__':

    # Pass the initial states to the game_simulation.
    game_round = game_simulation(SpinState.START, 0, 0)

    print(f"Jackpot!\nNumber of spins to hit the jackpot: {game_round[1]}\nNumber of two of a kinds hit: {game_round[2]}")

Success!

Notice how our slot machine was made by applying and composing functions, and passing values from one function to another (as opposed to keeping a global state). We also avoid modifying state which can help eliminate deadlocks when paralellizing code.

When I first began learning functional programming after becoming familiar with OOP, I found it difficult to wrap my head around how I could model things without resorting to the familiar OOP design patterns. However, as time progressed I began to see how smaller groups of pure functions can be chained together to create elegant easily testable programs, and I hope you can too!

Share:
Back to Blog