Crazy Tetrominoes

As the second project we will reuse a lot of know-how from the Snake game and build upon it. We will tackle a clone to a very popular game and the challenge compared to the Snake game is that its timing is much more demanding. If we don’t think through the data structures and algorithms well enough we can easily end up with a game that is too slow to be playable. We also need to take care of more keyboard controls and will need to pay more attention to it’s appearance with more colorful graphics and we need to introduce sound effects.

The knowledge required is just knowing basic programming and it would be beneficial to read some of the other blog posts describing color palette and custom character sets.




Please enjoy and leave comments for future improvements.

Project specs:

  • Emulator Version R.37
  • Low Resolution character mode 40x30
  • No sprites
  • Standard color palette with few minor adjustments
  • Standard character set with few minor changes
  • Use of PETSCII special characters for title screen and graphics on play screen
  • 25 levels with increased speed, move to higher level after every 10 lines cleared
  • Scoring based on original Tetris game:
    • 40 points for one line cleared
    • 100 points for two lines cleared
    • 300 points for three lines cleared
    • 1200 points for four lines cleared
    • Same number of points is rewarded regardless of level
  • Keyboard controls:
    • Left arrow – move tetromino left
    • Right arrow – move tetromino right
    • Up arrow - turn tetromino counter clockwise
    • Space bar - fast drop tetromino without chase to move it left or right again
  • Basic sound effects, no music:
  • Sound for dropping tetromino
  • Ping sound for every line cleared

Program Structure

We will start analyzing the source code by looking at the setup where we create necessary data structures to make later code as efficient as possible. Let’s dive right into it and check the setup code and what data structures we initialize and why.


We will use four arrays with following content:


DIM SH(18,3) 19 distinct tetrominos with 4 squares each
DIM IX(27) Index into shapes that we use with rotation variable 7 shapes * 4 rotations = 28 positions
DIM CO(6) Color for each of 7 tetrominos
DIM FULL(4) We can have up to 4 full lines when tetromino hits the bottom, we will store row numbers that are full in this array on positions 1-4


Before we can populate these arrays we have to understand exactly what kind of data we are dealing with. A tetromino is a geometric shape composed of four squares, connected orthogonally. There are 7 distinct shape possibilities.
More info on Wikipedia

We can name each based on similarity to letters O,I,S,Z,T,J and L and assign them distinct colors:




Once we understand all the shapes we have to also determine all the possible rotations. It is clear for example that O shape remains the same in all rotations so we only need to store one shape. Shape I on the other hand can have two different appearances for 4 rotations and so on. Let’s draw all possible rotations for clarity.





We have marked the index numbers to each possible rotation that can be used when storing in array. Before we can do that we have to decide what is the most efficient way to store them. Modern computers are so fast they can do millions of calculations for 3D rotations in real time, unfortunately we don’t have that luxury.
We will follow two simple rules from 8bit days to speed things up:
  • Pre-calculate as much math before the execution so math inside game loop is as simple (fast) as possible
  • Unwrap the loop, so instead of looping through the array, just hardcode the execution for each element of the array.

Applying the second rule is pretty straight forward. We know that each tetromino consists of four squares (characters) so we don’t need to loop through them but just draw all four in separate commands.

For applying the first rule we have to remind ourselves how the screen memory layout works and what information is needed for drawing. As we know the screen memory is sequential and by default each line consists of 256 bytes for 128 characters and 128 color attributes. We could of course change that but for the sake of simplicity we will use one of predefined screen modes.
So to calculate each location on screen we use following formula to move from two dimensions of the screen to one dimension of the video memory:

M = Y * 256 + X * 2

We also know that multiplying and dividing is much slower than adding and subtracting so we try to limit the number of times we need to perform it. Since the Tetrominoes are moving left, right and down the playing field we will have to calculate the starting point but then we can just store the offset for all four squares relative to the starting point which we will set as the top left corner. Every tetromino shape can fit into matrix of 4 x 4 so we can calculate offset quite easily:



Now we only have to fit each shape in every possible rotation into it. For example the shape S:




Now it is simple matter to store into array like so:

5041 SH(3,0)=4
5042 SH(3,1)=6
5043 SH(3,2)=258
5044 SH(3,3)=260


Obviously the first of two dimensions is the index we defined on the Image above for all 19 possible rotations and the second dimension is just so we can fit all four blocks. We can fill the rest of the array, which is done in lines 5000 - 5116.

Next array that we have to fill is the Index for rotations (IX). For every active tetromino we will later use three variables:
S - Shape with value 0 - 7 where 0 = shape O, 1 is shape I and so on
R - Rotation where 0 starting position, 1 90 degrees counterclockwise, 2 180 degrees counterclockwise and 3 270 degrees counterclockwise
C - Color index

We will handle colors separately because it doesn’t get changed based on rotation. That leaves us with index that can be calculated using shape and rotation using simple formula:

Index = S * 4 + R

That will point to shapes for Shape O four rotations at indexes 0,1,2,3 which obviously will point to the same shape definition. For shape I indexes 4,5,6 and 7 pointing to shapes 1 and 2 and so on. Let’s check the actual initialization code:



The only remaining initialization is the color index which couldn’t be simpler.

5310 CO(0)=$78:CO(1)=$3E:CO(2)=$D5:CO(3)=$29:CO(4)=$4B:CO(5)=$E6:CO(6)=$82

We define text and background color for each of the shapes from 0 (shape O) to 7 (shape L). We will mostly use default 16 colors with some minor modifications.

With these decisions we actually finished most of the decision making for this project since everything else is just a basic game loop and bunch of functions for drawing and some more involved collision checking and scoring.

Dynamic Drawing Functions

With all the data structures set up we can write drawing functions for displaying and deleting tetrominoes like follows:



We will look at variables in more detail later but it should be clear that CX and CY are tetromino positions inside play field so the formula:

M=(5+CY)*256+(10+CX)*2

Clearly calculates the top left corner of the tetromino position in video memory. And formula:

I=IX(S*4+R)

Calculates the index into a shapes array so we can then VPOKE directly into video memory. Here we unwrapped the code into 8 VPOKES (4 for shape and 4 for colors)  for maximum speed.


Game Loops


Let’s take a look at the main program loops. We actually have three nested loops.

┍━━ New Game Loop
Initialize new game
┍━━ New tetromino Loop
Generate new Tetromino
Initialize related variables
Check collisions and end the game if detected
┍━━ Game Loop
Process delays related to level difficulty
Process keyboard and actions
Move tetromino
Check collisions
Draw tetromino
┗━━ End Loop
┗━━ End Loop
┗━━ End Loop


It is time to look at above pseudo code in actual BASIC code:



To understand the code we have to first describe variables and how we use them. The only variable that was initialized outside New Game loop was

HI - to score High score. It gets evaluated and updated after every game is finished.

In the New Game loop we initialize following variables:

SC - Score
LV - Level, starts with 0
LI - Line counter. It gets updated every time we clear line(s)
HL - Highest line occupied in the play field. Scrolling is slow in BASIC so we track this value so we minimize the amount of screen we need to scroll down when deleting full lines

In line 130 we already generate new Tetromino, which is the first one to be played because of the functionality to also show the next one we jungle a bit with temporary variables (SS and SN for Shape Next) to be able to use standard display function.

In the New Tetromino loop we initialize following variables

S - Tetromino shape (0 -7 for O,I S,Z,T,J and L)
R - Rotation. Default is 0 and can then be become 0,1,2 or 3 for all four positions
CO - Collision. Default is 0 for no collision detected. SInce BASIC functions can’t return values we use this (global) variable to return it from Collision function (Lines 700-770)
CX - Current X position of top left corner of tetromino
CY - Current Y position of top left corner of tetromino
DR - Is tetromino being dropped. By default it has value 0 and we can manipulate it by moving left, right and rotating it. If we press space it is dropping fast and for that DR variable is set to 1

In the Main Game Loop we only add few important variables:

T - Time counter is used to determine the speed at which tetromino is falling and that speed depends on the level we are in. This is actually first line of the main loop:

200 T=50-(LV*2)+1

At Level 0, T is 51-(0*2) = 51 so the main loop loops 50 times for each drop of tetromino by one line. At the last level 25 though it only loops once 51-(25*2) = 1

Two outer loops are pretty self explanatory but let’s look at the rest of the main loop a little closer:

In Line 210 we save position to temporary variables DX and DY and set Collision to 0.
Line 220 is used to bypass the keyboard controls and speed delay and make sure the tetromino falls fast and goes straight to collision control and the drawing section of the main loop.
Lines 230-243 check the keyboard controls and call appropriate functions.
In Lines 250-260 we count down the Timer and decide if it is time to move tetromino down one line or go back the loop to keyboard checking. It is very important that keyboard checking is in the inner most loop so the game is very responsive and we can move and rotate tetromino multiple times on the same line in early levels.

Last section of the main loop is where a lot of action happen:
In line 270 we calculate new Y position in DY
In Line 280 we delete the current position of tetromino
Line 300 calls collision control and immediately checks the result of it. If there was no collision, redraw the tetromino at a new location and jump back to the beginning of the main loop.
If there was a collision means we got to line 310 which calls the function (410 - 495) that handles hitting the ground and everything related to that event.
After that processing is done we have to generate new tetromino at the top of playground and therefore we jump to the beginning of that loop from Line 320

To finish the core code we need to look at the crucial code that handles Collision Control, Hitting the ground and checking for full lines and scoring.

Collision control

There are many ways to do collision control. In this implementation we look directly into the video memory to see if there are blocks or edges of the play field in the area where we want to place our tetromino next.



The formulas in the beginning are the same as for drawing with only difference that we use future locations (DX and DY) instead of current locations (CX and CY).
To reduce the number of IF statements (that are very slow) we simply add content of all four block locations into variable N. If all four are empty (containing space character 32) then the sum has to be 128. If it is not then we set CO variable to 1 end exit.

Hitting the ground

Well, we don’t technically hit the ground after we have placed a few tetrominoes at the bottom of the playfield. We use the term to process the situation when collision is detected by regular movement down. It means that we hit an obstacle and that we reached the resting place for the current tetromino.
The source code handling is in function 400-495



We first have to do some housekeeping so
Line 410 redraws the tetromino at the resting position
In line 420 we turn off the sound
And in line 430 if needed we update the highest line occupied with blocks
The rest of the function from lines 440 to 490 is checking if we filled any lines completely and store those in FULL array.
In line 490 we call the function to collapse and update the score function if at least one line was full.

Collapse and Score

This function takes the array FULL and variable FL as input to remove the full lines, scroll the rest of the play field down and update the score based on the number of lines cleared.



This function could be optimized to do clearing of all lines in one loop but for now it does one at a time so if we cleared four lines it scrolls down the remaining blocks in the playfield four times. This is somewhat slow but it provides a nice visual and audio clue on how successful we were. It also provides a short break before the game continues.
The only optimization is that we only scroll the occupied part of the play field using variable HL.
The only thing remaining after collapsing the full lines is to update score and display updated score, lines and level on the screen

Game Controls

We have pretty clean game setup so the game controls are pretty straightforward. We have four keys we need to react to:
Left arrow to move tetromino one character to the left
Right arrow to move tetromino one character to the right
Up arrow to rotate tetromino by 90 degrees counterclockwise
Space bar to drop tetromino to the bottom at maximum speed

Each of the functions is only few lines of code:



The logic should be pretty clear. First we delete the tetromino from the old position, change the location or shape at a temporary new position and check for collisions. If there is none we update the new permanent position and redraw the tetromino there.
The only exception is dropping function because there is really nothing else but to change the DR variable to 1 which we do already in line 243. The function only set up the sound that is played while the tetromino is dropping.


Utility Drawing functions

We have to look at a few simple drawing functions that we used earlier to update the screen with some dynamic data. Those are:

Delete Next Area - we use standard function to draw next tetromino but before we have to delete the space on the screen by drawing spaces
Update High - overwrites the High score on the screen
Update Score - overwrites the current Score on the screen
Update level - overwrites the current Level on the screen
Update Lines - overwrites the current number of cleared lines on the Screen
Game Over - Draws large GAME OVER on red background in the middle of the screen and waits for any key to be pressed



They are all very similar and work in the same way. First we move the cursor to the top left corner and then just move the required number of lines down and right and overwrite the previous value.


Polishing it all up

This final area of discussion will cover the final touches to the game. We finished the functionality but in order to have a pleasing experience it is very important that we don’t only have bare bones of functionality but add some more display elements. Most common is to have a nice title screen and attractive looking play screen.
Another area of improvement is to adjust the colors a little bit and also the appearance of blocks that make up each tetromino.



We make Red, Yellow, Orange and Cyan a little more vibrant by changing the values in the palette.

The character we use for drawing blocks in tetromino is $7A hex. We use standard color mode meaning we can use two colors for each block where front color is used for shadow and background for the main color. The default character set was designed for Commodore 64 in times where TVs were often used as display in general color CRTs of that time had a very fuzzy picture therefore Commodore designed characters that used double pixel width. However now the single pixel width for shadow looks much more convincing so we have adjust the $7A character a little bit:



The needed change is done in line 4050.

To play tribute to the Russian origins of the original Tetris game we will draw a simple representation of one of the towers of St. Basil Cathedral in Moscow and very often Cyrillic text is symbolized by mirrored letter R.

We redefine letter R in the function:



We simply use the existing definition of R and mirrored all the bits for each line.

We use PETSCII characters and display the tower and the rest of the Play screen. The final results is as follows:




We display the picture using DATA statements and poking directly into video memory.



For the Title screen we employ different technique which is much faster and more compact but it is a bit more difficult to write unless some special software is used to draw and generate BASIC source code;




And the source code:



With this we can finally load up the game and enjoy it. Link to complete source code is available on GitHub:
Source Code


Source code reference with (line numbers)

Main Game Functionality
New Game Loop (100-300)
New Tetromino loop (150-320)
Main Loop (200-300)

Hit ground (400-495)
Collapse and Score (500-670)
Collision Control (700-770)
Game Controls
Move Left (1000)
Move Right (1200)
Rotate (1400)
Drop (1600)

Drawing Functions
Draw shape (2000-2070)
Delete shape (2100-2170)
Delete Next area (2200-2240)
Update High (2300-2320)
Update Score (2400-2420)
Update Level (2500-2520)
Update Lines (2600-2620)
Game Over message (2700-2790)

Draw Play Screen (3000-3300)
Draw Title Screen (9000-9260)

Game setup
Setup Graphics (4000-4060)
Turn R around (4100-4170)

Define game pieces (5000-5116)
Build Index for rotations (5200-5228)
Build Color Index (5300-5320)

Graphics Data (10000-10550)









Comments

Popular posts from this blog

Commander X16 Premium Keyboard

Hello VERA (BASIC vs C vs Assembly)

Default Palette