Techno-Plaza
Site Navigation [ News | Our Software | Calculators | Programming | Assembly | Downloads | Links | Cool Graphs | Feedback ]
 Main
   Site News

   Our Software

   Legal Information

   Credits

 Calculators
   Information

   C Programming

     Introduction to C

     Keyboard Input

     Graphics Intro

     Slider Puzzle 1

     Functions

     Pointers

     Dynamic Memory

     Slider Puzzle 2

     Structures

     Bit Manipulation

     Advanced Pointers

     File I/O

     Graduate Review

       Part I

       Part II

       Part III

   Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

C Programming Lessons

TIGCC Programming Lessons

Review 3: Introduction to Graduate Programming

Step 2a - Program Analysis

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.

Continue the Analysis in Part III

 

Copyright © 1998-2007 Techno-Plaza
All Rights Reserved Unless Otherwise Noted

Get Firefox!    Valid HTML 4.01!    Made with jEdit