Managing the various screens and menus of your UI can be greatly simplified by using a Finite State Machine(FSM) to control the flow of the UI. FSMs are easy to model and work very well for modeling the flow of a user’s action throughout a game. Furthermore, they compartmentalize code specific to each screen, making it simpler to reason about changes to any specific screen.

Let’s start with a simple example. You’re going to write a game of asteroids. Players can start a new game, load an old game, save the current game, and view high scores. (the mechanics of scoring a game of asteroids is immaterial). Immediately, looking at the actions we just described, we can identify several independent interfaces. And if two or more things are independent, we want their code to be independent. So let’s start out by writing skeletons for each independent interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def title_screen(screen, input):
    while True:

        # Paint the screen
        screen.clear()
        screen.put_text("start new game", 0,0)
        screen.put_text("load game", 10,0)
        screen.put_text("view high scores", 20,0)

        # Handle input
        for key in input: # input is a queue of events from the UI library.
            if key == "S":
                # Code here for running a game
            elif key == "L":
                # Code here for loading a game
            elif key = "H":
                # Code here for viewing high scores
            elif key = "Q":
                # Code here to quit the game

def game_screen(game, screen, input):
    while True:
        screen.clear()
        game.progress()
        screen.render(game)
        for key in input:
            game.handle(key)
        if game.hasWon():   # Go to the next_level
            # Go to the next_level
        if game.hasLost():
            # Go to the title_screen, or the high score screen

def highscore_screen(screen, input):
    while True:
        screen.clear()
        for i, score in enumerate(global.high_scores):
            screen.put_text(score, 10*i, 0)
        for key in input:
            if key == "Q":
                # return to title screen

if __name__ == '__main__':
     title_screen(screen, input)

Ok, there are some quick, basic descriptions of how to display different screens, and respond to input. I want to point out something that we’ve immediately gained from separating these screen functions out. The input handlers and rendering code for each function doesn’t need to know about each other. Keys that close the game on one screen can instead be back buttons on other screens.

Now, let’s look at how to move from one screen to another. A first instinct might be to just call the code for the new screen from within the old screen.

1
2
3
4
5
6
7
8
def highscore_screen(screen, input):
    while True:
        screen.clear()
        for i, score in enumerate(global.high_scores):
            screen.put_text(score, 10*i, 0)
        for key in input:
            if key == "Q":
                title_screen(screen, input)

Can you see the memory leak in here? Every time you switch screens, you end up adding another layer to the stack. The solution is to unwind the stack before calling the new screen. To do that, we need to return not the execution of the screen, but a reference to the screen itself. We also need to change __main__ to call each new screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def title_screen(screen, input):
    while True:

        # Paint the screen
        screen.clear()
        screen.put_text("start new game", 0,0)
        screen.put_text("load game", 10,0)
        screen.put_text("view high scores", 20,0)

        # Handle input
        for key in input: # input is a queue of events from the UI library.
            if key == "S":
                game = new_game()
                c
            elif key == "L":
                game = load_game()
                game = new_game()
            elif key = "H":
                return highscore_screen
            elif key = "Q":
                return

def game_screen(game):
    def screen(screen, input):
        while True:
            screen.clear()
            game.progress()
            screen.render(game)
            for key in input:
                game.handle(key)
            if game.hasWon():   # Go to the next_level
                game = new_game()
                return game_screen(game)
            if game.hasLost():
                if score > global.high_score:
                    global.high_score = score
                    return highscore_screen
                else:
                    return title_screen
    return screen

def highscore_screen(screen, input):
    while True:
        screen.clear()
        for i, score in enumerate(global.high_scores):
            screen.put_text(score, 10*i, 0)
        for key in input:
            if key == "Q":
                # return to title screen

if __name__ == '__main__':
     cur_scr = title_screen
     while cur_scr != None:
        cur_scr = cur_scr(screen, input)

Do you see what we did there? Instead of calling the new screen from the old screen, we return a reference to the screen. Then in the while loop in __main__, we call each new function returned from the old one. This makes it very easy to move directly from one interface to another without cluttering the stack or having to worry about where we will return to. And if we ever want to exit the game, we just return None and the interface game loop will end.

One thing to highlight in this code is the changes we needed to make to the game_screen method. Specifically, on line 23, we change game_screen from a function that runs the screen directly into a closure that encloses a game object and returns a function that runs that game as a screen. This was just a convenience for us, as a way of providing the game object while not changing the signature of the function.

We could just as easily use classes, passing game to the GameScreen constructor, and then return the constructed instance. We’d then change the while loop to invoke a method on each screen object, and that method would be what actually runs the interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class GameScreen(object):
    def __init__(self, game):
        self.game = game

    def execute(screen, input):
        while True:
            screen.clear()
            game.progress()
            screen.render(game)
            for key in input:
                game.handle(key)
            if game.hasWon():   # Go to the next_level
                game = new_game()
                return GameScreen(game)
            if game.hasLost():
                if score > global.high_score:
                    global.high_score = score
                    return HighscoreScreen()
                else:
                    return TitleScreen()

if __name__ == '__main__':
    cur_scr = TitleScreen()
    while cur_scr != None:
        cur_scr.execute(screen, input)

One additional fact worth pointing out is that by switching to this method of invoking screens, we’ve actually not lost the flexibility to directly call one screen from within another. For example, consider if we want to be able to view the high scores during gameplay. If we just return a reference to the highscore_screen when we leave the highscore screen, we’ll go to the title screen. Which means we’ve lost our place in the game.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def game_screen(game):
    def screen(screen, input):
        while True:
            screen.clear()
            game.progress()
            screen.render(game)
            for key in input:
                if key == "H":
                    highscore_screen(screen, input)
                game.handle(key)
            if game.hasWon():   # Go to the next_level
                game = new_game()
                return game_screen(game)
            if game.hasLost():
                if score > global.high_score:
                    global.high_score = score
                    return highscore_screen
                else:
                    return title_screen


    return screen

Now, on line 8, if we press “H” during gameplay, we invoke highscore_screen right there in the middle of game_screen, and use the stack to return to exactly the same place in the game we were before the “H” key was pressed. The game can then continue, and we’ve managed to track exactly where we came from before we went to the highscore screen.

Thus, we’ve got a few great tools