From zero to snake in 900 seconds
This is intended to be the gentlest introduction to programming video games that I could imagine. An easy to follow tutorial that breaks it down in simple steps. The end result is a short Python program that implements the game 'Snake.'
A basic understanding of programming is helpful, but even without that, you can still follow along and get an idea about what steps are involved in making a simple video game.
First a little history: when I learned to program in 1982, the threshold for doing this was so much lower. You would simply plug in your micro computer, and you are instantly in your development environment: the prompt of a BASIC interpreter. Compare this with heavy-handed frameworks, drivers, libraries, plugins, assets and you'll come to the conclusion that it was easier back then. See below on how it used to be: instant-on development machine.
This tutorial goes back to the roots, but on a modern PC. So, we're not going to burden ourselves with the GPU, with window creation, graphics contexts, assets, or anything like that. Instead we will leverage the power of a text based terminal. We can do this with the curses module that comes with Python.
The nice thing about snake is the simple essence of it: it just boils down to outputting characters to a text screen at certain locations, according to certain game-rules of course.
Table of Contents
We will be using curses to handle the printing to a terminal for us. So the first thing to do is tell Python we will be using curses.
Next, we will define what our game board looks like: just an empty fenced field. We define it as a list of rows. And each row consists of 25 characters.
# Our snake lives in a world of 15 rows of 25 characters. world = [ "+-----------------------+", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "| |", "+-----------------------+", ]
The curses module will be calling our main function. Our main function will loop indefinitely, and in each iteration will clear the screen, add a little help message and draw our board. When drawn, we tell curses to refresh the screen.
def main( stdscr ): while True: stdscr.clear() stdscr.addstr( 0, 0, "Snake game. Ctrl-C to quit." ) draw_board( stdscr, world ) stdscr.refresh()
We also tell curses where to find our main loop.
This leaves us to define how we actually draw the game board by specifying the draw_board() function as follows:
def draw_board( screen, board ) : for linenr, row in enumerate( board ): line = "".join( row ) screen.addstr( linenr+1, 0, line )
This code simply iterates over each row in our board definition and add it to the screen as a line of characters. The join() function call is a technicality that can be ignored for now. It enables us to print boards that are either lists of strings or lists of character-lists.
And that is it. We are displaying our game board on the screen. It's as simple as adding lines of character to the screen.
Download snake1.py and run it with
We need to represent the shape of the snake. And we need to be able to have the snake grow and move over the board. The best way to achieve this is to store all the body sections as (row,col) coordinates in a list. The first coordinate in the list will represent the snake's head. The last coordinate in the list will represent the snake's tail. We'll start out with a snake of length 3 that is positioned on row 7 of the screen and with its head on the right:
snake_body = [ ( 7,7 ), # The head on the right. ( 7,6 ), ( 7,5 ), # The tail on the left. ]
Then in our main loop, instead of drawing our empty world, we will set up a board that is a copy of our world onto which we place the snake body parts like this:
# Build up the board: start with a copy of the empty world. board = [ list(row) for row in world ] # Place the snake on our board. for idx, ( row, col ) in enumerate( snake_body ) : symbol = '#' if idx > 0 else 'O' board[ row ][ col ] = symbol
Note that we modify the board by adding a 'O' character for the head and a '#' for all the other body sections of the snake. We then pass the board we just built up to our drawing function:
draw_board( stdscr, board )
Running snake2.py will now show a snake on the board:
Our next task is making the snake move. Before we can define movement, we need to add some timing to our main loop. We can't move the snake at the full speed of the computer, we will pace it on a 0.2 second period. So first order of business is to import Python's time module like this:
And slow down our main loop with
time.sleep( 0.2 )
We will have to define the travel direction of our snake too. We will use two deltas for this:
snake_drow = 0 snake_dcol = 1
To move to the right, the delta row (snake_drow) is +1, and to move left it is -1. To move down, we will use snake_dcol is +1, or to move up with snake_dcol is -1. So the initialization above will make it move right.
Each frame we will move the snake using the following function:
# We move the snake by adding a new head, and clearing its old tail. # Returns True if the snake died because of the move. def move_snake() : global snake_body # Figure out where the head of the snake will move to. head_row, head_col = snake_body[ 0 ] # Current location. head_row += snake_drow head_col += snake_dcol # Did the snake eat its own body? if ( head_row, head_col ) in snake_body : return True # Re-assemble the body with a new head. snake_body = [ ( head_row, head_col ) ] + snake_body snake_body.pop() # remove its old tail. # Did we hit the fence? if world[ head_row ][ head_col ] != ' ' : return True return False
Because movement can also cause death, we will let the move_snake() function return a value True/False to signal if the snake died or not. The snake dies if it eats the fence, or its own body.
To move the snake, we first calculate the new position of the head by adding the snake_drow and snake_dcol deltas to the current position of the snake head. This coordinate becomes the new head. We also need to remove the tail of the snake, otherwise our snake would grow. And growing is tackled in the next section.
We will handle snake death by adapting our main loop. We no longer loop indefinitely, but stop when move_snake() has returned the value True.
while not died: ... died = move_snake() ...
Running snake3.py will now have our little snake move to the right, and dying when it hits the fence. It's alive! (and dead.) Not bad at all for a few lines of Python, I would say.
Next up we will add food to our little game world. We'll just represent this with the '*' character.
food = ( 7,20 )
And before we draw our board, we place it on the board with:
# Place the food on the board. board[ food ][ food ] = '*'
Now that there is food on the board, we need to check if the snake eats it. This is simply a matter of checking whether the food position is the same as the newly calculated head position as we are moving the snake.
# Did the snake eat the food? ate_food = ( head_row, head_col ) == food
And if we did actually eat the food, we should grow the snake by one. We can do this by just not removing the tail after moving:
if not ate_food : snake_body.pop() # remove its old tail.
...and spawn the food at a new location:
if ate_food : # Spawn new food at a random location on the board. food = ( int(random.uniform(1,15)), int(random.uniform(1,24) ) )
And there we have it: snake4.py that implements a moving snake that eats food and dies on obstacles. And that means we are nearly there. All that is left is adding the player controls.
We will let the player control the snake with the W/A/S/D keys. The effect of the control is very simple: we modify the global variables snake_drow and snake_dcol to point in the desired direction.
Before we can do that, we need a way to detect key presses. We need to set the curses window to a mode that does not echo the key presses, and will not delay the program while waiting for a key press. This is achieved with the snippet:
This enables us to get keypresses with getch() and act on them:
c = stdscr.getch() if c == ord('w') and snake_drow == 0 : snake_dcol = 0 snake_drow = -1 if c == ord('s') and snake_drow == 0 : snake_dcol = 0 snake_drow = 1 if c == ord('a') and snake_dcol == 0 : snake_dcol = -1 snake_drow = 0 if c == ord('d') and snake_dcol == 0 : snake_dcol = 1 snake_drow = 0
And that is it, folks! We have implemented a full game of Snake with the minimal amount of effort.
Would you believe it? Even written for clarity, not brevity, it only weighs in at 81 lines of code. Nice!
Download snake5.py for the finished game.
Excercises for the reader:
- Replace the Ctrl-C text on top of the screen with a score counter.
- After each food that is eaten, speed up the snake.