Because this is a review lesson, we will not touch upon every aspect of
the program, only the key concepts. It will be up to you to make sure
you know what the program does by applying the concepts you have learned
in previous lessons with what is presented in the program.
The first important concept is the sprite declarations. There are 15
sprites representing the 15 puzzle pieces. In addition, we have a
pointer array which contains pointers to all the sprites, so we can use
them all from one variable.
The pieces are declared before the start of the _main() function.
// define the puzzle sprites
static unsigned short int piece1[] =
{0xFFFF,0xC003,0x8001,0x8181,0x8781,0x8181,0x8181,0x8181,
0x8181,0x8181,0x8181,0x8181,0x8181,0x8001,0xC003,0xFFFF};
static unsigned short int piece2[] =
{0xFFFF,0xC003,0x8001,0x83C1,0x8661,0x8661,0x8061,0x80C1,
0x8181,0x8301,0x8601,0x8601,0x87E1,0x8001,0xC003,0xFFFF};
static unsigned short int piece3[] =
{0xFFFF,0xC003,0x8001,0x83C1,0x8661,0x8061,0x8061,0x81C1,
0x8061,0x8061,0x8061,0x8661,0x83C1,0x8001,0xC003,0xFFFF};
static unsigned short int piece4[] =
{0xFFFF,0xC003,0x8001,0x80C1,0x81C1,0x83C1,0x83C1,0x86C1,
0x86C1,0x8CC1,0x8FF1,0x80C1,0x80C1,0x8001,0xC003,0xFFFF};
static unsigned short int piece5[] =
{0xFFFF,0xC003,0x8001,0x87E1,0x8601,0x8601,0x8601,0x87C1,
0x8661,0x8061,0x8061,0x8661,0x83C1,0x8001,0xC003,0xFFFF};
static unsigned short int piece6[] =
{0xFFFF,0xC003,0x8001,0x83C1,0x8661,0x8601,0x8601,0x87C1,
0x8661,0x8661,0x8661,0x8661,0x83C1,0x8001,0xC003,0xFFFF};
static unsigned short int piece7[] =
{0xFFFF,0xC003,0x8001,0x87E1,0x8061,0x80C1,0x80C1,0x8181,
0x8181,0x8181,0x8301,0x8301,0x8301,0x8001,0xC003,0xFFFF};
static unsigned short int piece8[] =
{0xFFFF,0xC003,0x8001,0x83C1,0x8661,0x8661,0x8661,0x83C1,
0x8661,0x8661,0x8661,0x8661,0x83C1,0x8001,0xC003,0xFFFF};
static unsigned short int piece9[] =
{0xFFFF,0xC003,0x8001,0x83C1,0x8661,0x8661,0x8661,0x8661,
0x83E1,0x8061,0x8061,0x8661,0x83C1,0x8001,0xC003,0xFFFF};
static unsigned short int piece10[] =
{0xFFFF,0xC003,0x8001,0x8C79,0xBCCD,0x8CCD,0x8CCD,0x8CCD,
0x8CCD,0x8CCD,0x8CCD,0x8CCD,0x8C79,0x8001,0xC003,0xFFFF};
static unsigned short int piece11[] =
{0xFFFF,0xC003,0x8001,0x8C31,0xBCF1,0x8C31,0x8C31,0x8C31,
0x8C31,0x8C31,0x8C31,0x8C31,0x8C31,0x8001,0xC003,0xFFFF};
static unsigned short int piece12[] =
{0xFFFF,0xC003,0x8001,0x8C79,0xBCCD,0x8CCD,0x8C0D,0x8C19,
0x8C31,0x8C61,0x8CC1,0x8CC1,0x8CFD,0x8001,0xC003,0xFFFF};
static unsigned short int piece13[] =
{0xFFFF,0xC003,0x8001,0x8CF1,0xBD99,0x8C19,0x8C19,0x8C71,
0x8C19,0x8C19,0x8C19,0x8D99,0x8CF1,0x8001,0xC003,0xFFFF};
static unsigned short int piece14[] =
{0xFFFF,0xC003,0x8001,0x8C0D,0xBC1D,0x8C3D,0x8C3D,0x8C6D,
0x8C6D,0x8CCD,0x8CFD,0x8C0D,0x8C0D,0x8001,0xC003,0xFFFF};
static unsigned short int piece15[] =
{0xFFFF,0xC003,0x8001,0x8CFD,0xBCC1,0x8CC1,0x8CC1,0x8CF9,
0x8CCD,0x8C0D,0x8C0D,0x8CCD,0x8C79,0x8001,0xC003,0xFFFF};
As we learned in the third lesson, 16-bit sprites are declared as
unsigned short integer arrays[]. Each of the numbers we defined
within represents one row of the sprite, and if you convert the
hexadecimal numbers to binary, and stack them on top of each other, you
can see what the sprite looks like. Here is what the 15th piece looks
like in binary, with, and without the zeros in place:
1111111111111111 | 1111111111111111
1100000000000011 | 11 11
1000000000000001 | 1 1
1000110011111101 | 1 11 111111 1
1011110011000001 | 1 1111 11 1
1000110011000001 | 1 11 11 1
1000110011000001 | 1 11 11 1
1000110011111001 | 1 11 11111 1
1000110011001101 | 1 11 11 11 1
1000110000001101 | 1 11 11 1
1000110000001101 | 1 11 11 1
1000110011001101 | 1 11 11 11 1
1000110001111001 | 1 11 1111 1
1000000000000001 | 1 1
1100000000000011 | 11 11
1111111111111111 | 1111111111111111
In case you were wondering about the static keyword, no, I don't know
why I did that. It is completely unnecessary and has no real importance
here. Just ignore that part. You can even delete it if you want.
It looks like the piece is a bit stretched when we view it in HTML, but
it will be a perfect square on the TI-89, and you can get the feel for
what the sprites look like in this fashion.
If you were curious, I used Microsoft Visual Developer Studio's Visual
C++ Resource Editor to create the sprites. They have a simple 16x16 icon
editor which will let you add text on an icon, which helped me place the
letters pixel by pixel. Then I just took the icon image pixel data and
converted it by hand to hex. To do this part of the lesson, I simply
converted the hex values back to binary, by hand. If you recall the
system used in lesson 3 for binary to hex conversions, it's pretty
simple. The trick is getting your sprites to look right. I'm no artist,
so I used Microsoft tools to help out. Hopefully I can find a free
alternative which I will share soon. I haven't found a good one yet.
Now that we have the sprite data completed, we created a static pointer
to all the sprites contained within one array. This was done so we could
loop through the drawings with a for loop instead of having to use the
sprite functions for 15 sprites. It would have also been very difficult
to randomize the puzzle pieces without an array to sort them in. Let's
take a look at the pointer array:
// a pointer to the puzzle pieces to access them via an array
static unsigned short int *pieces[] =
{piece1, piece2, piece3, piece4, piece5,
piece6, piece7, piece8, piece9, piece10,
piece11, piece12, piece13, piece14, piece15};
This pointer array simply allows us access to all the sprites in this
one array. So instead of using piece1, piece2, etc., we can use
*pieces[1]. This is more efficient because we can use a loop variable to
go through all the pieces, as we will see later on. We also have to make
sure to use *pieces[position] instead of pieces[position], because we do
not want a pointer to a variable, we want an actual variable. We have
not covered pointer dereferencing in any detail, so for now, just
remember this. The asterisk '*' means to convert a pointer to a
variable. The only reason we need pointers is so we can keep track of a
single variable, and not make multiple copies of it. So we can use
pointers to access variables and change them. However in this case, we
are only accessing the variables, not changing them. But we must still
use a pointer array because you must use pointers when you want to
access an array as a whole. If we just wanted one part of the array, we
could use a standard array, but since we need to maintain the position
of the entire array, we must use pointers. I know that was a bit
confusing, but just glaze over it for now. It will become clear soon
enough.
The next thing to examine is the incredible number of variables in the
_main() method. Here is one of the many places where this program is
poorly written, but is done so as to stay within the confines of the
knowledge you should have from lessons 1-3. We will explain why this is
not a good idea in a future lesson, for now, it is enough to know that
this is not a good idea, and that we will provide an alternative in the
future. Here is what the variable declarations look like:
int xLoop, yLoop, piece = 0;
int loop, temp, randNum;
int puzzle[16] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,-1};
int x1 = 0, x2 = 0, x3 = 0, x4 = 0, x5 = 0, x6 =0, x7 = 0, x8 = 0;
int x9 = 0, x10 = 0, x11 = 0, x12 = 0, x13 = 0, x14 = 0, x15 = 0;
int y1 = 0, y2 = 0, y3 = 0, y4 = 0, y5 = 0, y6 =0, y7 = 0, y8 = 0;
int y9 = 0, y10 = 0, y11 = 0, y12 = 0, y13 = 0, y14 = 0, y15=0;
int *x[] = {&x1,&x2,&x3,&x4,&x5,&x6,&x7,&x8,&x9,&x10,&x11,&x12,&x13,&x14,&x15};
int *y[] = {&y1,&y2,&y3,&y4,&y5,&y6,&y7,&y8,&y9,&y10,&y11,&y12,&y13,&y14,&y15};
int position, currentPiece = 0, done = 0, key = 0, on = 1, moved = 0;
int winner = 0;
Wow! By my count, there are 46 variables there (counting the arrays as a
single variable). Pointers take up 4 bytes of space and integers take up
2 bytes. Later, we will learn about techniques that will cut down on
these variable declarations, you will see how we can get rid of the x
and y pointers all together.
For now, let's go over the basics. You know what most (if not all of
these do). The ones that may look a little ominous are the puzzle
array[] and the *x[] and *y[] arrays. Let's start with the puzzle array.
int puzzle[16] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,-1};
This array is responsible for storing the puzzle pieces, relative to one
another. Every 4 numbers is a new row, and there are 4 total rows.
Computer science always starts counting at 0, so we go from 0 to 14 for
1 to 15. Just remember to add one to the numbers and you will be fine.
You will also notice at the end is the -1 number. This is an internal
reference to the empty slot. When we encounter the empty slot, we ignore
it. So just remember that -1 is the empty slot, and we start counting at
0, not 1.
int *x[] = {&x1,&x2,&x3,&x4,&x5,&x6,&x7,&x8,&x9,&x10,&x11,&x12,&x13,&x14,&x15};
int *y[] = {&y1,&y2,&y3,&y4,&y5,&y6,&y7,&y8,&y9,&y10,&y11,&y12,&y13,&y14,&y15};
These are the pointer arrays. You will notice that the *x[] array holds
the values of all the x* variables, where * is a number between 1 and
15. These represent the x positions held by the puzzle pieces. x1 is the
x position of piece 1 (not the piece at position 1, but the actual
puzzle piece with the number 1).
We want this pointer array so we can reference all the x and y positions
from a single variable (a single array). It makes it easier than
checking every x variable.
We did not cover it in detail before, because we have not gone over
pointers in much detail, but the & operator, when put before a
variable means "address of". Pointers are variables which store the
address of a variable. So, if we want to make a pointer to a variable,
we declare the pointer to be the address of the variable. Simple, right?
So, the *x[] and *y[] arrays simply keep all of the x and y variables in
one place. Note that we could have used just a plain array, and done
away with all the pointer arrays altogether, but I thought it would be
nice to show different ways of doing the same thing. Just remember there
are often many ways to do most anything in C, so don't get confused if
you see two pieces of code that seem to be doing the same basic thing.
The rest of the variable declarations should be rather straightforward.
They are just integer declarations and initialization statements, which
you have already seen.
// 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 1.0", A_NORMAL);
FontSetSys(F_4x6);
DrawStr(80, 10, "Copyright (C) 2000", 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 1", A_NORMAL);
DrawStr(67, 50, "Learn more at our website", A_NORMAL);
DrawStr(70, 58, "www.technoplaza.net", A_NORMAL);
Here we see fairly simple string printing statements. We choose to avoid
the printf() functions from the TIGCC library and concentrate instead on
using AMS functions only. This keeps the program smaller.
The only real point of interest here is the FontSetSys() function, which
changes the font strings are written in. We have not done this before,
so it is a good idea to point it out here. The TI-89/92+/V200 have three
built-in fonts. There is the 6x8 size font (which is mono-spaced,
meaning all character have the same width). This is the normal size font
used by the TI-89, and the default if you do not specify a font size
before printing. There is a larger font size, the 8x10 font size, which
is also mono-spaced. Then there is a small font, the 4x6 font size. This
font size is variable-width, meaning the characters are not always 4
pixels wide. Some are actually 5 pixels wide, so it is harder to
calculate the width of a string written in this font. When I made this
program, I just experimented with various x positions to see which ones
worked best. You'll find it's mostly guess work. Just note that unless
you call the FontSetSys() function, the default font will be 6x8. And
after you call the FontSetSys() function, the font size will not change
until it is called again, either by you, or by a ROM call which prints
strings. I think the DlgMessage() function does this, but I'm not
positive. The font will reset to the standard 6x8 when the program
exits.
// 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;
}
This piece of code is responsible for randomizing the puzzle pieces. You
can see that it's workings are rather simple. We get a random number,
save the puzzle piece at that location in the temp variable. Then we
swap the randomly selected puzzle piece with the puzzle piece at the
loop position. If we wanted to be more efficient, we could have used the
currentPiece variable instead of creating the temp variable, and saved
memory. But this isn't a lesson in efficiency, although it's worth
noting.
One more minor point that's worth noting is that this random puzzle
generation could conceivably create unsolvable puzzles. I didn't think
too much about this when I first wrote the program until someone pointed
it out. A better solution would be to pick random pieces and move them
from an initially solved puzzle. This would guarantee the movements were
reversible.
// display the puzzle pieces
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++;
}
}
Here is where the puzzle pieces get displayed initially. Since we only
need to display them once, and then move them one at a time, this is a
one time only thing, since we only play one game per program.
You can see the code begins with two for loops. There is the y loop
(which loops through all the y positions), and there is the x loop,
which loops through all the x positions within each row. (4 pieces per
row with 4 rows, minus the empty slot, of course). Since the sprites are
16 pixels wide, we check to see if the y and x loops reach 16 * the row,
or 16 * the column, for y and x, respectively.
Once we have an actual position (x,y), we check to make sure we are not
at the empty slot. As long as we are not at the empty slot, we draw the
piece, and set the correct x and y dimensions in the *x[] and *y[]
arrays. Remember that to get a variable, we must put the * in front of
the pointer, so it converts it to a variable instead of a pointer. If we
did not do that, we would overwrite our pointers with bad data, and then
if we accessed that data again, we would touch something in memory we're
probably not supposed to. This would lead to a crash, which is why
pointers are often so problematic in C.
As far as the Sprite16() function goes, the only thing of interest is
the sprite argument. You'll notice we use pieces[puzzle[piece]]. We
always evaluate the inner part first, so C will figure out the value of
puzzle[piece] before giving it to pieces[something], and will place the
value of puzzle[piece] in pieces[something] as something. Now we can
figure out which piece we actually want to draw. You'll notice we did
not need to put the * in from the pieces[puzzzle[piece]]. So why is
that? It's because we actually want a pointer, not a variable. Remember
that I said all arrays must be pointers. This is because we can't store
20 variables at the same spot, but we DO need to store the place of an
array at one spot. Just think of it as one of those C nuances that you
have to get accustomed to.
// 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());
I wanted to avoid adding anything new to this review, but I could think
of only three ways to differentiate the current puzzle piece. Make the
piece blink, make inverted sprites (sprites that look like the puzzle
pieces, but have the white and black parts swapped), or use pointer
arrows. Making inverted sprites would take at least an hour, maybe more,
and make a lot of extraneous sprite tracking necessary. So I crossed off
that idea. Using arrows on top and the side seemed like it would be
confusing to the player, since the arrows could only point to a row and
a column, and although accurate, would be hard to see. It would also
waste screen space, which is at a premium. So this was probably not the
best idea either. It left me with the only solution I had left, making
the piece blink. To do that, I had to use a timer to make the piece
blink once per interval. It's hard to regulate time with for loops,
because processor speeds are hard to track, so I decided to use one of
the internal system timers. It is small though, so don't worry too much.
Basically, this is how you use a built-in timer on the TI-89/92+/V200.
You ask the AMS to free the timer (to make sure it's not in use). Then
you register the timer. There are 6 timers, but it's rare that you need
more than one, so we will always use timer 6, which is reserved for user
programs (or so I have gathered from the TIGCC documentation). Then we
set the interval. The timer is based on auto-interrupt 5, which occurs
20 times per second. This means, for every 1 value you put in the timer,
it will time 1/20 of a second. So using 10 in the timer means wait 1/2
second.
To free the timer, we use the OSFreeTimer() function, which takes the
timer as an argument. The timer we will always use is the USER_TIMER. To
set the timer, we use the OSRegisterTimer() function, which takes a
timer and an interval as it's arguments. So, in this case
OSRegisterTimer(USER_TIMER, 10) means set the USER_TIMER to 1/2 second,
because the timer is interval * 1/20 seconds. The AMS automatically
checks for timer expirations, so we do not have to worry about this.
This is called event-driven programming, but don't concentrate on that.
Just to know that it works is enough for now. To check if the timer
expired, we use the OSTimerExpired() function, which takes a timer as
its only argument. So, OSTimerExpired(USER_TIMER) returns true if the
USER_TIMER expired, false otherwise.
This part of the code also introduces another new concept, the do
nothing while loop. A do nothing while loop does just that, nothing. We
use it only when we are waiting for something to happen, but do not need
to do anything until it does happen. In this case, we are actually
waiting on the AMS to tell us if either our timer expired, or the user
pressed a key. If either of these things happened, we need to do
something, so we quit the do nothing while loop. In case you missed it
though, the syntax for a do nothing while loop is simple, while
(condition); (remember that semi-colon at the end). This semi-colon at
the end is actually a statement. It's called the empty statement. We use
it when we have a loop that does nothing. It just makes it easier for
the C compiler to process it, because when we do not use the { } to
specify the body, the C compiler assumes the next statement to be the
body of the while loop. To do nothing, we need to add the empty
statement, so the C compiler doesn't do anything we didn't authorize.
} else if (key == KEY_ENTER) {
// if they tried to move a piece
moved = 0;
// try to move the piece right
if (position+1 <= 16) {
// 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 the piece any other direction
moved = 1;
}
}
// try to move the piece left
if (position-1 >= 0) {
// if the left position is vacant, and we haven't already moved
if (puzzle[position-1] == -1 && *x[currentPiece] != 0 && !moved) {
// 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);
// do not try to move this piece any other direction
moved = 1;
}
}
// try to move the piece up
if (position-4 >= 0) {
// if the up position is vacant, and we haven't already moved
if (puzzle[position-4] == -1 && *y[currentPiece] != 0 && !moved) {
// 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);
// do not try to move this piece any other direction
moved = 1;
}
}
// try to move the piece down
if (position+4 <= 15) {
// if the down position is vacant, and we haven't already moved
if (puzzle[position+4] == -1 && *y[currentPiece] != (16*4-16) && !moved) {
// 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);
// do not try to move any other direction
moved = 1;
}
}
I know this looks like a big slice of code, but most of it is very
similar. This is the code used when the user presses ENTER, and we need
to see if we can move one of the pieces. To do this, we must check to
see if the positions around the current piece are the empty slot. This
slider puzzle is very rudimentary, so we don't do things like move rows,
or anymore than one piece at a time. We only allow piece movement one at
a time. This is to keep the example simple.
// try to move the piece right
if (position+1 <= 16) {
// 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 the piece any other direction
moved = 1;
}
}
The first thing we do is try to move the piece right. Our first if
statement makes sure the next piece in the puzzle array won't be beyond
16. If we don't, we'll get address errors when we start accessing memory
outside our array. C has no built-in protection from this, like
higher languages do, so we have to do our own accounting. The tradeoff
is acceptable, since we gain an incredible amount of power this way.
If we passed the outer if statement, we can proceed to test the puzzle
array itself. We check the next puzzle piece in the array against the
empty slot, but this is only half of the condition. We also have to make
sure our current x position is not (16*4-16). This is the x position of
the 4th puzzle piece in a row, i.e. the edge. We can't move right if our
current piece is the array. If the first part is true, but the second
part is false, it means the empty slot is actually the first column of
the next row. Since you couldn't do that in a real slider puzzle, we
will disallow that here, too.
Next we check to see if the sprite is still drawn. The code to make the
sprite blink comes in later, but since we are looping here, we could
encounter this on the second loop, or any subsequent loop. We use the
variable 'on' to see if the sprite is drawn. On is true is the sprite is
drawn on the screen. It is not true (false, but you'll see why I say not
true later) if the sprite is not drawn right now (because it is blinking
on and off). If it is not on (i.e. off), then we redraw the sprite and
reset the on status to true. This is to make sure the sprite can be
properly erased when (and if) it is moved. This is probably not the best
way to do this, but it was done to promote code reuse, instead of having
to explain another segment of code. This would most likely be replaced
if we updated this program in a future lesson or review. I'm not sure if
I explicitly mentioned this before, but C has no concept of a boolean
variable (i.e. something that is either true or false), so it mimics
booleans with the values 1 (for true) and 0 (for false). Actually, any
number greater than or equal to 1 is true, and any value less than or
equal to 0 is false. But for simplicity, we say 1 is true and 0 is
false. Many programmers define boolean constants in C to make it the
code more readable, and when we introduce type definitions, we will
probably follow suit.
Now we erase the sprite. (I know, I know, we just made sure it was
drawn... Just bear with it) It's simpler this way, but it should be
replaced with something more efficient. The rest is pretty
self-explanatory. Add 16 pixels to the x position, so it's drawn one
slot to the right. We shift the puzzle position one to the right in the
array, and set the current position to be the empty slot. Then we make
the current piece the puzzle piece of our new position. Now we re-draw
the sprite.
The last thing to do is set the moved variable to true (1, as you'll
recall from above). This is done because we will also be checking if we
can move left next. Obviously, we don't want to move right, and then
move right back left, since the left position will now be opened up. We
use the 'moved' variable to prevent this. It tells the opposite
direction that we have already moved in the other direction, so don't
move back. You'll see it in action in a minute. You'll notice we did not
have to use it here, because we have not moved yet, so it could not yet
be moved.
// try to move the piece left
if (position-1 >= 0) {
// if the left position is vacant, and we haven't already moved
if (puzzle[position-1] == -1 && *x[currentPiece] != 0 && !moved) {
// 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);
// do not try to move this piece any other direction
moved = 1;
}
}
This is the code used to move left. You'll notice it's very
similar to the code used to move right. The key differences should be
obvious. Instead of checking the right edge (16*4-16) pixels, we check
the left edge (i.e. 0). Instead of checking the next piece in the puzzle
array, we check the previous piece. Instead of adding 16 pixels to the x
position if we move, we subtract 16 pixels. We still set the moved
variable, even though if we moved right or left, the empty slot could
not be up or down from the new current position. Even still, it just
makes it look more similar. To be more efficient, we wouldn't do this.
// try to move the piece up
if (position-4 >= 0) {
// if the up position is vacant, and we haven't already moved
if (puzzle[position-4] == -1 && *y[currentPiece] != 0 && !moved) {
// 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);
// do not try to move this piece any other direction
moved = 1;
}
}
This is the code for moving up. It is similar to the code for moving
right and left, but has some more distinct differences, based on the way
the array works.
Notice first and foremost that we look at the 4th previous position in
the array, not the one after, or just one previous. This is because the
array is flat, and all the positions are on the same level, even though
we actually know that every 4 positions in the array represent a
different row. So, to get to the row just above where the current piece
is, we must look to the array position 4 before the current position.
Knowing that last bit of data, we can see all the relevant differences
between this and the right/left segments. Remember also that since we
are moving through rows, not columns, we change the y position, not the
x position.
// try to move the piece down
if (position+4 <= 15) {
// if the down position is vacant, and we haven't already moved
if (puzzle[position+4] == -1 && *y[currentPiece] != (16*4-16) && !moved) {
// 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);
// do not try to move any other direction
moved = 1;
}
}
Knowing what you know from the up, left, and right segments, the down
movement should be trivial. We just move the y position in opposite
direction of the up movement, and check the position in the array 4
after the current position, instead of 4 before.