// the main game loop
while (!done) {
// setup game play
drawLevel(level);
initSnake(snakeptr);
drawSnake(snakeptr);
drawScore(score);
currentApple = drawApple(level, snakeptr);
applePoints = getApplePoints(level, speed, difficulty);
apples = totalApples;
drawApples(apples);
drawLives(snake.lives);
Okay, here is our main game loop. This loop is responsible for taking
the player through the levels, responding to key presses, moving the
snake, generating the apples, and all the other things that are part of
playing a Nibbles clone. The first step is to draw the level using the
drawLevel() function. Note that the play function initialized the level
variable to be 0.
// drawLevel - draws one of the maze levels
void drawLevel(short int level) {
short int yLoop,xLoop;
long int area, mask;
// clear the screen first
ClrScr();
for (yLoop = 0; yLoop < YTILES; yLoop++) {
area = levels[level][yLoop];
mask = 0x100000;
// start the x loop
for (xLoop = 0; xLoop < XTILES; xLoop++) {
// shift the mask index
mask >>= 1;
// if the level data says to put a block here, then do it
if (area & mask) {
drawBlock(xLoop * WIDTH, yLoop * HEIGHT, block, SPRT_OR);
}
}
}
// share the bottom line of the maze
clearDisplayLine();
}
If you went through lesson 8, this function should look very familiar.
It's nearly identical to the drawMap() function from lesson 8 part 3. In
fact, this is where I got the code in the first place. The differences
are very subtle. First, we have more than one level, so we need to grab
the appropriate level from the level definitions in gfx.h. Basically, we
loop through the y lines, and for each x position in the y that we find
a 1 in the level map, we draw a block on the screen using the
drawBlock() function.
// drawBlock - draws a block on the screen (all nibble graphics are composed of blocks)
inline void drawBlock(short int x, short int y, unsigned char *sprite, short int mode) {
Sprite8(x,y,HEIGHT,sprite,LCD_MEM,mode);
}
The drawBlock function simply draws a block on the screen. There are
three kinds of 'blocks'. The normal bricks, which make up the level
boundaries and obstacles. The empty blocks which are used to erase the
snake, and the 'apple shaped' blocks which somewhat resemble an apple.
I'm not much of an artist, but I did the best I could.
The final part of the drawLevel() function clears the bottom display
lines. Since the bottom part of the map is always the bottom border, we
can share the bottom pixels with some indicator texts.
// clearDisplayLine - removes the bottom 3 lines of the bottom border
// for extra indicator display room
void clearDisplayLine(void) {
short int loop;
for (loop = 92; loop < 95; loop++) {
DrawLine(0,loop,160,loop,A_REVERSE);
}
}
The clearDisplayLine function just erases the bottom 3 lines of the
border so we can share that space with the display indicators.
Now we move on to the initSnake() routine.
// initSnake - sets up the snake parameters
void initSnake(SNAKE *snake) {
short int loop;
// reset the snake to standard
snake->dead = FALSE;
snake->length = 3;
snake->direction = RIGHT;
// setup the initial position
for (loop = 0; loop < 4; loop++) {
snake->location[loop].x = 5 - loop;
snake->location[loop].y = 3;
}
// clear all the excess positions
for (loop = 3; loop < 28; loop++) {
snake->location[loop].x = 0;
snake->location[loop].y = 0;
}
}
The initSnake routine simply sets up the snakes initial settings. The
snake starts out alive, with a length of 3, moving right. Since we use
the same snake structure, we need to make sure all the parts past his
length are empty. This is because his location parts are how we will
draw him on the screen.
Now that we have a snake, we should draw him on the screen. Call the
drawSnake() function for this.
// drawSnake - draws the snake on the LCD
void drawSnake(SNAKE *snake) {
POSITION pos;
short int loop;
// draw the snake segments
for (loop = 0; loop < snake->length; loop++) {
pos = snake->location[loop];
drawBlock(pos.x * WIDTH, pos.y * HEIGHT, block, SPRT_OR);
}
// remove the last segment to create the illusion of movement
pos = snake->location[snake->length];
if (pos.x != 0 && pos.y != 0) {
drawBlock(pos.x * WIDTH, pos.y * HEIGHT, empty, SPRT_AND);
}
}
So, to draw the snake, we need to put a block for each POSITION of the
snake tail. Finally, drawSnake is called each time the snake moves, so
erase the last block of the snake to simulate movement.
Now we can draw the score indicator. For now, it will be 0, but that
will soon change.
// drawScore - draws the current player score at the lower-left
inline void drawScore(unsigned long int score) {
printf_xy(SCOREX,SCOREY,"%lu",score);
}
The drawScore method simply displays the score in the lower left side of
the screen. The printf_xy function is not actually based on printf. It
is actually a macro that calls sprintf and DrawStr. The DrawStr is
called with the A_REPLACE option, so each score will replace the last
one automatically.
Next, we draw an apple on the grid. To do this, we call the drawApple()
function.
// drawApple - draws an apple on screen at a randomly selected location
// returns - the position of the newly drawn apple
POSITION drawApple(short int level, SNAKE *snake) {
POSITION pos = {0,0};
// find a suitable random position for the apple
while (hasHitWall(pos,level) || hasHitSnake(pos,snake,SNAKE_HIT_APPLE)) {
pos.x = random(XTILES);
pos.y = random(YTILES);
}
// draw the apple sprite
drawBlock(pos.x * WIDTH, pos.y * HEIGHT, apple, SPRT_XOR);
// return the apple's position
return pos;
}
In this function, we find a random position for the apple to be drawn.
The only requirement is that we don't draw on the level walls or the
snake. So, we check the hasHitWall and hasHitSnake functions to make
sure we haven't done that. Finally, we draw an apple block, and return
the POSITION we came up with.
// hasHitWall - checks to see whether the snake hit a wall
// returns - TRUE if snake hit wall, FALSE otherwise
short int hasHitWall(POSITION pos, short int level) {
long int mask = 0x80000, area;
short int crash = FALSE;
// find the search area
area = levels[level][pos.y];
mask = mask >> pos.x;
// if we have a match, we crashed
if (area & mask) {
crash = TRUE;
}
return crash;
}
To check whether a POSITION intersects the wall, we first find the row
of the level we are on. Then, we check to see if our position x
intersects one of the block locations in the level data. So, to find the
level row, area is the levels array of the level we want to search, with
row pos.y. Then, our mask, what we check for, is a single bit 0x80000,
or binary 10000000000000000000 giving us 20 bits, the number of
horizontal blocks in the level. So, shift our mask bit right by the x
coordinate, which ranges from 0 - 19. Then, if area & the mask bit
returns a true value, we have an intersection (crash). Otherwise, we
don't. See how it works? This should be very familiar from lesson 8. If
you need some more review, take a look at lesson 8 again.
// hasHitSnake - checks to see if the snake hit itself
// returns - TRUE if the snake hit itself, FALSE otherwise
short int hasHitSnake(POSITION pos, SNAKE *snake, short int start) {
short int loop;
// loop through the body of the snake
for (loop = start; loop < snake->length; loop++) {
// if the position and the snake match, we hit the snake
if ((snake->location[loop].x == pos.x) && (snake->location[loop].y == pos.y)) {
return TRUE;
}
}
return FALSE;
}
Checking for snake hits is different though. Instead of needing to check
a single row, we have to check every place the snake is. So, if any of
the snake positions are the same as our POSITION position, we hit the
snake.
Now, for our scoring system, we need to know how many points an apple is
worth in this level, based on our speed and difficulty settings. The
faster you play and harder level you select, the more points you get,
obviously. This is the bonus for playing the hard levels.
// getApplePoints - calculates the number of points the apple is worth
// returns - points apple is worth
short int getApplePoints(short int level, short int speed, short int difficulty) {
short int speedBonus;
// determine speed bonus
if (speed <= MEDIUM) {
speedBonus = 0;
} else if (speed > MEDIUM && speed < QUICK) {
speedBonus = 25;
} else if (speed > AVERAGEPLUS && speed < VERYFAST) {
speedBonus = 50;
} else { // speed = VERYFAST
speedBonus = 100;
}
// calculate and return bonus
return (((level * 5) + 10 + (speedBonus)) * (++difficulty));
}
This function is just some basic math operations. It should be easy
enough.
Lastly, we just have to draw the apple and live indicators that tell the
player how many apples are left to eat in this level, and how many lives
he has.
// drawLives - displays the life indicator using life tokens
void drawLives(short int lives) {
short int loop, x = 76, max = lives;
if (max > MAX_LIVES) {
max = MAX_LIVES;
}
// draw a life token for each remaining life
for (loop = 0; loop < max; loop++) {
x+=4;
drawToken(x,97,lifeToken);
x++;
}
}
// drawApples - displays remaining apples using apple tokens
void drawApples(short int apples) {
short int loop, x = 76;
// draw one apple token for each remaining apple
for (loop = 0; loop < apples; loop++) {
x+=4;
drawToken(x,94,appleToken);
x++;
}
}
These two functions simply draw the live and apple token indicators. The
only thing to note is that it's possible to have more lives than we can
display. If that happens, we simply display as many life tokens as
possible, then stop. This is often done when tokens are used for
indicator in games. Certain scores give us extra lives, so we don't want
to draw off-screen, or worse, into memory that isn't part of the screen
buffer. This could cause the calc to crash.
Well, all that and we're finally ready to play the game. See how easy
game programming is?
begin = FALSE;
while (!begin) {
while ((key = getKey()) == 0);
if (key == KESC) {
snake.dead = TRUE;
begin = TRUE;
done = TRUE;
ending = QUITTER;
} else if (key == KENTER) {
begin = TRUE;
}
// wait for keypresses to dissipate
delay(KEYDELAY);
}
Here is our wait loop. We'll wait for the user to press ENTER to start
the level. Or, if the user wants, we can press ESC to end the game.
Before we start, make sure the key presses are gone, as ENTER is also
our pause key.
while (!snake.dead && apples > 0) {
for (loop = 0; loop < speedDelay; loop++) {
if ((key = getKey()) != 0) {
if (speed < MEDIUM) {
keyDelay = (6 * KEYDELAY) / 7;
} else if (speed < AVERAGEPLUS) {
keyDelay = (2 * KEYDELAY) / 3;
} else if (speed < FAST) {
keyDelay = (53 * KEYDELAY) / 100;
} else {
keyDelay = KEYDELAY / 2;
}
delay(keyDelay);
break;
}
}
Here is our level loop. We do this loop until we are either dead, or we
have eaten all the apples in the level.
switch (key) {
case KLEFT:
if (snake.direction != RIGHT) {
snake.direction = LEFT;
}
break;
case KUP:
if (snake.direction != DOWN) {
snake.direction = UP;
}
break;
case KDOWN:
if (snake.direction != UP) {
snake.direction = DOWN;
}
break;
case KRIGHT:
if (snake.direction != LEFT) {
snake.direction = RIGHT;
}
break;
case KCHEAT:
if (cheater == TRUE) {
if (level < levels) {
level++;
}
apples = -1;
}
cheater = TRUE;
break;
case KESC:
done = TRUE;
snake.dead = TRUE;
ending = QUITTER;
break;
case KENTER:
// pause game
delay(KEYDELAY);
while (getKey() != KENTER);
delay(KEYDELAY);
break;
}
Here we respond to any keypress the user made. The arrow keys simply
change the direction the snake is moving. However, we can't move
directly opposite the direction we are already going, so make sure we
aren't doing that before we change directions.
If the user pressed the cheat key, we advance their level. If the user
pressed ESC, we exit the game. If we pressed the ENTER key, we pause the
game waiting for them to press ENTER again.
// move the snake
moveSnake(snakeptr);
Now, as part of the game loop, the snake needs to be always moving, so
we call the moveSnake function.
// moveSnake - moves the snake across the screen
void moveSnake(SNAKE *snake) {
short int loop;
// relocate the positions
for (loop = snake->length; loop >= 0; loop--) {
snake->location[loop] = snake->location[loop-1];
}
// grab the second position
snake->location[0].x = snake->location[1].x;
snake->location[0].y = snake->location[1].y;
// adjust the snake based on the direction
if (snake->direction == UP) {
snake->location[0].y--;
} else if (snake->direction == DOWN) {
snake->location[0].y++;
} else if (snake->direction == LEFT) {
snake->location[0].x--;
} else if (snake->direction == RIGHT) {
snake->location[0].x++;
}
// re-draw the snake at the new position
drawSnake(snake);
}
Okay, to move the snake, we simply shift all the snake positions one
down. So, the head becomes tail piece 1, tail piece 1 becomes tail piece
2, and so on. When its finished the end part of the tail disappears. So,
now we just need to find the new place for the head based on the
direction the snake is moving. Then, we redraw the snake.
// check for snake hitting things
if (hasHitWall(snake.location[0], level)) {
// hit wall
drawCrash();
snake.dead = TRUE;
} else if (hasHitSnake(snake.location[0], snakeptr, SNAKE_HIT_SELF)) {
// hit self
drawCrash();
snake.dead = TRUE;
Now we need to check if the snake crashed. Notice the third parameter of
hasHitSnake is SNAKE_HIT_SELF. Since we are comparing the snake to
itself, the head will appear to be hitting the head, so we do a special
check to make sure we don't check for the head of the snake. In our
case, the snake head has to hit some part of the tail. Logically, we
could have started 3 places down, because the head couldn't hit the
first or second tail piece based on the way the snake is able to move,
but that's okay. Our program is fast enough.
If we did crash though, we draw the crash logo and set the snake's dead
parameter to TRUE.
// drawCrash - draws the crash message across the screen
void drawCrash(void) {
short int x, offset = 0;
for (x = 0; x < CRASHWIDTH; x += 32) {
Sprite32(CRASHX+x,CRASHY,CRASHHEIGHT,crash+offset,LCD_MEM,SPRT_AND);
Sprite32(CRASHX+x,CRASHY,CRASHHEIGHT,crash+offset,LCD_MEM,SPRT_OR);
offset+=CRASHHEIGHT;
}
// wait for keypress
delay(KEYDELAY);
while (!getKey());
delay(KEYDELAY);
}
Here we draw the crash logo. Instead of using XOR logic though, we use
AND and OR logic. Basically, this will let us replace whatever is behind
the crash logo. So, it will erase any background, and the snake, and
anything else in the way.
Before returning from the crash, wait for the user to press a key.
} else if (hasHitApple(currentApple,snake.location)) {
// ate apple
growSnake(snakeptr);
currentApple = drawApple(level,snakeptr);
score += applePoints;
drawScore(score);
apples--;
eraseApple(apples);
if (apples == 0) {
// next level, add point and life bonuses
level++;
score+=500*difficulty+10*snake.lives;
snake.lives++;
if (level > levels) {
ending = WINNER;
done = TRUE;
score += difficulty*10000;
}
}
}
Finally, if we haven't hit a wall or ourselves, the only other even of
note is that we hit the apple. So, check for that.
// hasHitApple - checks to see whether the snake ate an apple
// returns - TRUE if they hit the apple, FALSE otherwise
short int hasHitApple(POSITION apple, POSITION *snake) {
short int snagged = FALSE;
// if the position and the apple position match, we snagged it
if ((apple.x == snake[0].x) && (apple.y == snake[0].y)) {
snagged = TRUE;
}
// otherwise, we didn't
return snagged;
}
Basically, all we need to do is see if the apple's position intersects
the snake's head, which is at snake position 0.
Then we make the snake grow using the growSnake() function.
// growSnake - makes the snake grow
void growSnake(SNAKE *snake) {
short int loop;
// add snake positions for growth
for (loop = snake->length; loop < (snake->length+SNAKE_GROW); loop++) {
snake->location[loop].x = snake->location[snake->length].x;
snake->location[loop].y = snake->location[snake->length].y;
}
// adjust the snakes length
snake->length += SNAKE_GROW;
}
Okay, the snake grows by replicating the end of the tail to the lower
pieces of the snake position array. When we redraw the snake, these
pieces will be moved and drawn along with the rest of the snake.
Finally, just update the snake length and we're done.
If there are no more apples to eat, we are finished with the level. Give
the player the bonus for finishing the level, some points and an extra
life.
Finally, if we are past the last level, we are the winner.
if (snake.dead && !cheater) {
// cheaters get infinite lives, but they pay for it
if (--(snake.lives) < 0) {
done = TRUE;
ending = LOSER;
}
}
If we crashed, the snake is now dead. If they are not using the cheat
function, decrement their lives. If they have no more lives, they have
lost the game.
if (cheater) {
score = 0;
ending = CHEATER;
}
// display ending message
drawEndMessage(ending);
// wait for keypress before continuing
delay(KEYDELAY);
while (!getKey());
delay(KEYDELAY);
If the player is cheating, don't allow them to keep their score at the
end. Cheaters should not be in the hall of fame. Then we draw the ending
message, either that they lost, quit, won, or cheated. These images are
defined in gfx.h.
Wait for the user to press a key before continuing.
// drawEndMessage - draws the message at games end (i.e. winner, loser, quitter)
void drawEndMessage(short int ending) {
short int x, offset = 0;
unsigned short int *sprite = endings[--ending];
for (x = 8; x < 152; x += 16) {
Sprite16(x,38,25,sprite+offset,LCD_MEM,SPRT_AND);
Sprite16(x,38,25,sprite+offset,LCD_MEM,SPRT_OR);
offset += 25;
}
}
The drawEndMessage function just displays a sprite, like the drawCrash()
function.
// display the hiscore board
drawHiScoreBoard(score);
At the end of the game, we draw the hiscore board. In addition, this
function supplies the user's score, which will be added to the hiscore table
if they acheived a hiscore.
// drawHiScoreBoard - draws the hiscore board, and updates it with the hiscore from the latest game
void drawHiScoreBoard(unsigned long int newscore) {
SCORE scores[MAX_HISCORES];
short int loop, pos = -1;
char name[10], str[50], *error = "File I/O Error";
HANDLE dlg;
// restore auto-interrupts
SetIntVec(AUTO_INT_1,autoint1);
SetIntVec(AUTO_INT_5,autoint5);
if (!loadHiScores(scores)) {
// cannot open hiscore file -- display error
DlgMessage(error,"Unable to Load HiScore Data",BT_OK,BT_NONE);
return;
}
The Hiscore board uses AMS keyboard handing so we can input the player's
name, so we first restore the interrupt handlers.
Then we load the hiscores from file using the loadHiScores() function.
// loadHiScores - loads the hiscores from file into the hiscore table
// returns TRUE if hiscores were read, FALSE otherwise
short int loadHiScores(SCORE *hiscores) {
short int success = TRUE;
FILE *f;
// open file for reading and check for errors
if ((f = fopen(scorefile,"rb")) == NULL) {
success = FALSE;
} else {
// read the hiscore table and check for I/O errors
if (fread(hiscores,sizeof(SCORE),MAX_HISCORES,f) != MAX_HISCORES) {
success = FALSE;
}
fclose(f);
}
return success;
}
The loadHiScores() function is very simple if you understood lesson 10.
We open the scorefile (defined at the top of hiscore.c) and read the
data into the hiscore array. If we encounter an I/O error, we return
FALSE.
// check if latest score is a highscore
for (loop = (MAX_HISCORES - 1); loop >= 0; loop--) {
if (newscore > scores[loop].score) {
// new HiScore!!
pos = loop;
}
}
Next we check to see if the score the player had is a hiscore. So, start
at the top and check to see if any score on the list is higher than it.
Equal scores do not replace old scores on the list. If we find one, we
set the pos variable.
if (pos != -1) {
// if we found a new hiscore
if ((dlg = DialogNewSimple(DLGWIDTH,DLGHEIGHT)) == H_NULL) {
DlgMessage("Memory Allocation Error","Not Enough Free Memory!",BT_OK,BT_NONE);
} else {
DialogAddTitle(dlg,"New Hiscore!",BT_OK,BT_NONE);
DialogAddRequest(dlg,5,25,"Your Name:",0,9,11);
sprintf(str,"You earned the #%hd hiscore position!",pos+1);
DialogAddText(dlg,5,15,str);
do {
// truncate name variable
name[0] = 0;
} while (DialogDo(dlg,CENTER,CENTER,name,NULL) != KEY_ENTER);
// free the dialog box memory
HeapFree(dlg);
So, assuming we have a hiscore, we enter here. We built a custom dialog
box to ask the player for their name.
// move the hiscore list down
if (pos < (MAX_HISCORES - 1)) {
for (loop = (MAX_HISCORES - 1); loop > pos; loop--) {
scores[loop].score = scores[loop - 1].score;
scores[loop].name[0] = 0;
strcpy(scores[loop].name,scores[loop - 1].name);
}
}
This here is a very critical piece of code. If we have a hiscore that is
better than the lowest hiscore, then we need to move the other entries on the
list down.
So, if pos (our hiscore) is < (better than) the lowest hiscore, then
loop through the hiscores starting at the bottom and going until we
reach the hiscore position we beat. For each of these scores, move their
position in the array one down.
That piece of code doesn't look complex, but it's deceitful like that.
// fill in the new hiscore
scores[pos].score = newscore;
scores[pos].name[0] = 0;
strcpy(scores[pos].name,name);
if (!saveHiScores(scores)) {
DlgMessage(error,"Unable to save HiScore Board",BT_OK,BT_NONE);
}
Finally, we put our hiscore on the table and save the new hiscore table
to file.
// saveHiScores - saves the hiscore table to the hiscore file
// returns TRUE if the scores were saved to file, FALSE otherwise
short int saveHiScores(SCORE *hiscores) {
const char *filetype = "N68K";
short int success = TRUE;
FILE *f;
// delete any old scorefile before recreating
unlink(scorefile);
// open the hiscore file
if ((f = fopen(scorefile,"wb")) == NULL) {
success = FALSE;
} else {
// write the hiscore table to disk - check for I/O errors
if (fwrite(hiscores,sizeof(SCORE),MAX_HISCORES,f) != MAX_HISCORES) {
success = FALSE;
}
// write the file tag
fputc(0,f);
fputs(filetype,f);
fputc(0,f);
fputc(OTH_TAG,f);
fclose(f);
}
return success;
}
The saveHiScores() function shouldn't be any more difficult than
loadHiScores(). We delete the old score file first, then we open a new
score file. We write the array to the file, then write the file tag. If
we encounter I/O errors, we return FALSE.
// display the hiscore board
// clear the screen
ClrScr();
// draw the screen borders
drawBorder();
// draw the game logo
drawLogo();
FontSetSys(F_8x10);
DrawStr(25,35,"Hiscore Board",A_NORMAL);
FontSetSys(F_6x8);
for (loop = 0; loop < 5; loop++) {
printf_xy(20,50+loop*10,"#%hd %-9s %lu",loop+1,scores[loop].name,scores[loop].score);
}
ngetchx();
// redirect auto-interrupts 1 and 5
SetIntVec(AUTO_INT_1,DUMMY_HANDLER);
SetIntVec(AUTO_INT_5,DUMMY_HANDLER);
// wait for keypresses to dissipate
delay(KEYDELAY);
Finally, we are ready to display the hiscore table. Everything should be
simple except for the loop's printf statement. The format string is a
little complex. You can read all about the miracle of string formatting
tags in stdio.h's TIGCC docs under the printf function, but I will just
tell you what this one does for now.
The x and y should be simple enough, so here is what the format string
means.
#%hd (#number, in this case, #1, #2, #3, #4, and #5 for each hiscore
number)
%-9s (print the name with minimally 9 characters. If there are less than
9 characters, put spaces to the right of the string)
%lu - long unsigned decimal integer.
There are a horde of useful formatting tags you can use when you need
text data to look good.
Finally, we disable interrupts and wait for our keys to dissipate.