At this point, you may be feeling a little afraid, or maybe excited.
Either way, it's a big program, but we have the skills to tackle it. So,
don't be fooled by its size. There are programs much larger and more
complex. But, it's a good jumping off point for programming practice, so
let's take a look.
We will start with the _main method. We will work our way around the
program as the program would be executed, covering functions as they are
used. I have tried to organize the source files so that functions that
depend on other functions come later, and so that they are in groups by
category. You will see what I mean soon enough.
short int font = FontGetSys(), speed = AVERAGE, difficulty = NORMAL;
// Save the auto-interrupts
autoint1 = GetIntVec(AUTO_INT_1);
autoint5 = GetIntVec(AUTO_INT_5);
// create the hiscore file, if it doesn't exist
if (needScoreFile()) {
createHiScoreTable();
}
The first part of the main method is simple, declare some local
variables. The FontGetSys() function returns the current font that the
system is using. The TI-89 defaults to medium font, while the
TI-92+/V200 defaults to large font. We want to make sure all our text
defaults to medium to be compatible on both calculators.
I am not sure if we need to restore the font back to what it was
originally, but it can't hurt, so we may as well be safe. So, we will
save the system font size in the font variable, and restore it before we
exit the program.
The default speed and difficulty level is AVERAGE and NORMAL. These
constants are defined in the nibbles.h header.
We will be using low level keyboard reading, so we will redirect
auto-interrupts 1 and 5 soon. We save them in the global variables
autoint1 and autoint5 rather than in a local variable because there is
another place in the program where we will need to turn them back on
temporarily.
Next, we make sure the hiscore file exists. If it doesn't, we create it
using the createHiScoreTable() function. Let's take a look at those
functions from hiscore.c.
// createHiScoreTable - create a blank hiscore table and write it to file
// return the result of the disk save
short int createHiScoreTable(void) {
SCORE board[MAX_HISCORES];
// set all the entries in the table to blank
memset(board,0,sizeof(SCORE) * MAX_HISCORES);
return (saveHiScores(board));
}
// needScoreFile - checks whether a hiscore file already exists
// returns TRUE if there is no hiscore file, FALSE otherwise
short int needScoreFile(void) {
FILE *f = fopen(scorefile,"rb");
short int needed = FALSE;
// if we cannot open the file, we must need to create it
if (f == NULL) {
needed = TRUE;
} else {
fclose(f);
}
return needed;
}
These two functions are rather simple. The needScoreFile checks to see
if a hiscore file already exists. It does this by attempting to open the
file. We can't open a file that doesn't exist, so assume we need to
create one if we can't open it.
The createHiScoreTable() function simply creates a blank hiscore table
and writes it to the file. We will come back to the saveHiScores()
function later when we examine the rest of the hiscore functionality.
Until then, let's get back to the _main method.
// Make sure there are no keystrokes left in the buffer
GKeyFlush();
// setup medium size font
FontSetSys(F_6x8);
// seed the random number generator
randomize();
Okay, here is some basic program initialization. First, since we will be
reading the keyboard ourselves to make things faster, flush the keyboard
buffer to make sure there are no waiting keys. This will ensure we don't
read the enter key twice when we start the program.
Next, we set the font size to medium using the FontSetSys() function
from graph.h.
Finally, we will need random numbers so we can place the apples the
snake will eat. To do this, we will need to seed the random number
generator. We do this using the randomize() function from stdlib.h.
// redirect auto-interrupts 1 and 5
SetIntVec(AUTO_INT_1,DUMMY_HANDLER);
SetIntVec(AUTO_INT_5,DUMMY_HANDLER);
As mentioned above, we will be using low level keyboard reading. This
means we need to redirect auto-interrupts 1 and 5 so they don't
interfere with this.
// keep playing until doIntro returns FALSE (exit)
while (doIntro(&speed,&difficulty)) {
play(speed,difficulty);
}
// restore auto-interrupts
SetIntVec(AUTO_INT_1,autoint1);
SetIntVec(AUTO_INT_5,autoint5);
// restore the standard font
FontSetSys(font);
Here is the main loop that lets us play the game. We call the doIntro
function. Every time it returns a true value, we call the play()
function, which actually is the main game loop. When doIntro returns a
false value, our while loop exits and we do program cleanup.
The only thing we need to take care of before we are done is to
re-enable the interrupts (otherwise the calc will stop working because
the AMS won't be able to read the keyboard). Lastly, we restore the font
we saved before starting the program.
That should all be simple enough, so let's take a look at that doIntro()
routine.
// doIntro - display the game intro and wait for menu selections
// returns FALSE if user selects exit, TRUE otherwise
short int doIntro(short int *gamespeed, short int *gamedifficulty) {
short int selector = MENUSTART, done = FALSE, key, options = FALSE, selection = 0;
short int speed = *gamespeed, difficulty = *gamedifficulty;
// clear the screen
ClrScr();
// draw the intro screen
drawBorder();
drawLogo();
drawMenu();
drawCopyright();
drawSelector(selector);
Most of this code is straightforward, but we should take a look at what
the variables are used for. When you write your own code, you'll know
what your variables mean, but if you share code, it's a good idea to
make them 1) well named based on function, and 2) well documented in
comments so it becomes obvious what they are doing.
Many C programmers use variable names like s, i, j, and so on. Unless
you have a very tiny loop, or a very unimportant string, give your
variable names real names. It just makes your code easier to read. This
is especially helpful when you come back to a codebase, oh say, 5 or 6
years after you wrote it. (check the copyright dates)
So, taking a look at our variables, here is their meaning. The selector
is the position of the menu selector bar (the y coordinate offset from
MENUSTART to MENUEND - those constants are defined in nibbles.h). The
done variable should be obvious, it controls our menu loop. The key
variable should also be clear, it will hold our keycodes.
The options variable tells us, TRUE or FALSE, whether we are in the
options menu. There are two menus on the main screen, the main menu
where we can start the game, exit the game, or go to the options menu,
and the options menu which allows us to select speed and difficulty
level. If we are in the options menu, we need to interpret the user's
keypresses differently, because we are in a different menu. We start in
the main menu, so this is FALSE at first.
The selection var is used to track the selection we made, 0 at the top
going down the menu. The speed and difficulty options are quick copies
of the speed and difficulty level sent to us by the call to doIntro().
Although it isn't really that critical to speed, it's faster to access a
local variable than dereference a pointer. It also makes the code look
cleaner by getting rid of &'s and *'s.
Okay, that takes care of our variables, so let's look at the drawing
intro. We clear the screen and call 5 draw methods from gfx.c. All our
drawing functions are in gfx.c. They are simple functions, so let's take
a look at them here.
// drawBorder - draws four lines making a box around the screen
inline void drawBorder(void) {
DrawLine(X1,Y1,X2,Y2,A_NORMAL);
DrawLine(X3,Y3,X4,Y4,A_NORMAL);
DrawLine(X5,Y5,X6,Y6,A_NORMAL);
DrawLine(X7,Y7,X8,Y8,A_NORMAL);
}
// drawCopyright - prints the copyright notice at the bottom of the screen
inline void drawCopyright(void) {
FontSetSys(F_4x6);
DrawStr(COPYX,COPYY,(char *)intro[COPYRIGHT],A_XOR);
DrawStr(URLX,URLY,(char *)intro[URL],A_XOR);
FontSetSys(F_6x8);
}
These two functions should be simple enough. The drawBorder() function
draws 4 lines around the edges of the screen. The constants are defined
in nibbles.h. As for drawCopyright(), it simply draws the copyright
notice in small font at the bottom of the screen. Again, these constants
are defined in nibbles.h.
// drawLogo - draws the Nibbles 68k logo across the top of the screen
void drawLogo(void) {
short int loop, offset = 0;
// draw the logo pieces
for (loop = 0; loop < (LOGOTILES * LOGOWIDTH); loop+=LOGOWIDTH) {
Sprite32(loop,0,LOGOHEIGHT,logo+offset,LCD_MEM,SPRT_XOR);
offset += LOGOHEIGHT;
}
}
// drawSelector - draws the menu selector bar over the menu options
void drawSelector(short int y) {
short int loop, offset = 0;
// draw the selector pieces
for (loop = 0; loop < (SELECTORTILES * SELECTORWIDTH); loop+=SELECTORWIDTH) {
Sprite32(loop,y,SELECTORHEIGHT,selector+offset,LCD_MEM,SPRT_XOR);
offset += SELECTORHEIGHT;
}
}
These functions are very similar, but are using some more advanced
sprite graphic techniques. When we want to draw an image that is larger
(wider) than 32 pixels, what do we do? There are only three sprite
functions in the TIGCC library, one for 8, 16, and 32 pixel sprites. The
key is to define your images by section. 32 pixels at a time, or 16, or
8, as you need, to define your image. Both our logo and the selector are
defined in a space of 160 horizontal pixels. So, to draw them, we simply
define a sprite for each part of our image. Then, take the sprite data,
and append one segment onto the next. Let's take a look at the selector
sprite so you can see what I mean. It's defined in gfx.h.
// menu selector block
static unsigned long int selector[40] = {
0x03ffffff,0x03ffffff,0x03ffffff,0x03ffffff,0x03ffffff,0x03ffffff,0x03ffffff,0x03ffffff,
0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,
0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,
0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,0xffffffff,
0xffffffc0,0xffffffc0,0xffffffc0,0xffffffc0,0xffffffc0,0xffffffc0,0xffffffc0,0xffffffc0
};
The selector is 8 pixels tall, the same height as our medium text font
(6x8). So, the first 32 pixels of height are defined at the top row. So,
the leftmost part of the selector looks like this:
00000011111111111111111111111111 or
However, you'll have to imagine that it is actually much shorter and
less tall. Now, we keep going down the line. The next 32 pixels are all
black, so we continue with a full black line for 3 more lines. Then, we
keep the last line's last 6 pixels blank.
So, if you stack them all together, we get something that looks like
this:
Simple, no?
Okay, now, to give the sprite data to the SpriteXX functions, we need to
tell it where the sprite is. Normally, we just tell it the sprite name.
But in this case, that's not enough. We are going to reference different
parts of the sprite. So, we need to use pointer arithmetic to find the
correct sprite offset. Remember, any place inside an array is also the
start of another array, or more simply, every address is a pointer to
something. In this case, we add the height of our sprite to the base
sprite address to get the offset of the next piece.
Knowing that we are going to begin at offset 0, and go up to offset
height * number of pieces (0-4 in our case), we simply add height to our
offset each time.
// drawMenu - displays the game menu
void drawMenu(void) {
short int loop;
// draw the three menu selections
for (loop = START; loop <= QUIT; loop++) {
centerText(intro[loop],MENUSTART+(loop*MENUSTEP));
}
}
Okay, our last function is the drawMenu() function. It draws the text
part of our menus. To make it look better, we center the text on the
screen. We can draw centered text by using the centerText() function.
The string arrays are defined in gfx.h, which the constants are defined
in nibbles.h.
// centerText - draws a string at the horizontal center of line y
inline void centerText(const char *str, short int y) {
short int len = (short int)strlen(str), x = ((160 - (len * 6)) / 2);
DrawStr(x,y,(char *)str,A_XOR);
}
The centerText() function is rather simple. We calculate the strings
horizontal center in the screen, based on the medium size font. Remember
that the medium font is 6x8 pixels, and it is monospaced (i.e., all the
letters are the same size, not like the font you are most likely reading
this lesson in. Notice how the m takes more space than the i. However,
if we use a different font, like Courier all
the letters start taking up the same amount of space. m and i now take
up the same space. See the difference?)
Because the font is monospaced, we can calculate how much horizontal
space a string will take up based on its length. So, take the the LCD
width (160) - the length of the string times the size of each letter (6
pixels), then divide by 2 and you have the x coordinate to draw the
string at.
To make our strings easy to draw/erase, we draw them using XOR logic.
This means we can draw and erase the menu using the same function.
Okay, let's get back to the doIntro() function.
while (!done) {
// wait for keypress
while ((key = getKey()) == 0);
Inside our while loop, we wait for key presses. Since we are reading the
keys ourselves, we use a method called getKey() which is defined in
gameplay.c. It returns a constant for each of the keys we want to check
in the game.
// getKey - checks for keypresses
// returns - non-standard keycode of the key pressed, or 0 if no key was pressed
inline short int getKey(void) {
if (_rowread(ARROWROW) & UPKEY) {
return KUP;
} else if (_rowread(ARROWROW) & DOWNKEY) {
return KDOWN;
} else if (_rowread(ARROWROW) & LEFTKEY) {
return KLEFT;
} else if (_rowread(ARROWROW) & RIGHTKEY) {
return KRIGHT;
} else if (_rowread(ESCROW) & ESCKEY) {
return KESC;
} else if (_rowread(ENTERROW) & ENTERKEY) {
return KENTER;
} else if (_rowread(CHEATROW) & CHEATKEY) {
return KCHEAT;
}
return 0;
}
The getKey() function simply reads the keyboard rows. If we find a key
we want, return our custom key code. The key constants are defined in
nibbles.h, but we can stick to using the key constant names. There are
only 6 keys we check for, the arrows, the ESC key (for quitting), the
ENTER key, and the cheat key which gives us infinite lives if we are
playing. If none of these keys were pressed, we simply return 0.
if (key == KUP) {
// move up the menu
drawSelector(selector);
if (selector == MENUSTART) {
selector = MENUEND;
} else {
selector -= MENUSTEP;
}
drawSelector(selector);
} else if (key == KDOWN) {
// move down the menu
drawSelector(selector);
if (selector == MENUEND) {
selector = MENUSTART;
} else {
selector += MENUSTEP;
}
drawSelector(selector);
Here we check for up and down arrows which move around the menu. We
remove the selector (the selector is drawn using XOR), change the
selector position variable, and redraw at our new position. The function
wraps around the menu if we hit up at the top or down at the bottom.
} else if (key == KLEFT) {
if (options) {
// if we are in the options menu
selection = (selector - MENUSTART) / MENUSTEP;
// set the speed option
if (selection == START) {
// same as speed option -- option 1 == SPEED
drawSpeedOption(speed);
if (speed == VERYSLOW) {
speed = VERYFAST;
} else {
speed--;
}
drawSpeedOption(speed);
// set the difficulty option
} else if (selection == OPTIONS) {
// same as difficulty option -- option 2 == DIFFICULTY
drawDifficultyOption(difficulty);
if (difficulty == EASY) {
difficulty = HARD;
} else {
difficulty--;
}
drawDifficultyOption(difficulty);
}
}
The left key is only checked when we are in the options menu. The
options menu lets us select speed and difficulty settings using the left
and right arrow keys.
So, find the place on the menu based on the position of the selector.
Change the speed or difficulty options one position down the scale,
wrapping around to the top of the scale, if necessary. Then, we simply
redraw the speed or difficulty option text.
// drawSpeedOption - draws the speed setting (i.e. slow, fast, average)
void drawSpeedOption(short int speed) {
char speedStr[25];
// truncate to 0-length
speedStr[0] = 0;
// create the speed string
strcat(speedStr,intro[SPEED]);
strcat(speedStr,speeds[speed]);
// display the string
centerText((const char *)speedStr,MENUSTART);
}
// drawDifficultyOption - draws the difficulty setting (i.e. easy, hard, medium)
void drawDifficultyOption(short int difficulty) {
char difficultyStr[25];
// truncate to 0-length
difficultyStr[0] = 0;
// create the difficulty string
strcat(difficultyStr,intro[DIFFICULTY]);
strcat(difficultyStr,difficulties[difficulty]);
// display the string
centerText((const char *)difficultyStr,MENUSTART+MENUSTEP);
}
The drawSpeed and drawDifficultyOption() functions work pretty much the
same. We draw a string based on the speed, taking strings from the
intro, speeds, and difficulties string arrays. These arrays are defined
in gfx.h. We draw them using the centered text feature, too.
} else if (key == KENTER || key == KRIGHT) {
// select menu option
selection = (selector - MENUSTART) / MENUSTEP;
if (options) {
// if we're in the options menu
// exit the options menu
if (selection == QUIT) {
// close options menu
options = FALSE;
// switch the options and main menus
drawOptionsMenu(speed,difficulty);
drawMenu();
// reset the selector
drawSelector(selector);
selector = MENUSTART;
drawSelector(selector);
Okay, here we check for the right arrow or enter key. In our menu, they
function the same way. Check which selection we are at on the menu, and
then check to see if we are in the options menu. If so, we check to see
if the user wants to exit the options menu. If they do, we erase the
options menu, and redraw the main menu. We can see the drawOptionsMenu()
is pretty simple.
// drawOptionsMenu - draws the options menu items
inline void drawOptionsMenu(short int speed, short int difficulty) {
drawSpeedOption(speed);
drawDifficultyOption(difficulty);
centerText(intro[QUITOPTIONS],MENUSTART+(2*MENUSTEP));
}
As you can see, to draw the options menu, we simply call the drawSpeed
and drawDifficultyOption functions, and add a quit option. Since these
are all drawn using XOR logic, we can draw and erase with the same
function.
So, then we redraw the original menu, and reset the selector back to the
top.
} else if (selection == START) {
// same as speed option -- option 1 == SPEED
drawSpeedOption(speed);
if (speed == VERYFAST) {
speed = VERYSLOW;
} else {
speed++;
}
drawSpeedOption(speed);
} else if (selection == OPTIONS) {
// same as difficulty option -- option 2 == DIFFICULTY
drawDifficultyOption(difficulty);
if (difficulty == HARD) {
difficulty = EASY;
} else {
difficulty++;
}
drawDifficultyOption(difficulty);
}
This part of the if-then-else clause is the same as the left arrow in
the options menu, we just reverse the direction.
} else {
// if we chose to start or exit, end the loop
if (selection == START || selection == QUIT) {
done = TRUE;
} else {
// enter the options menu
options = TRUE;
// switch the main and options menus
drawMenu();
drawOptionsMenu(speed,difficulty);
// reset the selector
drawSelector(selector);
selector = MENUSTART;
drawSelector(selector);
}
}
If we aren't in the options menu, there are only three things to do. We
either enter the options menu, or we start or stop the game. So, if we
selected start or stop, the menu loop is over.
Otherwise, we need to enter the options menu, so we erase the menu, draw
the options menu, and reset the selector. Simple, no?
} else if (key == KESC) {
// exit the options menu
if (options) {
// close options menu
options = FALSE;
// switch the options and main menus
drawOptionsMenu(speed,difficulty);
drawMenu();
// reset the selector
drawSelector(selector);
selector = MENUSTART;
drawSelector(selector);
} else {
selection = QUIT;
done = TRUE;
}
}
The last key to check on is the ESC key. We use the ESC key to exit from
the options menu, or to quit the game if we aren't in the options menu.
Based on our other menu options, this should be fairly straightforward.
// wait for keypress to dissipate
delay(KEYDELAY);
This is an important line in many places of our game. Since we have
disabled interrupts, the program is very very fast. This is sometimes a
good thing, but it can be a bad thing. We don't want to be so fast that
we read our key presses twice, or worse dozens of times. Try commenting
out that line and see what happens to the menu.
// set the game options
*gamespeed = speed;
*gamedifficulty = difficulty;
return (selection == START) ? TRUE : FALSE;
At the end of our method, we set our original gamespeed and
gamedifficulty levels back to what we chose in our options menu. Then,
we return TRUE or FALSE, depending upon whether the menu selection was
START.
Now, based on our return value, we will either start a new game by
calling the play() function, or exit the program. So let's take a look
at the play function in gameplay.c.
// play - the main game loop
void play(short int speed, short int difficulty) {
SNAKE snake, *snakeptr = &snake;
short int totalApples, levels, speedDelay, done = FALSE, applePoints, level = 0, apples;
short int key = 0, cheater = FALSE, begin, loop, keyDelay, ending = NONE;
unsigned long int score = 0;
POSITION currentApple;
// difficulty level values
const short int startLives[3] = {6, 4, 2};
const short int startApples[3] = {5, 8, 12};
const short int startLevels[3] = {5, 8, 10};
// speed delay
const short int startDelay[10] = {750,650,600,550,425,375,325,250,175,100};
// setup the game options
snake.lives = startLives[difficulty];
totalApples = startApples[difficulty];
levels = startLevels[difficulty];
speedDelay = startDelay[speed];
The play function is the main game loop. It's job is to control all the
functioning of the game while the player still wants to play, and still
has extra snake lives. When the play function ends, the game is over.
This is probably the most important function in the game. So, let's look
over the initialization. There are many local variables. We have a SNAKE
structure which represents the snake's characteristics. The SNAKE
structure is defined in nibbles.h. Let's take a quick look.
typedef struct {
unsigned short int x:8, y:8;
} POSITION;
typedef struct {
POSITION location[28];
signed short int lives;
unsigned short int dead:1, direction:2, length:13;
} SNAKE;
The SNAKE structure is built on the number of lives the snake has, if
it's dead, the direction its moving, the length, and 28 POSITION
structures that tell us where the snake's pieces are at.
Most of the variables should be self-explanatory. The constant integer
arrays are used for difficulty and speed settings. There are 10 speeds,
and 3 difficulty levels. How many starting lives you get, the number of
levels you must win, and the number of apples on each level is based on
the difficulty setting. The speed just changes how fast the snake moves.
Those delay numbers and the delay routine aren't perfect yet, but it's a
hard thing to control.
I'm going to point out real quick that the delay() method is stupid.
It's a horrible way of timing the game as you will notice from the jerky
gameplay. I wish I had thought of doing an event-driven timer model when
I wrote this, but I don't have time to rewrite it now, and it would big
a big rewrite to fix it properly.