Implementing a snake game in the terminal
tsnake, a snake game in the terminal
Lately, I have been kicking the dust off my C++ skills, and decided to start by learning to use a library which I have been eyeing for a while,
ncurses is a C library which lets you create text-based UI programs for the terminal, in the same fashion as the gif above. Basically, you can use the terminal to implement text-based user interfaces. Since I seem to have an obsession with snake games, I figured I’d create a snake game for the terminal.
The project I’m sharing today is
tsnake, a terminal-based snake game which supports maps, different difficulties, and interactive resizing, all rendered in a terminal window using
How to start
Let’s see how it works. The game itself is initialised and run within the function
start_game(int, int). This function gets two paramters: the starting length of the snake and the map identifier. When a game finishes either because the user won or because she crashed, a window pops up displaying the final score and the user is given a couple of options. Either quit or keep playing. If she chooses to keep playing, the game resets with a new map. That is implemented in a simple loop in the main function:
The first thing we do when starting a new game is creating the game window. To do so, we use the
newwin(nlines, ncols, y, x) ncurses call. This returns a
WINDOW type object, which we keep in the game state structure, along with its width and height as cols and lines. The window has a size of [
COLS], leaving the last line for the status bar, where we print some useful information like the key bindings, the current score or the speed of the snake.
Here is a sketch of what this very simple setup looks like.
+-------------------------------------------+ | | | | | | | SNAKE GAME IS | | RENDERED IN THIS WINDOW | | | | | | | +-------------------------------------------+ STATUS BAR GOES HERE
Then we draw the map identified by the given id and after that we start the actual game loop.
In the following sections we will mostly use the function variants which take in a window as one of the arguments. These functions contain a
w somewhere in the name, and are counterparts to the ones without
w, which act on the default canvas. For example,
mvinch(y, x) gets the character at a given position in the main canvas, while
mvwinch(*win, y, x) gets the character at a given position of a given window.
Drawing maps with ncurses
The code snippet that follows draws the initial map, which contains a pool with a fence surrounding it.
Let’s break it down a little. The variable
state holds the game state, and contains the game windoe (
gamew), the window width and height (
gw_h), the current score, the snake position and a few more pieces of information. In this snippet, we use
wattroff(*win, attr) and
wattron(*win, attr) to control the colors with which the characters will be printed. We use defines to link color location integers with meaningful names like
C_WATER (blue) or
Then, we can use
mvwhline(*win, y, x, char, num) and/or
mvwvline(*win, y, x, char, num) to create vertical and horizontal lines of length
num starting at
[x, y] with the character
char in the window
win. We also use defines for the character types. In this spirit, water tiles are
#define WATER '^' and the walls are
#define WALL '#'. With all this, building the maps is just a matter of putting the right tiles at the right places.
We only ever draw the map in two situations: when the game starts and when the window is resized. We will query the window state with
char mvwinch(*win, y, x), which returns the character at a given position in a window.
This is what the map produced by the code above looks like:
The game loop
Once we have printed the map, we start with the main loop. The main loop manages the resize events (even though they are not really events), manages the input and updates the state.
In order to support terminal resizing, we need to check whether the variables COLS and LINES have changed. If so, we redraw the map, reposition the snake and the food using interpolation and refresh the window.
Then, we use
getch() to get input keys. Usually, this function blocks the program until a new character is received.
However, ncurses allows the getch function to be non-blocking by using the
After managing the user input, we do the collision checking using the state stored in the game window by ncurses. Basically, if we hit any character other than a whitespace we have a collision. This results in a very simple collision check function:
At the end of the loop, we need to refresh both the standard screen and the window in order for the buffers to be applied to the terminal:
There are obviously many more aspects to the implementation of tsnake which have not been presented here. This article only gives a very rough bird’s eye view on how to use ncurses and the possibilities that the library offers.
Feel free to check the full source code in the gitlab repository. You can install the tsnake aur package if you are on Arch or derivatives.