} 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];
Now we can look into the other key presses that we need to examine. The
first key we will take a look at is the LEFT arrow. The arrow keys move
around the puzzle changing the current piece, so we can move different
pieces around. If we had a real slider puzzle, we would simply put our
finger on the piece we want to move, but we had to alter that for the
digital plane. I thought about using the numbered keys to move the
different pieces, but that would have been complex, and what about
pieces higher than 9? So I decided on the current strategy instead.
You'll notice the first thing we do is make sure the piece is drawn. We
explained how this worked when we were doing the move right code
segment, so we will skip it here. It just checks the on variable, and
draws the piece if it's not on.
Albeit a bit inconsistent, I thought it would be easier for the
movements to wrap around. However, the left and right actually wrap
around from the end of one row to the beginning of the next, or from
the end piece to the beginning piece, because it does not do checking
for end positions, only array indexes past 16 (for complete global
wrap-around, instead of local).
So, the first check is to see whether or not the last position in the
array exists (i.e. position 0 has no last position, so we need to
perform wrap-around). A better version of the puzzle might make the left
and right movement wrap around to the same row, instead of the
next/previous, or beginning/end row, but we didn't write a better
version, we wrote this one, so we'll use this for now. It's simpler this
way, and it means I have to explain less. :-)
If there is a previous position in the array (i.e. the current position
is > 0), then the rest is pretty simple. We shift the position one to
the left. However, we also need to check for the empty slot. To do this,
we check to see if our new position has a previous position, and if it
is, did we hit the empty slot. If these two conditions are true, then we
shift the position left again. If we did hit the empty slot, but we are
at the beginning, then we must wrap-around to 15, the end position.
But what happens if there was no left position to begin with? Simple
again. We just move directly to position 15 (the end slot). Then we
check for the empty slot, and if we hit it, move left one. We do not
need to check for another previous slot, because we are already at 15,
so there will definitely be a previous slot.
Now that we have shifted the position, we need to update the current
piece to the puzzle piece of the slot we moved into. This is also very
trivial to do.
} 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];
This code looks very similar to the code used to move to the left. We
just need to move to the right instead, so instead of shifting the
position one left, we shift it one right. All the rules still apply,
just in the opposite direction. Just remember that to do wrap-around
now, we don't check position 0, we check position 15, because that's
where we will be if we are at the right-most position, not 0, as the
left-most position is. Everything else should be readily identifiable.
} 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];
Moving up is a different story than moving right or left. This is
because wrap-around was implemented more properly, by keeping the same
column, and only moving to the bottom row. Since up from the top row
would wrap around to the bottom row, in the same column.
We see it begins much the same way as right or left, by ensuring that
our piece is drawn. Then we move on to array position checking. To find
the position one above from us, we move 4 positions back in the array,
since there are 4 columns in each row.
If such a position exists, we shift to it. Now we must check for the
empty slot. If we hit it, and there is another up position in the array
before us, then we need to move to it. However, if no such position
exists, then we need to find the position on the bottom row in our same
column. To find this value, we take the number of slots in the array (so
as to start from the end), then subtract the inverse of the column
number, which is 4 - the column, since there are 4 rows in a column.
This is because the position 1 is 3 (or 4 - 1) from the end. We could
also have done 12 + position instead of 16 - (4 - position), but they
are both equivalent. I just thought of the 16 - (4 - position) first.
12 + 1 = 16 - (4 - 1) = 13, which is also the same as 12 + x = 16 - (4
- x) for all x's in the real number set. I hope that was already
self-evident. :-)
Knowing this information, the rest of it should be easy to follow, as
it's basically the same as the left movement.
} 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];
}
Now we examine the down movement. It is similar to the up, left, and
right movements, but we have a new way for calculating the wrap-around
position, as you might well imagine.
We also have to make sure the next position down (4 positions forward in
the array) is <= to 15, so we don't go past the end of the array.
Remember that this is dangerous (i.e. address error's are not only
possible, they are mandatory). So, if we have a down position, then we
need to go to it. Next we check again for the empty slot. If we have
another down position, and we hit the empty slot, we can move here, but
if we do not, then we need to find the wrap-around position.
Calculating the wrap-around position requires some knowledge of integer
division, and modulo division. Remember that we are not working with
floating point numbers (floats cannot even be processed by the 68000
processor natively), but rather with simple whole integers. This means
when we do division, we end up with two things. The quotient and the
remainder. The quotient is the number of times the divisor will evenly
divide into the number. For example, 7/4's quotient would be 1, because
we can evenly divide 7 into 4 pieces 1 time. 8/4's quotient would be 2,
since 8 can be evenly divided by 4 two times. Now let's discuss the
remainder. The remainder is the number we are left with after evenly
dividing the number by the divisor. So, in our examples above, 7/4's
remainder would be 3, since we have 3 left over after dividing 4 into 7
one time. 8/4's remainder would be 0, because 8/4 is a perfect quotient.
These are all basic algebraic operations which you should know already,
but it's worth mentioning here since C does not create floating point
numbers from the division of integers, because we can only store whole
numbers (i.e. integers) in integer variables. We have no use for floats
at this juncture.
Now that we've had an introduction to integer and modulo division (the
quotient and the remainder, respectively), we can talk about how this
helps us calculate the position of the correct column in the first row.
If we think about it, the remainder of the division between the position
we are at now, and the number of columns per row (4) would produce the
column number. Since the column number is in the first row, we do not
need to further adjust this number. We already have the correct position
within the array, since the column number + 0 would be the correct
offset in the puzzle array.
So, we know that the column position is calculated by the modulo
division of the position and the number of columns, 4.
With this information, the rest of the code is rather similar to the
other directions. Just know the the position down is actually 4 forward
from the current position, and you should be fine.
// 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);
Now that we are done with keystroke management, we can move on to the
next part; the blinking current position. To do the blinking, we use the
timer to timeout every 1/2 second, then when it expires (or the user
presses a key, though this is actually a side-effect), we flip the piece
off (or on). To keep track of the current status of the piece, we use
the on variable. You'll remember that earlier I said it is set to not
true if it is off. This is what I meant. In C, we can toggle a
variable's true/false status by assigning its not value. The statement
on = !on; means, whatever on is, reverse it. Make it whatever it is not.
This sounds weird, but it's just like a light switch you flip on, or
off. You don't need to check whether the light switch is flipped to the
on position to turn it off. We just flip it to the inverse of whatever
it is.
The Sprite16() function is pretty self-explanatory, since we already
maintain the current piece, we always know which one to blink.
// 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;
}
}
Here is where we check for a winner. It would actually be more efficient
to do this only when we move a piece, but it was simpler to do it this
way. This is just another place where we have to stick within the
confines of what we have already learned in lessons 1, 2, and 3, so as
not to make the review too complicated. As I've said before, this
program is definitely not the best that it could be, if it were
properly written. We will probably touch upon this in a future lesson or
review.
To see if we have a winner, we first set the winner variable to true.
We do this because it is very easy to disprove this statement, but very
hard to prove it. It's just easier to make this kind of test, because
instead of proving every position is in the correct position, all we
need to do is find one piece which is in the incorrect position. To
prove that the winner were true, we would need to check every spot for
the winning circle, which means we would have to check every slot, for
the correct position. Instead, we just assume this is true, and then
disprove it. (That sounds more complicated than it did in my head...)
To see if we have a winner, we must loop through the puzzle array, and
examine each piece until we 1) come to the end of the array (in which
case we have a winner), or 2) find a piece which is not in the winning
position.
To find if a piece is in the winning position, we calculate the correct
position for the piece. The y position is correct for a particular
position in the array if it's value is the row times 16 pixels (with the
row numbers being 0-3). To find the row position, we use integer
division. The quotient of the loop position in the array and 4 yields
the row number. Now to find the correct x position. The x position is
correct for a particular position in the array if it's value is the\
column times 16 pixels (with column numbers being 0-3). To find a column
number, we use modulo division. The remainder of the loop position in
the array and 4 yields the column number, as we save before above. If
either of these conditions yield false, then we set the winner variable
to 0, and break from the loop.
You'll notice the new keyword break. The break statement exits from a
loop, and stops it from going on. So, if we hit a break inside this for
loop at position 5, the rest of the positions won't be evaluated. This
is more efficient because we can stop when we've found one piece out of
place, which is very soon.
// 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);
}
If we still have a winner at the end of our check, we need to exit the
outer while loop, by setting the done variable to true. Then we make
sure the piece which was blinking stops blinking. Finally, we use a
dialog pop-up box to tell the user they have solved the puzzle.
DlgMessage() is a new function. It is used to bring up a message dialog
box with a simple message and a title. The arguments are as follows:
title message, display message, first button, second button. Since we
haven't really covered dialog box messages, it is enough to know that
the BT_OK means an OK button, and BT_NONE means no button. We'll cover
dialog boxes in more detail in a future lesson. For now, just understand
that this function will bring up a pop-up dialog displaying "Winner" as
a title, and "You have completed the puzzle!" in the main window, with
one button, and OK button. You can exit this dialog by pressing ESC or
ENTER.