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:
-
when its second argument is not specified, it should apply the color
to all pixels of the screen;
-
when the second argument is a single pixel and not a list of pixels,
it should apply the color to this pixel.
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: