The Great Refactoring

Time to clean up our code before it gets out of hand!

avatar

Sébastien Boisgérault
Associate Professor, ITN Mines Paris – PSL

Blacksmith

Make it work, then make it beautiful, then if you really, really have to, make it fast. 90 percent of the time, if you make it beautiful, it will already be fast. So really, just make it beautiful!

Joe Armstrong, designer of the Erlang programming language

Introduction

In this tutorial, we will continue to develop our snake game.

We will add almost no new feature in this session ; instead, we will do some refactoring, i.e., restructure our existing code using some “best practices” that will (hopefully!) make it easier to maintain and extend.

Here is a reminder of the current project state:

No More Magic

Modules

Fine-Grained Functions

Draw Refactoring

Implement a function display that draws list of colored pixels. Make sure that it works in a way that we can replace the original implementation of draw with:

def draw():
    pyxel.cls(WHITE)
    snake_body = snake_geometry[:-1]
    snake_head = snake_geometry[-1]
    display(DARK_GREEN, snake_body)
    display(LIGHT_GREEN, [snake_head])
    display(BLACK, rocks)
    display(PINK, [fruit])

Then extend the display function so that:

Simplify the implementation of draw to leverage these extensions

Move Snake

Introduce a crash function such that snake_move may be replaced with the code:

def snake_move():
    global snake_geometry, fruit
    snake_head = snake_geometry[-1]
    new_snake_head = [
        snake_head[0] + snake_direction[0],
        snake_head[1] + snake_direction[1],
    ]
    if crash(new_snake_head):
        snake_geometry = snake_geometry[1:-1] + [snake_head]
    elif new_snake_head == fruit:
        snake_geometry = snake_geometry + [new_snake_head]
        spawn_new_fruit()
    else:
        snake_geometry = snake_geometry[1:] + [new_snake_head]

Document the role of this function using an appropriate docstring.

Introduce a few comments into the snake_move function to explain the parts that may still be a bit cryptic (I am thinking about the evolution of the snake geometry). I’d like the game logic to be as explicit as possible here!

Generic Event Handling

Our current event handle_events code is not completely satisfying: if we need to manage more events, for example if we want to add a pause feature, we need to modify and grow the function.

To prevent this, we replace it with a generic event handling system, in which we never change the handle_events function when we introduce new events. The core of this system is described in the following events.py file:

import pyxel 

_handlers = []

def register(event, handler):
    _handlers.append([event, handler])

def handle():
    for event, handler in _handlers:
        if pyxel.btnp(event):
            handler()

This new module should be imported into our main Python file:

import events

and the update function modified as follows:

def update():
    events.handle()
    snake_move()

You can now delete the code of the handle_events function!

Initially, this events.handle function does nothing, but now we can easily register any action (implemented as as function) that we want to associate with an event. For example, to quit the game when the Q key is pressed, we can write:

events.register(pyxel.KEY_Q, pyxel.quit)

Use this new system to handle the arrows key press: define appropriate move_up, move_right, move_down, and move_left actions, and register them with the appropriate events.

Use this new system to pause/unpause the game when the P key is pressed (you will need to slightly modify the snake_move function).

Synthesis

Here is the final result of our refactoring: