This project includes about 1100 lines of code written entirely in C to be compiled with avrgcc. To make it more manageable, it is divided into 7 separate files each with a .c and .h file.
main.c main.h -- This file contain the large control loops moving between modes of operation and polling the buttons during the game.
check.c check.h -- Here are functions to check for completed lines or to see if a move is legal. These routines could be optimized based on knowledge of the piece orientation, but we had enough mips that it wasn't necessary.
data.c data.h -- Here are the global data structures. There are large arrays for the character sets, screen buffer, sound effects, and rotation vectors. There are also many small variable keeping various state used for flow control, etc.
lcd.c lcd.h -- This is almost half of the code. There are a couple primitives that directly access the ports WriteByte(), and WriteCmd() to send data and control to the LED. Then there are layers built on top of this to display the entire screen, print text, and print various other screens (game over, etc.)
timer.c timer.h -- The real time aspect of dropping the pieces . As well as the timer for the sound output.
button.c button.h -- This file reads the ports and translates that into a type of button push.
sound.c sound.h -- This does the sound output. The sounds are stored as adjacent entries in an array with duration frequency pairs.
There are four basic modes:
Startup screen -- Any button here takes you to the game.
Game screen -- This is the interesting part.
Game Over screen -- Since you can't play forever, when you finish you can see your score, level, etc. You can play again by pressing any key here.
Settings screen -- this is the secret screen to adjust the initial speed, incremental speed and lines per level. The leftmost button moves between selections, the right two buttons increment and decrement the values.
The main loop has four steps:
Check to see if the last piece already landed and was recorded. If so, look for complete lines and score them, then try to add a new piece. If that fails, the stack is too tall and the game is over.
Display the new screen.
Poll the buttons
Respond to any button presses. Any button presses is checked to see if that move is legal, otherwise it is ignored.
One of the most useful functions is CheckOverlaps(). It's used to check the validity of various moves. In a timer interrupt, the piece is moved down one line. Then if CheckOverlaps() fails, the piece is moved back up one and locked in place (i.e. it landed). Another place it's used is to determine if a button press is valid. The piece is moved and then it checks the new position. If there are problems, then it's moved back. Finally, CheckOverlaps() is used to see know when the game has ended. Immediately after creating a new piece, it checks for overlaps in the initial position. As soon as there are overlaps there, the game is over.
There are a couple of system routines that were either device specific or not included in avrgcc.
rand() -- All this function does is read a number from timer0, This isn't the truly random, but it's close enough for our purposes.
MyStrCmp() -- This is available somewhere, but it was quicker to rewrite it than find it.
LargePrint() and SmallPrint() -- (named for the fonts) With all the strings we needed to print for the intro and game over screens semi generic print string functions were needed for the graphic LCD. The large font was 7 bits wide, so it's printed aligned with the page boundaries. The small font is 4 bits wide plus a 1 bit space between each character. 5 bit pitch overlaps page boundaries regardless of where you start. The function takes an arbitrary x,y location (no alignment restrictions) to start from and prints the string. The only limitation is you can't overlap the chip boundary at y=64.
There are three categories of data that are used. Most of this is pretty boring so skip this section unless you want to try to read the code.
Game state
These are dynamically updated through the game. Most are single byte variable and a few use enum types. Examples are the screen buffer piece type and position:
uchar screen[WIDTH][HEIGHT];
uchar score1000 = 0;
uchar level = 0;
piecetype PieceType = NOTYPE;
piecerot PieceRot = ZERO;
char PieceX = 0;
char PieceY = 0;
buttonlr ButtonLR = NOLR;
buttonrot ButtonRot = NOROT;
Game control
These get set at the beginning and determine the speed of play and how much the difficulty increases each level:
uchar lpl = 4;
uchar lines_clear = 0;
uchar speed = 0x20;
uchar timercomp = 0xFF;
Lookup tables
A lot of the data was easier to lookups than to compute it at runtime. For things the sound samples and character set, these had to stored somehow, and array happen to be very easily accessible.
All the 8 entry arrays are indexed by the PieceType. One of the more interesting tables is the PieceArray which stores the shape in one byte. PieceArray[1] is the square piece, so the bottom two bits are set in each nibble. PieceArray[7] is the straight piece so all four bits are set in the upper nibble and the lower nibble is empty. This format works particularly well since all the pieces use a uniform way of storing that information. Earlier revisions had unique formats based on piece type.
The other interesting one is the RotArray has the delta x and delta y for each piece and orientation. There is some complex code in the Rotate() function in main.c that interprets this based on direction of rotation and initial orientation to compute a new X,Y location after the rotation. The reason for this is to give the illusion that the piece is rotating around some imaginary center. This is some of the more interesting code in the project.
uchar PieceWidth[8] = {0,2,2,2,2,2,2,1};
uchar PieceHeight[8] = {0,2,3,3,3,3,3,4};
uchar PieceArray[8] = {0x00, 0x33, 0x71, 0x17, 0x72, 0x63, 0x36, 0xf0};
uchar RotArray[8][4] = { {0x00, 0x00, 0x00, 0x00},Andy did the sound samples, so he can tell you more about them. They appear to be based on a sets of frequency, duration entries.
uchar Sound1[20]
The small characters are 4x7, so they are packed two horizontal slices to a byte. The larger character set uses 7x11 characters, so only one slice is stored in each byte.
uchar charmap['Y'-52][4] = { {0x69, 0x99, 0x99, 0x60}, // 0
In order to be able to test much or the Program Core on a PC. The devices were created with PC equivalents to display the screen or poll the buttons. This made initial testing very fast and gave high confidence in basic functions like Rotate() and CheckForFullLines(). Most of the time using the processor was spent debugging code for the LCD. The other devices were simpler.
Buttons -- Almost nothing to this one. Read the port and look for zeros.
Speaker -- Also simple set a couple of registers related to the PWM.
LCD -- Some key learnings from the LCD. When resetting, wait for the reset flag to clear before continuing. Make sure you are always printing something to the LCD during testing. Lots of time was wasted debugging the LCD code when actually the bug was in creating a new piece to display. Since the new piece didn't appear it was easy to assume the bug was in the LCD routines.
Several times through the development we ran out of memory. So several of the larger data structures have been compressed. They're packed into nibbles for each entry instead of an entire byte. Examples of this are the charmap, RotArray, and PieceArray. If we needed more memory there were still data to compress. One of the sparsest is the screen array. This array wastes an entire byte to store a true or false. Unfortunately there are more pieces of code that access that structure, so optimizing it would have taken more time compared with the other arrays.
Next time, static types to would remain in program memory to be read from there instead of wasting all RAM on lookup tables. That only requires learning a few new functions.