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

       Part I

       Part II

       Part III

     Functions

     Pointers

     Dynamic Memory

     Slider Puzzle 2

     Structures

     Bit Manipulation

     Advanced Pointers

     File I/O

     Graduate Review

   Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

C Programming Lessons

TIGCC Programming Lessons

Lesson Review 1 - Text Display, Keyboard Input Handling, and Sprite Graphics

Step 3b - Program Overview Analysis

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.

Continue with 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