TIGCC Programming Lessons
Lesson Review 2 - Functions, Pointers, and Dynamic Memory
Step 2a - Programmatic Analysis
Since we have not done a review lesson in some time (at least 3 months
if you have been checking the site regularly), let me reiterate how I
handle review lessons. First, we do not cover every line of code. This
is because we are only reviewing concepts in a broader context. By now,
you should already understand the concepts that are being presented. If
you need more information about a specific concept, please consult the
lessons as they go into more detail about function usage, and the
background surrounding the use of the functions presented. So, with that
in mind, let's continue with the analysis.
#include "slider.h" // include the slider puzzle header file
Remember from lesson 4 that we will often create our own header files
for the purpose of containing specific information pertaining to our
program within a special file. These header files include our function
prototypes, and useful global variable declarations. In this case, we
put our sprite declarations inside the header file along with our
function prototypes.
Let's begin the function analysis with the _main() function, since it's
always a good place to start.
Step 2b - Analysis of the _main() function
void _main(void) {
int *x = NULL, *y = NULL, *puzzle = NULL;
const char *title = "DMA Error", *error = "Unable to allocate array space";
// allocate memory for the x position array
if ((x = (int *)calloc(16,sizeof(int))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
return;
}
// allocate memory for the y position array
if ((y = (int *)calloc(16,sizeof(int))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
// free the memory used by the x positions
free(x);
return;
}
// allocate memory fro the puzzle piece position array
if ((puzzle = (int *)calloc(16,sizeof(int))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
// free the memory used by the position arrays
free(x);
free(y);
return;
}
// seed the random number generator
randomize();
// initialize the puzzle
initPuzzle(puzzle);
// loop and play the game
while ((DlgMessage("Slider Puzzle 2.0","Press ENTER to play, ESC to quit.",
BT_OK,BT_NONE)) == KEY_ENTER) {
newGame(x,y,puzzle);
}
// free the memory allocated for the arrays
free(x);
free(y);
free(puzzle);
}
The _main() function is where program execution begins. Of course, we
already know that, so what does our _main() function do.
The _main() function is the place to take care of global program needs.
Data initialization, dynamic memory allocation (where possible), and any
other kind of initialization that needs to be done before the program
can start doing what it needs to do. Then we call helper functions to
handle the main program execution. After the program is finished
executing, we perform any necessary closing tasks and then exit the
program.
// allocate memory for the x position array
if ((x = (int *)calloc(16,sizeof(int))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
return;
}
// allocate memory for the y position array
if ((y = (int *)calloc(16,sizeof(int))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
// free the memory used by the x positions
free(x);
return;
}
// allocate memory fro the puzzle piece position array
if ((puzzle = (int *)calloc(16,sizeof(int))) == NULL) {
DlgMessage(title,error,BT_OK,BT_NONE);
// free the memory used by the position arrays
free(x);
free(y);
return;
}
In the first segment of the program, we allocate memory for the integer
arrays we will use during the course of the program. While we are doing
this, we also check to make sure all the arrays got the memory they
requested, and if any of the allocation requests fail, we send the user
an error message inside a dialog box. Remember that if we get one of the
arrays and then fail to get the next one, then we still need to free the
memory from the first one before we can exit the program. If you have
other questions about how this works, you should consult lesson 6.
// seed the random number generator
randomize();
// initialize the puzzle
initPuzzle(puzzle);
In line with our initialization phase, we need to make sure our random
number is properly seeded so we can get random numbers. Then we call the
initPuzzle() function to initialize our puzzle array. This function
simply puts all the initial values into the puzzle array so we know what
pieces are where. Since we are on that function anyway, let's go ahead
and analyze it. It's very simple so this will be quick.
void initPuzzle(int *puzzle) {
int loop;
// initialize the puzzle positions
for (loop = 0; loop < 15; loop++) {
puzzle[loop] = loop;
}
// make the last piece the blank piece
puzzle[15] = -1;
}
As we can see in this simple function, all the pieces of the puzzle
array just get initialized to the puzzle piece numbers. This is just a
handy way of keeping track of where the pieces are in the puzzle
relative to one another. Remember that since we only use 15 pieces, the
last piece will be made blank. The only other thing of note in this
function is that, since puzzle is an array, which means it is also a
pointer, the values can be changed inside the function and the changes
will still be there when we return from the function call. This is why
we use pointers. If you need more help with pointers and arrays, please
consult lesson 5.
// loop and play the game
while ((DlgMessage("Slider Puzzle 2.0","Press ENTER to play, ESC to quit.",
BT_OK,BT_NONE)) == KEY_ENTER) {
newGame(x,y,puzzle);
}
The next part of the _main() method is the heart of the program. This is
the basic loop that lets us play new games over and over, or exit the
program, as we choose. So, this should be pretty easy, since we know how
dialogs work (basically). Remember that dialog boxes return the key
press corresponding to the OK and CANCEL values we selected. So, our
while loop will ask the user to press ENTER to play, or press ESC to
quit. So long as the user presses ENTER at this dialog box, the
newGame() function will be called. Once the user stops pressing ENTER at
this dialog box, we stop looping through our new games.
// free the memory allocated for the arrays
free(x);
free(y);
free(puzzle);
If we reached this point and exited our game loop, then we need to
perform our closing tasks. These include memory deallocation for the
most part, but it is conceivable that we could have other tasks that are
necessary to perform before the program exits, although this is by far
the most common.
Well, that pretty well covers what we need to go over in the _main()
function, so let's take a look now at the newGame() function, where the
heart of the program is. This is where new games get created and play
out.
Step 2c - Analysis of the newGame() method
void newGame(int *x, int *y, int *puzzle) {
int key, loop;
int position = 0, currentPiece = 0, on = 1, done = 0, winner = 0;
// draw the background screen
drawScreen();
// draw a new puzzle
drawPuzzle(x,y,puzzle);
// set the first piece to be piece 0
currentPiece = puzzle[position];
// loop until the puzzle is completed, or until the
// user presses ESC
while (!done) {
// set the timer interval to 1/2 second
OSFreeTimer(USER_TIMER);
OSRegisterTimer(USER_TIMER, 10);
// wait for the timer to expire, or the user to press a key
while (!OSTimerExpired(USER_TIMER) && !kbhit());
if (kbhit()) {
// if the user pressed a key, grab it
key = ngetchx();
} else {
// otherwise, erase old keystrokes
key = 0;
}
// handle keystrokes
if (key == KEY_ESC) {
// ESC means they quit
done = 1;
} else if (key == KEY_ENTER) {
if (position+1 <= 16) {
// try to move the piece right
// if the right position is vacant
if (puzzle[position+1] == -1 && x[currentPiece] != (16*4-16)) {
// make sure that the old piece is still drawn
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// erase the piece
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// move the piece right 1 position (16 pixels)
x[currentPiece]+=16;
// reset the puzzle position holders
puzzle[position+1] = puzzle[position];
puzzle[position] = -1;
position++;
currentPiece = puzzle[position];
// draw the piece at the new position
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// don't try to move any other direction
continue;
}
}
if (position-1 >= 0) {
// try to move the piece left
// if the left position is vacant, and we haven't already moved
if (puzzle[position-1] == -1 && x[currentPiece] != 0) {
// make sure the piece is drawn, so it can be properly erased
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// erase the piece
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// move the piece 1 position left (16 pixels)
x[currentPiece]-=16;
// reset the puzzle position holders
puzzle[position-1] = puzzle[position];
puzzle[position] = -1;
position--;
currentPiece = puzzle[position];
// draw the piece at the new location
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// don't try to move any other direction
continue;
}
}
if (position-4 >= 0) {
// try to move the piece up
// if the up position is vacant, and we haven't already moved
if (puzzle[position-4] == -1 && y[currentPiece] != 0) {
// make sure the piece is drawn, so it can be properly erased
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// erase the piece
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// move the piece 1 position up (16 pixels)
y[currentPiece]-=16;
// reset the puzzle position holders
puzzle[position-4] = puzzle[position];
puzzle[position] = -1;
position-=4;
currentPiece = puzzle[position];
// redraw the piece at the new location
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// don't try to move any other direction
continue;
}
}
if (position+4 <= 15) {
// try to move the piece down
// if the down position is vacant, and we haven't already moved
if (puzzle[position+4] == -1 && y[currentPiece] != (16*4-16)) {
// make sure the piece is drawn, so it can be properly erased
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// erase the piece
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// move the piece one position down (16 pixels)
y[currentPiece]+=16;
// reset the puzzle position holders
puzzle[position+4] = puzzle[position];
puzzle[position] = -1;
position+=4;
currentPiece = puzzle[position];
// redraw the piece at the new location
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// no need to continue since there are no other directions...
}
}
} else if (key == KEY_LEFT) {
// make sure the piece is still drawn, so it's still there when we move
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// if there is a left position to move to
if (position > 0) {
// move left
position--;
// if we are at the empty slot, move left again
if (puzzle[position] == -1 && position > 0) {
position--;
// if we can't move left anymore, start at the end
} else if (puzzle[position] == -1 && position == 0) {
position = 15;
}
} else {
// set the position to the end
position = 15;
// if we found the empty slot, move left one
if (puzzle[position] == -1) {
position--;
}
}
// reset the puzzle position holder
currentPiece = puzzle[position];
} else if (key == KEY_RIGHT) {
// make sure the piece is drawn, so we can move away safely
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// if there is a position to move right to
if (position < 15) {
// move one position to the right
position++;
// if we hit the empty slot, move right again
if (puzzle[position] == -1 && position < 15) {
position++;
// if we hit the empty slot and the edge, start over
} else if (puzzle[position] == -1 && position == 15) {
position = 0;
}
// else, we need to start over at the beginning
} else {
// move to the beginning
position = 0;
// if we hit the empty slot, move right again
if (puzzle[position] == -1) {
position++;
}
}
// reset the puzzle position holder
currentPiece = puzzle[position];
} else if (key == KEY_UP) {
// make sure the piece is drawn, so we can move safely
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// if there is an up position to move to
if ((position - 4) >= 0) {
// move up one position
position-=4;
// if we hit the empty slot
if (puzzle[position] == -1) {
// if there is another up position, go there
if ((position - 4) >= 0) {
position-=4;
// otherwise, run to the bottom row
} else {
position = 16 - (4 - position);
}
}
// else, move to the bottom row
} else {
// set position at the bottom row
position = 16 - (4 - position);
// if we hit the empty slot, move up again
if (puzzle[position] == -1) {
position-=4;
}
}
// reset the puzzle position holder
currentPiece = puzzle[position];
} else if (key == KEY_DOWN) {
// make sure the piece is still drawn, so we can move safely
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// if there is a down position to move to
if ((position + 4) <= 15) {
// move down one position
position+=4;
// if we hit the empty slot
if (puzzle[position] == -1) {
// if there is another down position
if ((position + 4) <= 15) {
// move to the next down position
position+=4;
// otherwise, goto the top row
} else {
position%=4;
}
}
// else, move to the top row
} else {
// move to the top row
position%=4;
// if we hit the empty slot
if (puzzle[position] == -1) {
// move down one more time
position+=4;
}
}
// reset the puzzle position holder
currentPiece = puzzle[position];
}
// blink the current piece so we know which one it is
on = !on;
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
// set the winner to one, then try to disqualify it with
// the for loop. it's easier to reach a false conclusion than
// a true conclusion, so start true, and work towards false
winner = 1;
// test for winner
// loop through all game pieces, and see if their positions
// match their piece numbers. If they do, then the player won
// if we find any piece that is not right, we have no winner
// and we stop the for loop
for (loop = 0; loop < 15; loop++) {
if (y[loop] != ((loop / 4) * 16) || x[loop] != ((loop % 4) * 16)) {
winner = 0;
break;
}
}
// if we have a winner, make sure the piece is drawn and
// set done to yes
if (winner) {
// if we win, then we are done, obviously
done = 1;
// make sure the piece is drawn so it looks nice
if (!on) {
Sprite16(x[currentPiece], y[currentPiece], PIECE_HEIGHT,
pieces[currentPiece], LCD_MEM, SPRT_XOR);
on = 1;
}
// tell the player they won
DlgMessage("Winner", "You have completed the puzzle!", BT_OK, BT_NONE);
}
}
}
As you can clearly see, the newGame() function is the largest function
in the program. In a complex program, you would probably have more than
one function that is this large, or you would break the function down
into multiple parts. However, in a program as simple as the slider
puzzle, such a breakdown is unnecessary.
// draw the background screen
drawScreen();
The first thing we take care of in the newGame() function is to make a
call to the drawScreen() function. The drawScreen() draws the background
elements of the game and clears the screen for us. Let's take a look at
exactly what the drawScreen() function does.
inline void drawScreen(void) {
// clear the screen
ClrScr();
// draw the game control strings
DrawStr(0, 70, "Use Arrows to Move Pieces", A_NORMAL);
DrawStr(0, 80, "Press ENTER to Slide Piece", A_NORMAL);
DrawStr(0, 90, "Press ESC to Quit", A_NORMAL);
// draw the game information strings
DrawStr(67, 0, "Slider 2.0", A_NORMAL);
FontSetSys(F_4x6);
DrawStr(80, 10, "Copyright (C) 2001", A_NORMAL);
DrawStr(80, 18, "John David Ratliff", A_NORMAL);
DrawStr(67, 30, "Created by Techno-Plaza", A_NORMAL);
DrawStr(75, 38, "for Lesson Review 2", A_NORMAL);
DrawStr(67, 50, "Learn more at our website", A_NORMAL);
DrawStr(70, 58, "www.technoplaza.net", A_NORMAL);
// reset font to normal size
FontSetSys(F_6x8);
}
We can see that the first unique thing about the drawScreen() function
is that it is inline. Remember from lesson 4 that inline functions are
not actually separated from the places they are called, but all the code
from the function is just added in place of the function call. This
means for short functions, or functions that do not get called often,
inline functions are faster and more efficient than regular functions.
Although there is a slight advantage to declaring this function inline,
there is no reason why it couldn't be a normal function other than the
fact that I wanted to recap inline functions in this review lesson. If
you remember the first slider puzzle program, you will see that these
strings are just the strings we had displayed in the first program, with
some small differences in version number, copyright date, and which
review it was created for. However, this function is very simple, so it
doesn't really need much explanation.
// draw a new puzzle
drawPuzzle(x,y,puzzle);
Having drawn the background screen, we now need to draw our puzzle. This
is the purpose of the drawPuzzle() function, as I'm sure you already
surmised. We pass it three parameters, the x position array, the y
position array, and the puzzle array. This lets us fill up the arrays
with the correct values for the new puzzle. In addition to drawing the
puzzle, the drawPuzzle() function creates a new puzzle before drawing,
so every call to drawPuzzle() will draw a different puzzle. Let's take a
short look at the drawPuzzle() function.
void drawPuzzle(int *x, int *y, int *puzzle) {
int xLoop, yLoop, piece = 0;
// randomize the puzzle before drawing
randomizePuzzle(puzzle);
// loop through the rows of the puzzle to draw them
for (yLoop = 0; yLoop < (16 * 4); yLoop += 16) {
for (xLoop = 0; xLoop < (16 * 4); xLoop += 16) {
// only attempt to draw valid pieces
if (puzzle[piece] != -1) {
Sprite16(xLoop, yLoop, PIECE_HEIGHT,
pieces[puzzle[piece]], LCD_MEM, SPRT_XOR);
// mark the position of the piece
x[puzzle[piece]] = xLoop;
y[puzzle[piece]] = yLoop;
}
piece++;
}
}
}
Okay, this function is not too complex, but there are some pieces worth
exploring. You'll notice that the first thing the function does is call
the randomizePuzzle() function and pass it the puzzle array. This is how
we ensure each puzzle drawn will be new, and this seemed the logical
place to put the randomizePuzzle() function since we would never draw a
puzzle before randomizing it. We can examine the randomizePuzzle()
function here:
void randomizePuzzle(int *puzzle) {
int loop, randNum, temp;
// randomize the puzzle pieces
for (loop = 0; loop < 16; loop++) {
// select a random puzzle piece
randNum = random(16);
// replace the old piece with the new piece
temp = puzzle[loop];
puzzle[loop] = puzzle[randNum];
puzzle[randNum] = temp;
}
}
As you can see from the randomizePuzzle() function, this is the same
thing we did in the first slider puzzle version. Remember that we
already seeded our random number generator in the _main() method, and we
only need to do that once per program. The method simply loops through
all the pieces in the array, selects a random number for each piece, and
swaps the piece at the random position with the piece we are currently
at in the loop. All in all, we haven't done anything complicated.
The rest of the drawPuzzle() function is just the same as it was from
the first slider puzzle version, only now it has been encapsulated
within a function, which is the proper way to handle it.
The last part I think we need to examine in the newGame() function is
embedded in the big while loop.
// don't try to move any other direction
continue;
You can see this on all of the direction movements when the user presses
enter and we attempt to slide the piece. Remember that in the first
slider puzzle example, we used a variable named 'moved' to see if we had
already moved the piece in some direction, implying there is no more
need to move it again. While there is nothing wrong with that method, it
uses extra variable space, and it's really not a good idea to waste
memory.
You remember from lesson 4 when we used the switch statement? Remember
that every time we had a case block, we had to end it with a break
keyword. This told the computer where to stop executing code for that
block. Well, we can use break keywords within loops, too. It works the
same way, when we encounter a break keyword, we exit the body of the
loop. Well, that is nice, but we do not want to exit out of the body of
the loop, we want to skip to the next loop iteration (return to the
beginning of the loop). So, instead of the break keyword, there is a
special keyword called the continue keyword which will allow us to do
just that. The continue keyword skips to the closing brace } of the
while loop (or for loop, or whatever kind of loop we have), and then we
start the loop again at the condition statement. Since we don't use a
variable here, it's more efficient, because we only have to do a jump,
and a single jump is better than wading through numerous conditional
statements checking conditions that we know will never be true. Since we
already moved the piece in one direction, there is no need to do
anything else in this iteration of the loop. The logical thing is to
skip to the end, and the continue keyword is our means of achieving this
goal.
Continue with the Analysis in Part III
|