Recap
In the last article we put together the framework for retrieving our puzzle from memory, populating our multidimensional array and rendering the puzzle on the screen.
In this tutorial we will concentrate on the actual game play itself – still in the confines of a puzzle that is 5 x 5 cells however we start to add functionality that will allow us to move to large puzzles later. The play logic consists of two distinct modes – selecting a node and then laying pipe.
The sample code detailed in this article can be found in the repository at https://github.com/filmote/Pipes_Article2_Pokitto. The game is playable now but only with the first puzzle as it is hardcoded in the setup()
routine.
Structures
Before diving into the first game playing mode, I want to draw your attention to a number of
data structures that I have defined to capture game play information. If you aren’t familiar with structures refer to the accompanying article Structures.
The first, Node
, represents a node in the puzzle and contains three variables that hold the coordinates and cell value respectively. The second, Player, uses this new data type to capture the player’s game state including the selected node and highlighted node.
The final structure holds the current puzzle details including the puzzle number being played, the game’s dimensions - represented as a Node so we have width (x) and depth (y) dimensions – and the puzzle board itself. As development of the game continues we will add additional details to this structure to hold the current level and offset information to render the board centrally in the screen. For the moment it only has three members.
struct Node {
uint8_t x;
uint8_t y;
uint8_t value;
};
struct Player {
Node highlightedNode;
Node selectedNode;
}
player;
struct Puzzle {
uint8_t index;
Node maximum;
uint8_t board[9][9];
}
puzzle;
Game Play
Game play consists of three different modes –
- game initialization
- node selection
- laying pipe
The following sections describe each of the modes. As the user selects nodes or completes the laying of pipe, the code transitions from one mode (or state) to another via the main loop()
. As shown below, the current mode is stored in the gameState
variable and this controls the flow of the application.
void Game::loop() {
PC::buttons.pollButtons();
switch (gameState) {
…
case STATE_INIT_GAME:
play_InitGame();
[[fallthrough]]
case STATE_NO_SELECTION:
play_NoSelection();
break;
case STATE_NODE_SELECTED:
play_NodeSelected();
break;
…
}
}
Mode One: Game Initialization
The setup()
method has also been modified and I have hard-coded the puzzle number and dimensions of the first puzzle. Before exiting the procedure, the gameState
variable that controls the overall operation of the game is updated STATE_NO_SELECTION
to progress it immediately to the game mode where the user has yet to select a node.
If you refer back loop()
code above, you will notice that the case
for the game initialisation does not break
but rather uses the compiler directive [[fallthrough]]
. Normally, each case clause in a switch is terminated by a break statement thus telling the code to complete execution and to move to the next statement after the switch. Using the [[fallthrough]]
directive allows code to immediately execute the STATE_NO_SELECTION
clause when it has finished initializing the game in the previous clause.
void Game::play_InitGame() {
puzzle.index = 0;
puzzle.maximum.x = 5;
puzzle.maximum.y = 5;
initBoard(puzzle.index);
gameState = STATE_NO_SELECTION;
}