[Tetris] Update 2 2022/10/10 — Final Version

Concept

For this project, I have created an 8-bit style Tetris game. The general concept of the game is similar to the original game, but there are two additional features in this game. First, the game sometimes generates a random block that does not look like a traditional Tetris block. The block may consist of more than 5 blocks and some empty spaces. This is a challenge that this game gives to the players. Secondly, there is a bomb block that removes all blocks. If the bomb is in row n, it will clear all blocks from row n-2 to n+2, removing 5 lines total. This feature is added because randomized blocks may be too difficult for the users. For additional concepts, please refer to my previous posts.

Code

**note: I will only discuss codes that are new or different from the previous post I made on Tetris. I recommend reading the previous first and coming back.**

For update 2, I started off by fixing these one minor bug:

  • Block cannot be moved as soon as it reaches the bottom.

This happened because the code generates new block as soon as the block touches the ground (when block cannot go further down). In order to solve this, I added a condition which the code will wait for the new keyboard input if it cannot go further down for a specific amount of time. If no key input is given in that time range, the code will then generate the block. Bolded code shows the new lines that I have added.

if (millis() - counterBlockDown > 1000 / this.speed) {
        fixBlock = true;
        counterBlockDown = millis();
      }
      if (fixBlock) {
        //Once block cannot be moved down further, update game field
        for (let i = 0; i < 4; i++) {
          for (let j = 0; j < 4; j++) {
            if (this.shapeArr[i][j] == 1) board[this.y + i][this.x + j] = 1;
          }
        }
        //generate new block
        generateBlock = true;
        fixBlock = false;
      }
    }

Not to mention, there are tons of new features that I added in this update:

  • Soft drop

//soft drop
if (keyIsDown(DOWN_ARROW)) {
  if (myBlock != null) myBlock.speed = blockSpeed * 3;
} else {
  if (myBlock != null) myBlock.speed = blockSpeed;
}

When down arrow key is pressed, the block will move down with the speed that is 3 times its initial speed. Not to mention, block’s speed is in variable because initial speed will increase as level goes up.

  • Hard drop

When spacebar key is pressed, the block will instantly move down to the bottom. Notice that I used 32 for keyCode because spacebar is not mapped in p5js.

//Hard drop when spacebar pressed
if (keyCode == 32) {
  if (myBlock != null) {
    let yPos = myBlock.y;
    while (myBlock.validMove(0, 1, 0)) {
      myBlock.y = yPos;
      yPos++;
    }
    hardDropMusic.play();
  }
}

For  this, additional calculation is done to find the lowest possible position for the block. The program will essentially start from the lowest position and see if the block can fit there. If block cannot be fitted, the block will moved 1 unit up to see if it fits there. This process repeated until the block finally fits.

  • Rotation

rotate() {
    for (let i = 0; i < 2; i++) {
      for (let j = i; j < 4 - i - 1; j++) {
        // Swap elements of each cycle
        // in clockwise direction
        let temp = this.shapeArr[i][j];
        this.shapeArr[i][j] = this.shapeArr[3 - j][i];
        this.shapeArr[3 - j][i] = this.shapeArr[3 - i][3 - j];
        this.shapeArr[3 - i][3 - j] = this.shapeArr[j][3 - i];
        this.shapeArr[j][3 - i] = temp;
      }
    }
  }

  //rotate block with condition check
  rotateBlock() {
    if (!this.validMove(0, 0, 1)) {
      for (let i = 0; i < 3; i++) this.rotate();
    }
  }

rotate() uses a rotation function that I have found in:
https://www.geeksforgeeks.org/rotate-a-matrix-by-90-degree-in-clockwise
-direction-without-using-any-extra-space
.

In order to rotate blocks, there must be additional condition that will check if the rotation is a valid movement at the given position of the block. Bolded code shows the new condition I have added to do this.

//Checks if block can be moved in a given direction
  validMove(dX, dY, dR) {
    if (dR == 0) {
      for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) { if (this.y + i >= 0 && this.x + j >= 0) {
            if (
              /*ignores all empty spaces. 1s must always be within the 
            boundary of game field and must not overlap with non-empty,
            or 1s, when moved*/
              this.shapeArr[i][j] != 0 &&
              (board[this.y + i + dY][this.x + j + dX] != 0 ||
                this.y + i + dY >= boardHeight ||
                this.x + j + dX < 0 || this.x + j + dX >= boardWidth)
            ) {
              if (this.shapeArr[i][j] == 2){
                this.explode();
              }
              return false;
            }
          }
        }
      }
    } else {
      this.rotate();
      if (this.y >= 0) {
        for (let i = 0; i < 4; i++) {
          for (let j = 0; j < 4; j++) {  if ( /*ignores all empty spaces. 1s must always be within the boundary of game field and must not overlap with non-empty, or 1s, when moved*/ this.shapeArr[i][j] == 1 && (board[this.y + i][this.x + j] != 0 || this.y + i >= boardHeight ||
                this.x + j < 0 || this.x + j >= boardWidth)
            ) {
              return false;
            }
            if (this.shapeArr[i][j] == 2) return false; //bomb cannot be rotated
          }
        }
      } else return false;
    }
    return true;
  }

validMove() will first check if the block is being rotated or translated. If block is being rotated, i.e. third parameter is 1, it will first rotate the block and check if the block is not overlapped with other blocks and is within the width and height of the game field. If block satisfies all conditions, validMove() returns true. Else, it returns false. Since validMove() actually rotates block to check condition, rotateBlock() will rotate block 3 more times to reset the block status if no rotation can be made.

  • Random block

As level goes up, the game will start to generate random a 4 by 4 block that may contain empty spaces like such:

Each time a random block is generated, the block will have randomized shape. This feature was added to increase the difficulty of the game and make the game more interesting/different every time.

else if (_type == "R") {
      //Random 4*4 block
      for (let i = 0; i < 4; i++) {
        shapeArr[i] = [
          int(random(0, 1) + 0.5),
          int(random(0, 1) + 0.5),
          int(random(0, 1) + 0.5),
          int(random(0, 1) + 0.5),
        ];
      }
    }

Block object now has new type called R.

let blockType = random(0, 10);

if (blockType <= 11 - level * 0.5)
      myBlock = new block(5, -4, random(blockTypeArr), blockSpeed);
    else myBlock = new block(5, -4, random(specialTypeArr), blockSpeed);

This is a updated condition for generating block.  blockType is a random number between 0 and 10. Regular blocks will be generated if blockType is less or equal to 11- level * 0.5 and random blocks or bomb (will be discussed in the next section) will be generated else. Note that  the probability of getting random blocks or bomb increases as level goes up. For this reason, random block and bomb will only appear from level 3.

  • Bomb

Bomb is a special type of block that will remove blocks nearby. If block is at row n, it will remove all blocks from row n-2 to row n+2.

else if (_type == "bomb") {
      for (let i = 0; i < 4; i++) {
        shapeArr[3][2] = 2;
      }
    }

/*explode bomb block. if bomb is at (x,y), it will destroy every row
from x-2 to x+2*/
explode() {
  let go = true;
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (this.shapeArr[i][j] == 2 && go) {
        go = false;
        for (let k = this.y + i - 2; k <= this.y + i + 2; k++) {
          if (k >= 0 && k < boardHeight) {
            for (let l = 0; l < boardWidth; l++) {
              board[k][l] = -1;
            }
          }
        }
      }
    }
  }
}

Block object now has new type called bomb. Chance of generating random block and bomb is both 50%.

  • Line Clearing

Every time a game field is updated, the program will look for any completed lines. If there is a completed, the game field will be updated. The update will make game field to hold value -1 instead of 1.

//Check if there is a line cleared
function lineClear() {
  for (let i = 0; i < boardHeight; i++) {
    if (board[i][0] == 1) {
      for (let j = 0; j < boardWidth; j++) {
        if (board[i][j] == 1) {
          if (j == boardWidth - 1) {
            for (let k = 0; k < boardWidth; k++) board[i][k] = "-1";                                
              counterLineClear = millis(); 
          } 
        } else break; 
      } 
    } 
  } 
}

Because the game filed can now hold -1, I added new condition to make sure the game can display -1s as a grey block. Then, the game filed will start to remove grey blocks line by line at each frame. This code is shown as bold code below.

//visually display the gameboard.
function displayBoard() {
  blockImg = blockImg.get(0, 0, 20, 20);
  push();
  translate(25, 30);
  for (let i = 0; i < boardHeight; i++) {
    for (let j = 0; j < boardWidth; j++) { 
      if (board[i][j] == 1) image(blockImg, blockSize * j, blockSize *      
      i); 
      else if (board[i][j] == -1) { image(greyBlockImg, blockSize * j,       
      blockSize * i); 
        removeLine(i); 
      }
    } 
  } 
  pop(); 
} 

function removeLine(index) { if (millis() - counterLineClear > 400) {
    for (let i = index; i > 1; i--) {
      for (let j = 0; j < boardWidth; j++) {
        board[i][j] = board[i - 1][j];
      }
    }
    lineClearMusic.play();
    lines++;
    score += scorePerLine;
    counterLineClear = millis();
  }
}
  • Score, level, and line cleared counting

For each line cleared, the game will update score, level, and line cleared displayed on the right side. For each line cleared, the score will increase more as level goes up. Level increases by 1 for each 10 lines cleared. As mentioned above, block speed will also increase as level goes up.

  scorePerLine = int(level * 0.5) + 1;
  level = int(lines / 10) + 1;
  blockSpeed = int(level * 1.25) + 2;
  • Game Over

A condition that checks if game is over. If the position of non-empty block unit is below game field boundary, the game is over.

else {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (this.y + i < 0 && this.shapeArr[i][j] == 1) {
        gameOver = true;
        break;
      }
    }
  }
...

This  condition is within blockDown() method of the block object.

  • Sound effect

Last feature that was added to this game is sound effect for background music, the block is moved and soft/hard dropped, and game over. Code will not be demonstrated because the codes for sound effect is scattered throughout the entire program.

Future Improvements

Overall, I am very satisfied with I have made for this project, but here are some things that I would love to fix in the future:

  1. I realized when this program is played with chrome browser, the sound effect will slow down as time progresses. I found out this is the problem with the chrome itself.
  2. Add settings page so players can change sound effects.
  3. Due to the time limit, I failed to include a feature which shows next 2 blocks that will show up in the game. I would love to add this feature in the future.
  4. Include hold feature so user can hold the block and use it later when needed.
  5. The rotation of the blocks is not centered because rotation is done as if I rotating a matrix.

Leave a Reply