[Tetris] Update 1 2022/10/3 — Game Interface & Block Display

Concept

For this project, I created a Tetris. Tetris is a simple game, in which users move or rotate blocks to fill a game field. Once a horizontal line is created with blocks, blocks on the line will be removed and the user will gain a certain amount of points. Users will gain points if:

  1. Removes blocks by making a horizontal line of blocks.
  2. Uses hard drop
  3. Extra points if multiple lines are removed at once

Although I am using a high-resolution screen, I wanted to give a “retro” appearance to the game (8bit). To achieve this effect, I used 8-bit style fonts and images.

Tetris (NES) - online game | RetroGames.cz

Code

Coding the game was indeed complex because the game required not only game logic, which I partially implemented, but also a friendly user interface. For this reason, I used 3 js. files, each responsible of running the code (sketch.js), displaying game interface (Page.js), and running game (Game.js).

Before making an actual “gaming” part of the project, I made the game interface of the game first. For this project, I used Press Start 2P font downloaded from https://fonts.google.com/specimen/Press+Start+2P.

1. sketch.js

<2022/10/3> UPDATE 1

This file manages how different pages will be displayed based on the user input. Depending on the user input, the game will show the main page, game page, instruction page, interrupt page, or game.

function draw() {
  background(0);
  if (!escapePressed) {
    //if user did not press ESC
    if (currentPageIndex == 0) {
      //MAIN PAGE
      displayMain();
      if (mySelectorMain != null) {
        //draw selector object
        mySelectorMain.drawSelector();
      } else {
        //if selector is new, create new selector object
        mySelectorMain = new selector(150, 340, 21);
      }
    } else if (currentPageIndex == 1) {
      //START GAME selected
      startGame(); //start game based on Game.js
    } else if (currentPageIndex == 2) {
      //HOW TO PLAY selected
      displayInstructions(); //display instructions
    }
  } else {
    if (currentPageIndex == 1) {
      //when user is in the middle of the game
      displayReturn("EXIT GAME AND RETURN TO MAIN?");
      if (mySelectorESC != null) {
        //draw selector object
        mySelectorESC.drawSelector();
      } else {
        //if selector is new, create new selector object
        mySelectorESC = new selector(230, 310, 21);
      }
    } else if (currentPageIndex == 2) {
      //when user is reading instructions
      displayReturn("QUIT READING AND RETURN TO MAIN?");
      if (mySelectorESC != null) {
        //draw selector object
        mySelectorESC.drawSelector();
      } else {
        //if selector is new, create new selector object
        mySelectorESC = new selector(230, 310, 21);
      }
    }
  }
}

Code above will display appropriate pages based on the current index of the game and ESC input(0: MAIN PAGE, 1: GAME, 2: INSTRUCTION). If ESC is pressed, the game will display RETURN TO HOME PAGE with appropriate texts.

//When keyboard input is given
function keyPressed() {
  if (keyCode == DOWN_ARROW) {
    if (currentPageIndex == 0) {
      /*Main page. Move selector accordingly. Selector will move 
      to HOW TO PLAY when it was previously at START GAME, and vice versa*/
      if (mySelectorMain.y == 340) mySelectorMain.y = 400;
      else mySelectorMain.y = 340;
    } else if (
      (currentPageIndex == 1 || currentPageIndex == 2) &&
      escapePressed
    ) {
      /*if ESC is pressed in the middle of the game or in the instruction,
      move selector accordingly.Selector will move to NO when it was previously 
      at YES, and vice versa*/
      if (mySelectorESC.y == 310) mySelectorESC.y = 370;
      else mySelectorESC.y = 310;
    }
  }
  if (keyCode == UP_ARROW) {
    if (currentPageIndex == 0) {
      /*Main page. Move selector accordingly. Selector will move 
      to HOW TO PLAY when it was previously at START GAME, and vice versa*/
      if (mySelectorMain.y == 340) mySelectorMain.y = 400;
      else mySelectorMain.y = 340;
    } else if (
      (currentPageIndex == 1 || currentPageIndex == 2) &&
      escapePressed
    ) {
      /*if ESC is pressed in the middle of the game or in the instruction,
      move selector accordingly.Selector will move to NO when it was previously 
      at YES, and vice versa*/
      if (mySelectorESC.y == 310) mySelectorESC.y = 370;
      else mySelectorESC.y = 310;
    }
  }
  if (keyCode == ENTER) {
    if (currentPageIndex == 0) {
      //user selects START GAME. Change currentPageIndex to 1
      if (mySelectorMain.y == 340) currentPageIndex = 1;
      //user selects HOW TO PLAY. Change currentPageIndex to 2
      else currentPageIndex = 2;
    } else if (
      (currentPageIndex == 1 || currentPageIndex == 2) &&
      escapePressed
    ) {
      /*ESC is pressed in the middle of the game or in the instruction.
      If user selects YES, change currentPageIndex to 0 (returning to MAIN PAGE);
      if user selects NO, simply return to the current page*/
      if (mySelectorESC.y == 310) {
        //YES
        currentPageIndex = 0;
        escapePressed = false;
      } else {
        //NO
        //user selects NO
        escapePressed = false;
      }
    }
    //Reset selector objects' position (DEFAULT: First option)
    mySelectorMain = null;
    mySelectorESC = null;
  }
  //If ESC is pressed, change escapePressed to true;
  if (keyCode == ESCAPE) {
    if (currentPageIndex != 0) escapePressed = true;
  }
}

Code above deals with user inputs and how the game will react to each  input. For this version of the game, user has an option to select different options using up/down arrows and enter. Variables with name including “selector” are objects of triangles that is shown next to the options. It will be further explained in PAGE.JS section.

2. Page.js

<2022/10/3> UPDATE 1

This file manages how each page will visually displayed to the user. It is where all positions and alignments of window/texts are shown. The page starts with preload() to load fonts and images that will be used through out the game.

function preload() {
  font = loadFont("assets/8Bit.ttf"); //load textfont to p5js
  blockImg = loadImage("assets/block.png");  //load single block unit image to p5js
}

Most of the codes in this page manages positions and alignments of the visual elements for each page (MAIN, GAME, INSTRUCTION, and INTTERUPTION).

//MAIN Page
function displayMain() {
  currentPageIndex = 0;

  //display main title of the game: TETRIS
  setFontStyle(titleFontSize);
  text("TETRIS", width / 2, 250);

  //display options
  setFontStyle(normalFontSize);
  text("START GAME", width / 2, 350);
  text("HOW TO PLAY", width / 2, 410);
}

//Instruction Page
function displayInstructions() {
  setFontStyle(titleFontSize - 40);
  text("HOW TO PLAY", width / 2, 95);

  createWindow(width / 2, 320, 530, 370);

  setFontStyle(normalFontSize);
  text(
    "↑ : ROTATE BLOCKS\n\n\n← : MOVE BLOCKS LEFT\n\n\n→ : MOVE BLOCKS RIGHT\n\n\n↓ : SOFT DROP\n\n\nSPACE : HARD DROP",
    width / 2,
    180
  );

  setFontStyle(normalFontSize - 7);

  if (millis() - currentMillis > 800) {
    currentMillis = millis();
    blink = !blink;
  }

  if (blink) {
    fill(0);
    stroke(0);
  } else {
    fill(255);
    stroke(255);
  }

  text("PRESS ESC TO RETURN", width / 2, 560);
}

//Game
function displayGameBackground(score, level, lines) {
  createWindow(width / 4 + 25, height / 2, width / 2 + 6, height - 60 + 6);

  setFontStyle(normalFontSize);
  text("——— NEXT ———", 462.5, 50);

  //Next blocks
  createWindow(400, 125, 106, 106);
  createWindow(525, 125, 106, 106);

  //Score, level, line cleared
  createWindow(462.5, 385, 231, 376);

  setFontStyle(normalFontSize);
  text("SCORE", 462.5, 245);
  text(score, 462.5, 290); //display score
  text("LEVEL", 462.5, 358.3);
  text(level, 462.5, 403.3); //display level of the round
  text("LINES", 462.5, 481.7);
  text(lines, 462.5, 526.7); //display total # of lines cleared in the round
}

//Return to Main Question Page
function displayReturn(_text) {
  //Create window
  createWindow(width / 2, height / 2, 400, 300);

  setFontStyle(normalFontSize);

  //Text Question
  text(_text, width / 2, 220, 350);

  //Options
  text("YES", width / 2, 320);
  text("NO", width / 2, 380);
}
//default setting of the font
function setFontStyle(size) {
  strokeWeight(1);
  textFont(font);  //set font style
  fill(255);
  textWrap(WORD);  //wrap text
  textAlign(CENTER);  //align text to center
  textSize(size);  //change text size
}

//create a rectangular window where texts will be displayed
function createWindow(x, y, w, h) {
  fill(0);
  stroke(255);  //white border
  strokeWeight(6);  //border weight of the rectangle
  rectMode(CENTER);  //rectangle is centered
  rect(x, y, w, h);
}

setFontStlye(size) and createWindow(x,y,w,h) are functions, where size is textSize, x and y are coordinates of the rectangle’s/window’s center, and w and h are width and height of the window.

3. Game.JS

<2022/10/3> UPDATE 1

This file contains all the game logics and runs Tetris game. The game consists of 2 main parts: block objects and game field array. Block objects are used to display blocks in motion. Game field array is what the program use to visualize the game. Once blocks cannot further be moved, it will be stored into game field array, so no further calculations will be needed in the future.

Game logic follow procedures listed below:

  • Create an empty game field array. (0: empty space, 1: something is present)
board = [];
for (let i = 0; i < boardHeight + 1; i++) {
  board[i] = [];
  for (let j = 0; j < boardWidth; j++) {
    board[i][j] = 0;
  }
}
  • User selects START GAME. Game Starts based on SCRIPT.JS
else if (currentPageIndex == 1) { 
//START GAME selected 
startGame(); 
}
  • Load background layout of the game (shapes and texts) based on PAGE.JS
function startGame() {
  displayGameBackground(0, 1, 0, 1);
  //Code is partially shown for demonstration
}
  • Generate, display, and move block object. Types of the block is randomly generated from the array that contains 7 different block types. Up until now, block will move only vertically down every time interval. When the block reaches bottom, or cannot be further moved, a new random block will be generated.
let blockTypeArr = ["I", "J", "L", "O", "S", "T", "Z"]; //Block Type Array

//GAME
function startGame() {
  displayGameBackground(0, 1, 0, 1); //display background interface

  if (generateBlock) {
    //If new block is needed, create a random shaped block
    myBlock = new block(int(random(0, 11)), 10, random(blockTypeArr), 4);
    generateBlock = false;
  }

  myBlock.drawBlock();
  myBlock.blockDown();
  displayBoard();
}

Most of the game logic is handled by block class below. For more, read the commented lines.

//block object for tetris blocks
class block {
  constructor(_x, _y, _type, speed) {
    this.x = _x;
    this.y = _y;
    this.type = _type;
    this.shapeArr = [];
    this.speed = 1;
  }
  
  /*this class method will fill shapeArr based on the
  type of the block. shapeArr is a 4*4 array, in which
  0: empty, 1: block*/
  shapeType(_type) {
    let shapeArr = [];
    /*example L block
      0 0 0 0
      0 1 0 0
      0 1 0 0
      0 1 1 0*/
    for (let i = 0; i < 4; i++) {
      shapeArr[i] = [0, 0, 0, 0];
    }
    if (_type == "I") {
      shapeArr[0][0] = 1;
      shapeArr[1][0] = 1;
      shapeArr[2][0] = 1;
      shapeArr[3][0] = 1;
    } else if (_type == "J") {
      shapeArr[1][2] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "L") {
      shapeArr[1][1] = 1;
      shapeArr[2][1] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "O") {
      shapeArr[2][1] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "S") {
      shapeArr[2][1] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][0] = 1;
      shapeArr[3][1] = 1;
    } else if (_type == "T") {
      shapeArr[2][1] = 1;
      shapeArr[3][0] = 1;
      shapeArr[3][1] = 1;
      shapeArr[3][2] = 1;
    } else if (_type == "Z") {
      shapeArr[2][1] = 1;
      shapeArr[2][2] = 1;
      shapeArr[3][2] = 1;
      shapeArr[3][3] = 1;
    }
    this.shapeArr = shapeArr;
  }

  //Checks if block can be moved in a given direction
  validMove(dX, dY) {
    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 + 1][this.x + j] != 0 || this.y + i + dY >= boardHeight ||
            this.x + j + dX < 0 || this.x + j + dX >= boardWidth)
        ) {
          return false;
        }
      }
    }
    return true;
  }

  //Move block down for every time interval
  blockDown() {
    if (this.validMove(0, 1)) {
      if (millis() - counter > 400 / this.speed) {
        this.y++;
        counter = millis();
      }
    } else {
      //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;
    }
  }

  //Move Block left or right. left when direction is -1, right when direction is 1
  blockLeftRight(dX) {
    if (direction == -1) {
    }
  }

  //draw blocks. for each 1s in the shapeArr, block image will be placed
  drawBlock() {
    this.shapeType(this.type);
    blockImg = blockImg.get(0, 0, 20, 20);
    push();
    translate(25, 30);
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        if (this.shapeArr[i][j] == 1)
          image(blockImg, blockSize * (this.x + j), blockSize * (this.y + i));
      }
    }
    pop();
  }
}

//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);
    }
  }
  pop();
}

Future Improvements

There are tons of more things to implement in this game:

    1. Moving blocks horizontally using key inputs.
    2. Rotating blocks using key inputs.
    3. This means more logic must be added to the movement validation method.
    4. Add conditions that will check if game is over or not.
    5. Add conditions that will check if user had made a horizontal line using blocks. Then, the program must be able to remove that line and shift whole game filed down by the number of removed lines unit(s).
    6. The game will not reset even if user returns to the main page in the middle of the game. This can be fixed by resetting every variable at the start of each game.

Leave a Reply