Tutorial 11: Game Logic – Tetris Game by Osama Hosam


Introduction

We have presented the interface development of the Tetris game. In this tutorial we are going to introduce the core implementation of the game. First we will look at the game matrix and how to update the game with the new values updated in the game matrix.


The Overall Idea

We have the game block entity with the following properties

	int row_pos;
	int col_pos;
	int rotation_pos;
	int shape;
	int next_shape;
	bool is_halt_mode;

row position is the horizontal position of the block, col position is the vertical position of the block. is_halt_mode refers to the situation in which the user presses the Esc key in the keyword, accordingly the game must be halt until the user decides to continue or exit the game.

rotation_pos is the value of the rotation; every block has a rotation value, according to this rotation value the shape will not be changed but the orientation will be changed; it will be changed only if the user pressed the UP_ARROW from the keyboard, Fig.1 shows the concept of the block rotating. Shape refers to one of the list of shapes we stated before in the previous tutorial, i.e number 1 for the T shape, 2 for the L shape, etc. The next_shape refers to the shape that will be the next and will be displayed in a small window at the top left corner of the game screen.

The above block properties will be taken to the Game_Matrix’s single block matrix. The values in single block matrix will be changed according to block’s properties (fields).

Fig.1 The single block matrix manipulation according to the block’s fields.

The next step is to make a comparison between both single_block matrix and the block_store matrix. In this comparison the collision of the moving block with one of the blocks in the block store will be detected. also the collision between the moving block and one of the window sides will be detected. According to this detection the moving block will be restricted in its moving. If the moving block hits the bottom side or one block in the block store from the bottom it will be added directly to the block store matrix. The block store matrix will be detected to see if a complete row is exist, this will be done by examining the row to see if all the row values greater than zero, if so the row will be removed and all the rows above it will be shifted down one row.

The Moving Block Class

The following is the implementation of the block class


enum Sides{LEFT,RIGHT,TOP,BOTTOM};

class Block
{
private:
	int row_pos;
	int col_pos;
	int rotation_pos;
	int shape;
	int next_shape;
	bool is_halt_mode;
public:
	Block()
	{
		setNextShape();
		resetBlock();
	}
	int SetRandShape()
	{
		// 1:T, 2:L , 3:Z, 4:I 5:O 
		int rand_shape=rand()%5; //output from 0 to 4
		rand_shape++;
		return rand_shape;
	}

The SetRandShape gets a random value from 1 to 5, This will be the next shape to be displayed. Also this random value will be the shape value of the next shape.

	void resetBlock()
	{
		row_pos=0;
		col_pos=7;
		is_halt_mode=false;
		rotation_pos=1;
		shape=next_shape;
		setNextShape();
	}

In the above function we set the shape to be the next_shape, this means the next shape value (randome) will be the shape of the next shape to be displayed.

	void setNextShape()
	{
		next_shape=SetRandShape();
	}
	int getNextshape()
	{
		return next_shape;
	}
	void MoveBottom(bool isDownArrowPressed)
	{
		if(!isCollide(BOTTOM) && !is_halt_mode)
		{
			row_pos++;
			//this way we will increase time
			//only if the keydown is not pressed
			if(!isDownArrowPressed)
				game_settings.increaseElapsedTime();
		}
	}

The MoveBottom function will move the block to bottom until it collides with the Bottom of the screen or one of the blocks in the block store matrix.
The time of the game will be increased only if the player didn’t press the DOWN_ARROW from the keyboard otherwise the game time will not be increased, simply because pressing the DOWN_ARROW will make the block move directly from the current position down to the first block it hits in the block store.

	void MoveRight()
	{
		if(!isCollide(RIGHT) && !is_halt_mode)
		{
			col_pos++;
			game_settings.increaseElapsedTime();
		}

	}
	void MoveLeft()
	{
		if(!isCollide(LEFT) && !is_halt_mode)
		{
			col_pos--;
			game_settings.increaseElapsedTime();
		}

	}

MoveRight and MoveLeft move the block to right or left by increasing and decreasing the horizontal position of the block respectively.

	void RotateShape()
	{
		if(!is_halt_mode)
		{
			if(rotation_pos == 4)
				rotation_pos=1;
			else
				rotation_pos++;
		}
	}

Rotate shape changes the orientation of the block, if the orientation is 4 it set back to 1.

	void haltGame()
	{
		is_halt_mode=true;
	}
	void continueGame()
	{
		is_halt_mode=false;
	}
	bool isHaltMode()
	{
		return is_halt_mode;
	}
	int getRow()
	{
		return row_pos;
	}
	int getCol()
	{
		return col_pos;
	}
	int getRotation()
	{
		return rotation_pos;
	}
	int getShape()
	{
		return shape;
	}
	bool isCollide(Sides sd)
	{
		switch(sd)
		{
		case LEFT:
			if(col_pos <= 0 )
			{
				return true;
			}
			break;
		case RIGHT:
			if(col_pos >= MATRIX_COLS-1)
			{
				return true;
			}
			break;
		case BOTTOM:
			if(row_pos >= MATRIX_ROWS-1)
			{
				return true;
			}
			break;
		}
		
		return false;
	}

The isCollide method detects if the block collided with one of the sides of the screen, if so it returns true, otherwise it returns false.

}current_moving_block;

The GameMatrix Class

The GameMatrix has internally two matrices, the single_block matrix and the block_store matrix. The single_block matrix in addition to the block store matrix together form the digital (number) representation of the game screen. The main objective is to update the single_block matrix according to the moving_block values then the single_block matrix is added to the block store matrix to form the digital representation of the game screen. This representation is taken to the Paint class to draw it to the screen. The implementation of the game matrix class is as follow:

class GameMatrix
{
private:
	int single_block[MATRIX_ROWS][MATRIX_COLS];
	int block_store[MATRIX_ROWS][MATRIX_COLS];
public:
	GameMatrix()
	{
		ClearSingleBlockMatrix();
		ClearBlockStoreMatrix();
	}
	void ClearSingleBlockMatrix()
	{
		for(int i=0;i < MATRIX_ROWS;i++)
		{
			for(int j=0;j < MATRIX_COLS;j++)
				single_block[i][j]=0;
		}
	}

The ClearSingleBlockMatrix sets all the values of the single_block matrix to be zero

	void ClearBlockStoreMatrix()
	{
		for(int i=0;i < MATRIX_ROWS;i++)
		{
			for(int j=0;j < MATRIX_COLS;j++)
				block_store[i][j]=0;
		}
	}

The ClearBlockStoreMatrix sets all the values of the block_store matrix to be zero

	void setSingleBlockElement(int row,int col,int value)
	{
		if(row > = 0 && row  <  MATRIX_ROWS && col  > = 0 && col  <  MATRIX_COLS)
		single_block[row][col]=value;
	}

The setSingleBlockElement sets the value at the row position row and column position col to the value “value”. The function checks to see if the row and column are within the matrix boundaries.

	int getBlockStoreMatrixElement(int i,int j)
	{
		if(i  > = 0 && i  <  MATRIX_ROWS && j  > = 0 && j  <  MATRIX_COLS)
			//touch block
			return block_store[i][j];
		else
			//touch the walls
			return -1;
	}

The function getBlockStoreMatrixElement gets the value of the block store element at the row position i and the column position j. Notice if the value is -1 means the block is outside the boundary and the block hit the wall.

bool isBlockTouchBlock(Sides sd)
{
	if(sd == BOTTOM)
	{
		
		for(int i=0;i < MATRIX_ROWS;i++)
		{
			for(int j=0;j < MATRIX_COLS;j++)
			{
				if(single_block[i][j]  >  0)
				{
					if(getBlockStoreMatrixElement(i+1,j)  >  0 || getBlockStoreMatrixElement(i+1,j) == -1) return true;
				}
			}
		}
	}
	else if(sd == RIGHT)
	{
		for(int i=0;i < MATRIX_ROWS;i++)
			{
				for(int j=0;j < MATRIX_COLS;j++)
				{
					if(single_block[i][j]  >  0)
					{
						if(getBlockStoreMatrixElement(i,j+1)  >  0 || getBlockStoreMatrixElement(i,j+1) == -1) return true;
					}
				}
			}
	}
	else if(sd == LEFT)
	{
		for(int i=0;i < MATRIX_ROWS;i++)
		{
			for(int j=0;j < MATRIX_COLS;j++)
			{
				if(single_block[i][j]  >  0)
				{
					if(getBlockStoreMatrixElement(i,j-1)  >  0 || getBlockStoreMatrixElement(i,j-1) == -1) return true;
				}
			}
		}
	}
	return false;
}

The isBlockTouchBlock function takes one side of the game screen and check if the moving block hits this side or not. If the side is the BOTTOM we check the current position of the moving block with the next row position of the block store matrix, if value in that position in the block store is greater than zero this means it has a block and the moving block hit it from the BOTTOM. Also the hitting will occur if the next position in the block store matrix is out of the boundary (hits the bottom boundary)
The same concept can be applied to the remaining sides.

	bool isBlockStoreFull()
	{
		for(int j=0;j < MATRIX_COLS;j++)
		{
			//if the first row has blocks
			if(block_store[0][j]  >  0) return true;
		}
		return false;
	}

The isBlockStoreFull checks the first row in the block store matrix, if one of its values is greater than zero this means it has a block and the block store is full or Game Over.

	bool isCompleteRow(int row)
	{
		for(int j=0;j < MATRIX_COLS;j++)
		{
			//if you find an empty spot return false
			if(block_store[row][j] == 0) return false;
		}
		pcResult+=4;
		return true;
	}

The isCompleteRow checks the entire row values if one of them is zero it will return false otherwise it will return true. The player score will be increased by 4 since the entire row is 16 and the block has four elements, so by completing one row means completing 4 blocks.

	void shiftBlocksDown()
	{
		for(int i=0;i < MATRIX_ROWS;i++)
		{
			if(isCompleteRow(i))
			{
				//do the shift starting from the row 
				//which is full moving backwords to 
				//reach the top of the matrix
				for(int k=i;k > 0;k--)
				{
					//first clear the full row
					for(int j=0;j < MATRIX_COLS;j++)
					{
						block_store[k][j]=0;
					}
					//second: move the data of the above row to
					//the full row
					for(j=0;j < MATRIX_COLS;j++)
					{
						block_store[k][j]=block_store[k-1][j];
					}
				}
			}
		}
	}

The shifBlocksDown function uses the isCompletRow function and iterates all rows, if the row is completed it starts from this position k=i until k=0. In other words it will start from the full row and move up row by row until it reaches row 0. In every iteration, it clears the current row and copies the upper row values to the current row.

	void DrawBlockStoreMatrix(void)
	{
		int unit=game_settings.getGameRectLength();
		//this function to scan the blocks_store matrix and
		// if find an element not zero, so it will draw a single
		// rectangle "block element" in the corrisponding pos.

		int combined_element;	
		for(int i=0;i < MATRIX_ROWS;i++)
			for(int j=0;j < MATRIX_COLS;j++)
			{
				combined_element=block_store[i][j]+single_block[i][j];
				
				if(combined_element!=0 )
				{
					 
					RECTANGLE	temp_rect={6*unit+j*unit,unit+i*unit,6*unit+j*unit+unit,unit+i*unit+unit};
					paint_background.DrawTexturedRectangle(temp_rect,combined_element+4);
				}
			}
	}

The DrawBlockStoreMatrix is mainly for drawing the block store matrix to the screen, it checks to see if the element is greater than zero is draw the corresponding element with the required texture.

	void addBlockToBlockStore()
	{
		for(int i=0;i < MATRIX_ROWS;i++)
		{
			for(int j=0;j < MATRIX_COLS;j++)
				block_store[i][j]=block_store[i][j]+single_block[i][j];
		}
	}

The addBlockToBlockStore adds the current moving block to the block store matrix.

	void UpdateSingleBlockMatrix(Block *current_block)
	{
		//get the block info
		int row =current_block- > getRow();
		int col =current_block- > getCol();
		int rotation=current_block- > getRotation();
		int shape_value=current_block- > getShape();
		//set all elements to be all zeroes
		ClearSingleBlockMatrix();
		switch(shape_value)
		{
		case 1: //T shape
			if(rotation == 1)
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col-1,shape_value);
				setSingleBlockElement(row-1,col+1,shape_value);
			}
			else if(rotation == 2)
			{
				// this shape   |-
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col+1,shape_value);
				setSingleBlockElement(row-2,col,shape_value);
			}
			else if(rotation == 3)
			{
				// this shape   _|_
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row,col+1,shape_value);
				setSingleBlockElement(row-1,col+1,shape_value);
				setSingleBlockElement(row,col+2,shape_value);
			}
			else 
			{
				// this shape   -|
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col-1,shape_value);
				setSingleBlockElement(row-2,col,shape_value);
			}
			break;
		case 2: //L shape
			if(rotation == 1)
			{
				//this shape |_
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row,col+1,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-2,col,shape_value);
			}
			else if(rotation == 2)
			{
				// this shape   ___|
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row,col+1,shape_value);
				setSingleBlockElement(row,col+2,shape_value);
				setSingleBlockElement(row-1,col+2,shape_value);
			}
			else if(rotation == 3)
			{
				// this shape  ''| 
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-2,col,shape_value);
				setSingleBlockElement(row-2,col-1,shape_value);
			}
			else 
			{
				// this shape   |'''''
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col+1,shape_value);
				setSingleBlockElement(row-1,col+2,shape_value);
			}
			break;
		case 3: //Z shape   
			if(rotation == 1)
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row,col+1,shape_value);
				setSingleBlockElement(row-1,col+1,shape_value);
				setSingleBlockElement(row-1,col+2,shape_value);
			}
			else if(rotation == 2)
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col-1,shape_value);
				setSingleBlockElement(row-2,col-1,shape_value);
			}
			else if(rotation == 3)
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row,col+1,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col-1,shape_value);
			}
			else 
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-1,col+1,shape_value);
				setSingleBlockElement(row-2,col+1,shape_value);
			}
			break;
		case 4: //I shape
			if((rotation % 2) == 0)
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row,col+1,shape_value);
				setSingleBlockElement(row,col+2,shape_value);
				setSingleBlockElement(row,col+3,shape_value);
			}
			else
			{
				setSingleBlockElement(row,col,shape_value);
				setSingleBlockElement(row-1,col,shape_value);
				setSingleBlockElement(row-2,col,shape_value);
				setSingleBlockElement(row-3,col,shape_value);
			}
			break;
		case 5: //O shape
			setSingleBlockElement(row,col,shape_value);
			setSingleBlockElement(row-1,col,shape_value);
			setSingleBlockElement(row,col+1,shape_value);
			setSingleBlockElement(row-1,col+1,shape_value);
			break;
		default:
			break;
		}
		
	}

The UpdateSingleBlockMatrix updates the value in the single block matrix according to the fields of the moving block. The function takes a pointer to the moving block and gets the values of all of its fields then it uses the block fields to update the single block matrix. The values in the single block matrix will be different according to the shape value and rotation value.

For example if the shape value is 1 and the rotation value is also 1, we need to draw the “T” shape, we start by updating the pivot element which is the most lower element (the block is rotating around this element) the element is at (row,col) positions. Next we move up one step and draw the next element which is at (row-1,col), next we move left to draw the element to the left and then move right to draw the element at right.

The sequential execution of the game

We have completely explained the building blocks of the game, now it is time to use the entities together and see how to join them to make a running game. The game will be running in a sequence depicted in the following pseudo-code

  • Draw the background according to which state you are in.
  • In state 2
  • Draw the background images
  • Shift blocks down (if complete row) and increase the score of the player
  • If block touches block from the bottom so we have two cases

Case 1: Either this is a game over (the top row is full)
Case 2: Or it is just the block settled down to the bottom of the screen or on the top of the block store, in this case we just need to add the block to the block store.

  • Else move the block down one step.
  • Update the GameMatrix by updating the position of the moving block.
  • Draw the matrix to the screen.
  • In state 1
  • Draw the background images
  • Draw the menu itmes.

The above pseudo-code is depicted by the following code

void display()
{
	
	if(current_state.getState() == 2)
	{
		glClear(GL_COLOR_BUFFER_BIT);
		glLoadIdentity();

		glColor3f(1,1,1);
		paint_background.DrawBackgroundImage();

		glColor3f(0,0,0);
		paint_background.DrawGameSubwindows();

		//if there is a complete row shift down
		game_matrix.shiftBlocksDown();

		if(game_matrix.isBlockTouchBlock(BOTTOM))
		{
			if(game_matrix.isBlockStoreFull())
			{
				//game over 
				current_moving_block.haltGame();
				paint_background.printMessageGameOver();
				pcResult=0;
				game_settings.resetElapsedTime();
				game_matrix.ClearBlockStoreMatrix();
			}
			else
			{
				//if block touches block save the current block
				//to the block store and start new shape
				current_moving_block.resetBlock();
				game_matrix.addBlockToBlockStore();
			}
		}
		else
		{
			current_moving_block.MoveBottom(false);	
		}
		//apply changes of the block pos to the current
		//block
		game_matrix.UpdateSingleBlockMatrix(&current_moving_block);
		glColor3f(1.0f,1.0f,1.0f);
		game_matrix.DrawBlockStoreMatrix();

		glColor3f(1.0f,1.0f,1.0f);
		if(current_moving_block.isHaltMode())
		{
			paint_background.printMessageEsc();
		}
	}
	else if(current_state.getState() == 1)
	{
		//Menu screen
		glClear(GL_COLOR_BUFFER_BIT);
		glLoadIdentity();

		glColor3f(1,1,1);
		paint_background.DrawBackgroundImage();

		glColor3f(0.6,0.3,0.6);
		paint_background.DrawGameMenu();

	}
	else if(current_state.getState() == 3)
	{
		//top score screen
		glClear(GL_COLOR_BUFFER_BIT);
		glLoadIdentity();
	}

	glutSwapBuffers();
}

Notice that the game will be running this way until the first row has block, i.e game over. But, how to make the game interactive? this is done by interacting with the game by the keyboard.
The game can be controlled by pressing one of the following keys:

  • KEY_UP
  • KEY_DOWN
  • KEY_LEFT
  • KEY_RIGHT

KEY_UP will rotate the shape, that if the game in state2 or move the cursor on the menu to the upper item if it is in state1.
KEY_DOWN will force the moving block to move until it touches the bottom screen or touches one block from bottom. If in state 1 it will move the menu cursor to the lower item.
KEY_LEFT will just move the block to right if it is not touches another block from left.
KEY_RIGHT will move the block to the left if it is not touches another block from right.
The code for the above procedure is shown in the following code:

void keyboard_s (int key, int x, int y)
{
        
    switch (key)
    {
        case GLUT_KEY_UP:
			if(current_state.getState() == 2)
			{
				current_moving_block.RotateShape();
			}
			else if(current_state.getState() == 1)
			{
				paint_background.setPrevMenuItem();
			}
        break;
        case GLUT_KEY_DOWN:
			if(current_state.getState() == 2)
			{
				//move the block until it touches other block
				while( ! game_matrix.isBlockTouchBlock(BOTTOM)  )
				{
					current_moving_block.MoveBottom(true);
					game_matrix.UpdateSingleBlockMatrix(&current_moving_block);
				}
				game_settings.increaseElapsedTime();
			}
			else if(current_state.getState() == 1)
			{
				paint_background.setNextMenuItem();
			}
        break;
        case GLUT_KEY_LEFT:
			
				if(! game_matrix.isBlockTouchBlock(LEFT))
				{
					current_moving_block.MoveLeft();
					game_matrix.UpdateSingleBlockMatrix(&current_moving_block);
				}
			
        break;
        case GLUT_KEY_RIGHT:
			if(! game_matrix.isBlockTouchBlock(RIGHT))
			{
				current_moving_block.MoveRight();
				game_matrix.UpdateSingleBlockMatrix(&current_moving_block);
			
			}
        break;
    }
}

The source code:

Click here to download the source code of the Tetris game.