Game Tutorial: SSSnake!
Hi everyone,
I put together a little Snake game based on the arcade game Blockade (created in 1976!) and thought I'd write a tutorial on how the game works. (This code is based on work by Ahira Patel.) Feel free to fork the REPL and add to it!
The code is broken up into four main files: board.py
, constants.py
, snake.py
, and main.py
. The first file manages drawing the game board, constants.py
sets up nice enums for our board colors and symbols, snake.py
handles drawing the snake itself, and the last file, main.py
, ties everything together to run our game.
constants.py
Since constants.py
is the simplest, we'll start there. The entire file looks like this:
from enum import Enum class Colors(Enum): ''' Terminal colors. ''' BLUEBACK = '\x1b[44m' END = '\033[0m' GREEN = '\033[92m' RED = '\033[91m' class Symbols(Enum): ''' Symbols used during gameplay. ''' SNAKE = f'{Colors.GREEN.value}o{Colors.END.value}' EMPTY = ' ' GRID = '.' FOOD = f'{Colors.RED.value}*{Colors.END.value}' WALL = f'{Colors.BLUEBACK.value}{Colors.GREEN.value}|{Colors.END.value}'
All we're doing is setting up some nicer syntax so we can use Colors.RED.value
or Symbols.FOOD.value
throughout our code. This also makes it easier for us to change things throughout our program by centralizing logic in one place—for example, we can change the "food" symbol from *
to +
by just changing one character.
board.py
Next, let's take a look at board.py
. This file is a little longer, so we won't reproduce it here, but feel free to follow along in the REPL as we move from function to function.
All our functions are inside a Board
class, which keeps track of the symbols we use in the terminal for the various pieces of our board (such as walls and empty squares). (Note that functions inside classes are called methods, so we'll call them that throughout this tutorial.) We also do some arithmetic to build the board from rows and columns, which are in turn dictated by the dimensions of our terminal. (Feel free to play around with different terminal sizes! You can find these values in the get_terminal_dimensions
function in main.py
.)
Next, we define getters and setters, get
and set
. The get
method gives us the symbol associated with a particular pair of terminal coordinates (that is, what symbol is located at a particular row and column), and the set
method lets us change the symbol located at a set of coordinates. We also define width
and height
to give us the number of columns and rows, respectively, as well as an is_valid_coord
method that checks if a proposed row/column coordinate pair is within the bounds of our terminal.
Our last two methods handle drawing the board itself. The draw_initial_board
method does exactly that: it goes through and draws the board row-by-row (including the outer walls). Finally, the draw
method goes to the provided coordinates on the board and draws the provided symbol (for example, drawing the food symbol at a random spot on the board). Note that both methods use sys.stdout.flush()
to ensure the changes we've made are written to the screen (since content we write to the terminal is buffered; you can read more about data buffers in here). You can think of write
ing as telling the program how the terminal content is going to change, and flush
ing makes those changes appear in the terminal.
snake.py
Moving on, we get to the Snake
class in our snake.py
file. In our __init__
method, we keep track of the snake's current direction, head, tail, and what the snake has eaten, as well as a bit of bookkeeping to help us draw the snake. (A discussion of threading in Python is beyond the scope of this tutorial, but you can Google "Python threading" to find some more information on the topic, such as this WikiBook.) Most of our snake's functionality can be found in the move
method, which controls the snake's movement across the board: we acquire the lock (ensuring no other threads can change the game state while we're in the middle of moving), move the head to its new position, remove the tail, and then signal that we're done moving by updating our movement_processed
flag and releasing the thread lock. The result? Our snake moves one cell at a time across the board!
The consume
method handles the snake's behavior when it eats something: when it successfully eats the food symbol, we grow the snake by one unit. The is_hungry
method helps us figure out whether the snake has recently eaten any food, and is_dead
handles the two conditions that make us lose the game: if we "eat" a wall or "eat" our own tail (that is, collide with a wall or ourselves). The set_movement
method allows the snake to change direction (note that it prevents a snake that's more than one unit long from "reversing"—that is, the snake can't immediately go down while it's going up, left while it's going right, and so on).
Last but not least, the get_head
and get_old_tail
methods are helpers that make it easier for us to get ahold of the snake's head and tail while updating its state on the board.
main.py
Finally, we'll tie everything together in a Game
class inside our main.py
file. Most of our game logic lives here, so we'll spend a bit more time unpacking this code.
First, we __init__
ialize our class with a board (self.board
), how many pieces of food the player has to eat to win (self.num_food
), and the snake (self.snake
), as well as some bookkeeping to keep track of whether the game is over (self.over
) and, if so, how the player signalled they wanted to quit the game (self.key_quit
, self.sig_quit
). Next, we have our play
method, which sleep
s for two seconds to give the player time to switch over to the REPL, then does the following inside a loop:
- Ends the game as needed (if the player crashes into a wall/themselves, eats all the food, or quits);
- Updates the game board state;
- Draws the updated board on the terminal;
- Briefly sleeps to allow the user time to react.
Next, the spawn_new_food
method does what you'd expect: it generates a random coordinate, tries to place the food at that spot, and retries if it's not successful (for example, if the snake is already occupying that spot on the board). The draw_game_board
and update_game_board
are also fairly straightforward: the first method updates the board (and the snake as well, as long as it's still hungry), and the second updates the game state by moving the snake and updating the board, allowing the snake to consume whatever's in the current square (growing the snake if it eats food and ending the game if the snake bites the wall or itself).
Outside of our Game
class, we have a handful of helper functions that allow us to handle user input and nicely transition the terminal from "game mode" back to the regular REPL (and vice-versa). These can be a little confusing, since they have more to do with the particulars of the terminal than the mechanics of our game, so we'll go over them one by one:
movement_listener
listens for user input and sets the snake's direction viasnake.set_movement()
. (It also does some nice things to ensure the terminal looks good; these are described in the comments.)get_terminal_dimensions
is just a nice helper function for getting the height and width of our terminal. (We actually do some subtraction to make the dimensions slightly smaller than the actual terminal, in order to ensure the entire game is visible on-screen.)start_alternate_screen
andend_alternate_screen
are pretty much as-advertised: they handle the switch to the terminal game from the REPL and back again.exit_as_needed
is also pretty straightforward: it handles the four ways our game can end (the user loses by crashing the snake, the user wins by getting all the food, or the user quits the game by either explicitly quitting or stopping the game by sendingCtrl-C
).signal_handler
is a callback function we provide tosignal.signal
to tell Python what to do if it gets an interrupt (SIGINT
) or terminate (SIGTERM
) signal from the user: in either case, we quit the game.- Finally,
quit
setsgame.over
toTrue
and cleans up the game terminal, restoring our settings to the way they were before the game started.
The last few lines of code in main.py
set up our game for us by getting the terminal dimensions, showing a countdown timer for the game to begin, handle the switch from the "regular" REPL to the game (lines 181 to 183), and finally, creating a new game and starting it.
...and that's it! I hope you enjoyed this tutorial, and feel free to fork the REPL and add more to the game.
sooooooo coooooolllllllllllllllllllllllllllll
Well, the coding update here about the games is quite tremendous and these are helpful to understand for beginners. The au.edubirdie.com reviews beginner developers who really wants to understand the coding should bookmark the updates and learn.