Recap
In the last article, we managed to get a basic version of the Pipes application running – albeit a simple one that only plays a single 5 x 5 puzzle. In this installment, we will add the code to handle puzzles of different sizes and the ability to select a level and puzzle number.
The sample code detailed in this article can be found in the repository at https://github.com/filmote/Pipes_Article3_Pokitto. The game is totally playable now but needs more cowbells to make it complete. If you don’t understand that reference, you need to watch the SNL comedy sketch here More Cowbell - SNL - YouTube
Adding Levels
To date, our game play has been limited to two 5 x 5 puzzles. By now you are probably sick of them and can do both with your eyes closed! Let’s fix that immediately by adding a puzzles of different sizes to the Puzzles.h file. Due to the different sizes of the puzzles, they must all be declared in their own array. A subset of the puzzles is shown below.
const uint8_t puzzles_5x5_count = 2;
const uint8_t puzzles_5x5[] = {
0x10, 0x20, 0x40,
0x00, 0x30, 0x50,
0x00, 0x00, 0x00,
0x02, 0x04, 0x00,
0x01, 0x35, 0x00,
0x10, 0x00, 0x00,
0x00, 0x00, 0x00,
0x00, 0x40, 0x00,
0x24, 0x30, 0x10,
0x30, 0x00, 0x20,
}
const uint8_t puzzles_6x6_count = 2;
const uint8_t puzzles_6x6[] = { ... }
const uint8_t puzzles_7x7_count = 2;
const uint8_t puzzles_7x7[] = { ... }
const uint8_t puzzles_8x8_count = 2;
const uint8_t puzzles_8x8[] = { ... }
const uint8_t puzzles_9x9_count = 2;
const uint8_t puzzles_9x9[] = {
0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x23, 0x00, 0x00, 0x00,
0x00, 0x02, 0x40, 0x43, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00,
0x06, 0x06, 0x10, 0x00, 0x00,
0x75, 0x00, 0x00, 0x09, 0x80,
0x08, 0x70, 0x00, 0x00, 0x00,
0x09, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
...
}
Loading the Board (Revisited)
The initBoard()
function has undergone a few changes to date and the final tweak is to change it to accept a level parameter in addition to the original puzzle number one. The original code in the function remains unchanged however additional functionality has been added to allow the program to read from the different puzzle arrays we declared above.
The puzzleType
parameter is a byte which indicates the size of the puzzle from 5 to 9. Using this number, we can work out how many bytes to read per puzzle. In our original code, this value was hard-coded to 15 bytes which equated to 3 bytes per row multiplied by 5 rows. As you may have worked out by now, a 6 x 6 puzzle also uses three bytes per row but has 6 rows.
When determining how many bytes to read, we determine whether the puzzle size is an even number - puzzleType % 2 == 0
- and, if so, we need to read in one half of the width multiplied by the number of row bytes - (puzzleType / 2) * puzzleType
. If the puzzle size is an odd number, we need to round the puzzle size up to the next even number, halve that and multiply in by the number of rows - ((puzzleType + 1) / 2) * puzzleType
.
If the use of the modulus (%) function is unfamiliar to you, read more in the accompanying article Modulus in C / C++
When reading the actual puzzle data from the array, a simple switch statement is used to retrieve from the correct array.
#define PUZZLE_5X5 5
#define PUZZLE_6X6 6
#define PUZZLE_7X7 7
#define PUZZLE_8X8 8
#define PUZZLE_9X9 9
void initBoard(uint8_t puzzleType, uint8_t puzzleNumber) {
uint8_t x = 0;
uint8_t y = 0;
uint8_t byteRead = 0;
uint8_t bytesToRead = (puzzleType % 2 == 0 ? (puzzleType / 2) * puzzleType :
((puzzleType / 2) + 1) * puzzleType);
for (uint8_t i = (puzzleNumber * bytesToRead); i < ((puzzleNumber + 1) * bytesToRead); i++) {
switch (puzzleType) {
case PUZZLE_5X5:
byteRead = puzzles_5x5[i];
break;
case PUZZLE_6X6:
byteRead = puzzles_6x6[i];
break;
case PUZZLE_7X7:
byteRead = puzzles_7x7[i];
break;
case PUZZLE_8X8:
byteRead = puzzles_8x8[i];
break;
case PUZZLE_9X9:
byteRead = puzzles_9x9[i];
break;
}
...
}
}
Rendering the board.
The existing code to render the board will happily render puzzles of any size – however with the larger puzzles portions of the board will not be visible. Later in this article, we will look at how we can make the board scroll as the user moves around.
Storing the Player’s Progress.
My version of the Pipes game will not let a player advance to the next puzzle in each level until they have completed the one before that. I store and retrieve the progress of each level in the `EEPROM` using functionality provided as part of the Pokitto library, known as `Cookies`.
In Pokittoland, a cookie is a class that exposes a bunch of variables relevant to your game or application that you want to store in the EEPROM
. As you can see below, the cookie used for Pipes has five variables that are used to store the progress of each level. I have also defined another field, called initialised that is used to determine whether the cookie has been initialised before its first use.
class GameCookie : public Pokitto::Cookie {
public:
uint8_t initialised;
uint8_t level5x5 = 0;
uint8_t level6x6 = 0;
uint8_t level7x7 = 0;
uint8_t level8x8 = 0;
uint8_t level9x9 = 0;
public:
void initialise() {
initialised = COOKIE_INITIALISED;
level5x5 = 0;
level6x6 = 0;
level7x7 = 0;
level8x8 = 0;
level9x9 = 0;
saveCookie();
}
};
You may also have noted that the cookie extends Pokitto::Cookie
. By extending this class, our cookie knows how to save itself using the inherited saveCookie()
method.
The logic for creating and initializing the cookie is shown below and is taken from the main.cpp
file which, as discussed in the first article, is used to initialize the Pokitto before going into an endless loop of gameplay.
In addition to the code Pokitto library, the program now includes the PokittoCookie
library and our own GameCookie
implementation. An instance of a GameCookie
is created followed by a call to an inherited method, begin()
. This method, accepts a unique name for the cookie, the size of the cookie and the cookie value itself. The method first checks to see if a cookie exists with that name and, if so, populates the cookie with values retrieved from the EEPROM
.
If no matching cookie is found, the method reserves enough memory in the EEPROM
to store the cookie. Cookies can be any length and this process will allocate enough memory to store the entire cookie.
#include "Pokitto.h"
#include "PokittoCookie.h"
#include "src/utils/GameCookie.h"
#include "src/Game.h"
#define COOKIE_INITIALISED 34
Game game;
GameCookie cookie;
int main() {
cookie.begin("PIPES", sizeof(cookie), (char*)&cookie);
Once the cookie has been created and loaded, the initialised
variable is tested to see if the cookie has just been created. If it has, the variables are often filled with random data that was read from the EEPROM
rather than known values. The value of COOKIE_INITIALISED
is irrelevant but allows us to test the retrieved cookie for a known value.
// Has the cookie been initialised?
if (cookie.initialised != COOKIE_INITIALISED) {
cookie.initialise();
}
Finally, the created and initialised cookie can be passed to the game class and the game started!
// Play game!
game.setup(&cookie);
while (PC::isRunning()) {
if (!PC::update()) continue;
game.loop();
}
return 0;
}