In this version of the program, we add many features to the game that go beyond the basic concepts of a Nibbles clone. We will add a scoring system for when the snake eats apples and completes levels. We will use this scoring system to add a Hi Score board. (Yes, 'High' is misspelled, but it looked right at the time). We will also add a status display so the user can see how many lives he has, his score, and other useful information. We also fix a few bugs and code issues in this version.
As before with version 0.3, you will need to download a new source archive for version 0.4. Binaries are also available on Windows and Linux with GTK+ 2.0.
Download [ Source (.zip) | Win32 Binary | Source (.tar.bz2) | Linux Binary (GTK+ 2.0) ]
There are some new classes here, but we'll start with what we already know about. Take a look at the NibblesFrame constructor.
// create the status bar CreateStatusBar(4);We added a line here to create a status bar where our status display will be. There are four segments in the status bar: score, lives, remaining apples, and current level. Now let's look at the changes in createMenuBar().
// create the game menu wxMenu *gameMenu = new wxMenu; gameMenu->Append(IDM_GAME_INPUT, wxT("Input Settings\tF1")); gameMenu->Append(IDM_GAME_HISCORE, wxT("Display Hi Score Board\tF10")); // append the game menu to the menu bar mb->Append(gameMenu, wxT("&Game"));The edit menu we created in version 0.3 has been replaced by this new Game menu. We also have a new item on the menu which will display the Hi Score board at any time.
If you look at the EVT_CLOSE handler, you'll notice we fixed the bug where it would still close if you clicked no by vetoing the event. We'll move on to the NibblesPanel class now. We've added the following code to the EVT_PAINT handler onPaint().
// update the status bar information updateStatusBar();Now while we're drawing, we update the status display. Let's take a look at the updateStatusBar() method.
void NibblesPanel::updateStatusBar() { wxFrame &frame = static_cast<wxFrame &>(*GetParent()); wxString str; // display the score str.Printf("Score: %lu", game.getScore()); frame.SetStatusText(str, 0); // display the current level str.Printf("Level: %u", (game.getLevel().getLevel() + 1)); frame.SetStatusText(str, 1); // display the apples remaining str.Printf("Apples: %d", game.getApples()); frame.SetStatusText(str, 2); // display the lives remaining str.Printf("Lives: %d", game.getSnake().getLives()); frame.SetStatusText(str, 3); }For each segment of the status bar, we simply change the text. It's a very simple status display. It would be nicer if certain parts were graphical, but again, it wasn't a priority for me. Let's take a look at one more change in the onPaint() method.
// end the Game endGame();This replaces the call to game.end(). We need a special method so we can check to see if the user place on the Hi Score board.
void NibblesPanel::endGame() { // if we're playing if (game.isPlaying()) { // check for a new hi score checkForHiScore(); // end the game game.end(); } }The endGame() method simply checks to see if the user got a Hi Score, then it calls Game::end(). Let's check out checkForHiScore() now.
void NibblesPanel::checkForHiScore() { // which position did the player earn int position = findHiScorePosition(); // Top 5: place it on the board if ((position >= 0) && (position < 5)) { addHiScore(position); } }We use findHiScorePosition() to see where the user placed. 0-5 is a new Hi Score, and anything else is not.
int NibblesPanel::findHiScorePosition() { int pos = 5; // until we hit the top place while (pos > 0) { // if the next place higher than us if (hiscores[pos - 1] >= game.getScore()) { // then break break; } // otherwise keep looking --pos; } // return our hi score position return pos; } void NibblesPanel::addHiScore(int position) { // Get the player's name and create the HiScore wxString name = wxGetTextFromUser(wxString::Format("You Made Position #%d!", (position + 1)), wxT("New Hi Score!"), wxEmptyString, this); HiScore hiscore(name, game.getScore()); // move the other hiscores down for (int i = 4; i > position; i--) { hiscores[i] = hiscores[i - 1]; } // add the new HiScore hiscores[position] = hiscore; // show the HiScore Board showHiScoreBoard(); }findHiScorePosition() just runs through the Hi Score array and sees if the current score outranks any scores on the board. addHiScore() asks for the user to enter his name, and then puts the score on the board. Now let's head over to the updated NibblesPanel::newGame() method.
void NibblesPanel::newGame() { // confirm ending the current game before starting a new one if (game.isPlaying()) { int result = wxMessageBox(wxT("Quit current game?"), wxT("Warning: Game in Progress"), wxYES_NO | wxICON_QUESTION, this); if (result == wxNO) { return; } } // end any old game endGame(); // query for difficulty level const wxString choices[] = { wxT("Novice"), wxT("Average"), wxT("Expert") }; int result = wxGetSingleChoiceIndex(wxT("Please select a difficulty level"), wxT("Difficulty Level Selection"), 3, choices, this); // get the focus back SetFocus(); if (result != -1) { enum Difficulty difficulty = static_cast<enum Difficulty>(result); // start a new game game.start(difficulty); // refresh the screen Refresh(); } }This is a little more complex than what we used to do. For starters, we moved the game in progress question from NibblesFrame::onNewGame() here. After we're sure the user wants a new game, we ask them to choose a difficulty level. A better control here would be nice, but I was too lazy to go custom. After the user has selected, we start the game. Finally, let's take a look at the showHiScoreBoard() method.
void NibblesPanel::showHiScoreBoard() { wxString board, temp; // build the Hi Score Board string for (int i = 0; i < 5; i++) { temp.Printf("%d: %lu %s\n", (i + 1), hiscores[i].getScore(), hiscores[i].getName().c_str()); board += temp; } // display the scores wxMessageBox(board, wxT("Hi Score Board"), wxOK); }This method just creates a big wxString which we display in a message box. This would be another place where a custom control would be a little nicer, but again, I was too lazy for that. Let's move on to the Game class and the Game::start() method.
// reset the level level.setLevel(0); // reset the score score = 0; // reset the remaining apples apples = APPLES[difficulty];We have added these lines since the 0.3 version. We reset the level, score, and apples. We also reset the apples in Game::restart(). Moving on to the Game::tick() method.
// did the snake eat the apple? if (snake->getSegments()[0] == apple) { // decrement apples remaining --apples; // add apple points score += APPLE_POINTS * BONUS[difficulty]; if (apples > 0) { // grow the snake snake->addSegment(3); } else { // pause the game togglePause(); // add level completion points score += LEVEL_POINTS * BONUS[difficulty]; // advance to the next level level.nextLevel(); // reincarnate the snake snake->reincarnate(); // reset the apples apples = APPLES[difficulty]; } // randomize the apple position moveApple(); }Eating the apple now earns points for the player, and the possibility of level completion. So, we decrement the apple count, add the points for eating the apple, and then check if we've eaten enough apples for this level. If not, the snake grows. Otherwise, we pause the game, add points for level completion, move to the next level, reset the apple count, and finally reincarnate the snake. This version finally fixes our direction problem, so let's take a look at the new Snake::changeDirection() method.
void Snake::setDirection(enum Direction direction) { // make sure we don't run into ourself const wxPoint &head = segments[0]; const wxPoint &body = segments[1]; wxPoint temp; if (direction == NORTH) { // old position is south temp = wxPoint(head.x, head.y - BLOCK_SIZE); } else if (direction == EAST) { // old position is west temp = wxPoint(head.x + BLOCK_SIZE, head.y); } else if (direction == SOUTH) { // old position is north temp = wxPoint(head.x, head.y + BLOCK_SIZE); } else { // old position is east temp = wxPoint(head.x - BLOCK_SIZE, head.y); } // if we won't crash, set the direction if (temp != body) { this->direction = direction; } }Before, we simply changed the direction to whatever was set. This forced the caller to ensure that the direction was valid, and was a bad design. Our new method doesn't change direction unless it's not suicidal for the snake. As HiScore is an extremely simple class, I won't be going over anything in it.
The game's starting to be really nice now. It has status display, level completion, a point system, and a hi score board. Only one part remains in our tutorial.
Take a look at part 5 when you're ready to continue. Feel free to contact me if you have questions.
Need to contact us? We can be reached by email or via our online feedback form.