Code Sample: Panaconda - A Persistent Memory Version of the Game Snake

File(s):Download
License:3-Clause BSD License
Optimized for... 
OS:Linux* kernel version 4.3 or higher
Hardware:Emulated: See How to Emulate Persistent Memory Using Dynamic Random-access Memory (DRAM)
Software:
(Programming Language, tool, IDE, Framework)
C++ Compiler and Persistent Memory Developers Kit (PMDK) 
Prerequisites:Familiarity with C++


Introduction

Panaconda Game

Snake is a beloved game from childhood where you use arrow keys to navigate the board, pick up food to grow your snake, and avoid hitting walls or your own tail. Panaconda is a game of Snake designed to demonstrate persistent memory pools, pointers, and transactions. All objects are stored in persistent memory, which means that in the case of a power failure or application crash, the state of the game will be retained and you can continue playing from the point you were at before the failure. In this example, we demonstrate the details of what makes this game persistent, discuss how you can make your applications persistent by using similar methods, and wrap up with how to play the Panaconda game.

This article assumes you have basic knowledge of persistent memory and the concepts used in Persistent Memory Development Kit (PMDK) libraries. In our article, Introduction to Programming with Persistent Memory from Intel we provide a great introduction to what persistent memory is and why it is revolutionary. For setting up your development environment, refer to our Getting Started guide. Pmem.io has a great tutorial series describing use of the libpmemobj library for persistent memory programming. It is highly recommended to at least read part 1, which introduces basic concepts demonstrated in this article. 


Game Design

In Panaconda, everything happens within a while loop in main. Until the snake is stopped for any reason, it will loop taking steps, setting food, and checking for collisions.

while (!snake_game->is_stopped()) {
	…
}

Data structures

data structure flowchart
Figure 1. Data structure for Panaconda.

The game has three main classes: game, game_board, and snake. In the above diagram, you can see the additional classes and how they interact. The game class is the root object. This object is what anchors all the other objects created in the persistent memory pool. Through the game class, all other objects in the pool can be reached. This happens in the init() function of game, as shown:

persistent_ptr<game_state> r = state.get_root();

Game

In addition to being the root object, the game class checks whether the game file specified already exits. In the snippet below, the pool checks LAYOUT_NAME stored in game_state to see if it matches the game file passed in. This code is looking to see if the pool already exists. Whether the pool is being created or it already exists and is being opened, it is being assigned to the Pool Object Pointer (pop) variable.

if (pool<game_state>::check(params->name, LAYOUT_NAME) == 1)	
    pop = pool<game_state>::open(params->name, LAYOUT_NAME);
else
	pop = pool<game_state>::create(params->name, LAYOUT_NAME,
				           PMEMOBJ_MIN_POOL * 10, 0666);

In game::init we see our first transaction. This transaction wraps the maze setup process. If a power failure happens, the data structure does not get corrupted because all changes are rolled back. More details about transactions and how they are implemented can be found in the C++ bindings for libpmemobj (part 6) – transactions on pmem.io.

transaction::exec_tx(state, [&r, &ret, this]() {
	r->init();
	if (params->use_maze)
		ret = parse_conf_create_dynamic_layout();
	else
		ret = r->get_board()->creat_static_layout();

	r->get_board()->create_new_food();
});

use_maze is set to true if the game was started with the "–m" tag. If a custom maze is passed in, the game creates a dynamic layout, else it creates a static, predefined layout.

In this implementation of snake, the game_player class stores score and play_state. The state can be: STATE_NEW, STATE_PLAY, or STATE_GAMEOVER.

Game_board

Game_board creates persistent pointers to food, layout (the map), and snake. This is also where you would change the game board size if you were to create your own map.

game_board::game_board()
{
	persistent_ptr<element_shape> shape =
		make_persistent<element_shape>(FOOD);
	food = make_persistent<board_element>(0, 0, shape,
					      direction::UNDEFINED);
	layout = make_persistent<list<board_element>>();
	anaconda = make_persistent<snake>();
	size_row = 20;
	size_col = 20;
}

In the above snippet, the following objects are returned as persistent object pointers:

food—a persistent pointer to a board element of shape FOOD, with no point nor direction defined

layout—a persistent pointer to a list of board elements

anaconda—a persistent pointer to a snake object, which ultimately is a list of board elements

All of these allocations are part of a transaction, so if the game aborts, the allocations are rolled back, reverting any memory allocation back to its original state. More information about the make_persistent function can be found by reading C++ bindings for libpmemobj (part 5) – make_persistent.

Keep in mind, in the game_board destructor, these pointers are deleted using the following syntax:

game_board::~game_board()
{
	layout->clear();
	delete_persistent>(layout);
	delete_persistent(anaconda);
	delete_persistent(food);
}

Another function of the game_board class is to keep track of collisions. If the snake's head hits food, the snake gets longer and the game gets harder. Collisions happen between the snake and food, between the snake and a wall, and between the snake and its own body.

bool is_snake_collision(point point);
bool is_wall_collision(point point);
bool is_snake_head_food_hit(void);

Snake

In the snake class, snake_segments is a persistent pointer to a list of board_element objects. Initially, the snake is populated with five segments. More segments are added as the snake hits food.

snake_segments = make_persistent<list<board_element>>();

The move function in snake uses persistent pointers and a for loop to iterate through each element of snake_segments. The loop iterates backwards to assign the previous snake_segments point and location to the next segment. This gives the look of the snake moving. When the loop reaches the first element of snake_segments, it calculates the next position and sets the direction based on the direction that was passed into the function.

void snake::move(const direction dir)
{
	int snake_size = snake_segments->size();
	persistent_ptr<point> new_position_point;

	last_seg_position = *(snake_segments->get(snake_size - 1)->get_position().get());
	last_seg_dir = snake_segments->get(snake_size - 1)->get_direction();

	for (int i = (snake_size - 1); i >= 0; --i) {
		if (i == 0) {
			new_position_point =
				snake_segments->get(i)->calc_new_position(dir);
			snake_segments->get(i)->set_direction(dir);
		} else {
			new_position_point =
				snake_segments->get(i)->calc_new_position(
					snake_segments->get(i - 1)->get_direction());
			snake_segments->get(i)->set_direction(
				snake_segments->get(i - 1)->get_direction());
		}
		snake_segments->get(i)->set_position(new_position_point);
	}
}

As you can see in the image below, the snake is basically a moving array of snake_segments. Each element of snake_segments contains the x, y point where it is located and the direction it is going. When snake::move(const direction dir) is called, each element takes the position of the one in front of it. The first element moves based on the direction that was passed into the function.

Panaconda Game
Figure 2. Image of snake_segments before and after move function.


To Play

First, download and build Panaconda. Installation assistance, including dependencies, can be found in the PMDK readme.

Launch the game

$./panaconda /path/game/session/gameFile

The gameFile is where the game session is stored. This is either created the first time you play, or you can open a game file where you previously played. If this is your first time, make up a name for your file and start the game like this:

$./panaconda myFirstGameFile

Additionally, you can create your own game maze or use a friend's. "-m" specifies that you want to use a custom maze.

$./panaconda /path/game/session/gameFile –m /path/myMapCfg

panaconda/conf.cfg contains an example of a predefined maze. The maze is defined using a bitmap, where "1" is a wall and "0" is an open space. Currently, the map size is limited to 80 x 40 bits, but that is configurable in the code. Try creating your own maze and see if you can beat it; then share your maze with a friend!

Controls

Panaconda uses the arrow keys to move. "q" quits the game and "n" creates a new game.

To simulate killing your game, press "ctrl+c", "q", or execute "kill –p `pgrep panaconda`" in another terminal. This returns you to the command line and exits you from your game. To resume, simply launch the game again using the same game file you previously specified. Because of the game's persistent aspects, it resumes exactly where you left off.

Objective

The goal of the game is to stay alive as long as possible while growing your snake longer and longer. The snake grows longer when it runs into a food block. But be careful to avoid hitting any obstacles, walls, or other parts of the snake itself.


Summary

In this code sample, we saw examples of transactions and persistent pointers. We examined game, the root object that anchors all the other objects in the persistent memory pool. This is just one example of how persistent memory can be used. Although simple and fun, this sample demonstrates fundamental persistent memory programming concepts. If you're interested in learning more, I encourage you to dig deeper into Panaconda, or explore our other code samples on software.intel.com or in our GitHub* repo.

The PMDK is available on GitHub and on the Persistent Memory Programming home page.


About the Author

Kelly Lyon is a Developer Advocate at Intel Corporation with three years of previous experience as a Software Engineer. Most recently, Kelly was a maintainer of Snap, Intel’s open source telemetry framework. Kelly is dedicated to advocating for users and looks forward to bringing clarity to complex ideas by researching and providing simple, easy to understand guides and tutorials. Follow her journey on Twitter @a_lyons_tale.



References

For more complete information about compiler optimizations, see our Optimization Notice.