Levels

In this lesson, we will add levels with faster gameplay and more points to the Tetris game. Additionally, we will help the players by adding a Canvas showing the next piece.

When we get better at Tetris, the start speed becomes too easy. Too easy means boring, so we need to increase the level of difficulty. We can do this by increasing the gravity so that the tetromino drops faster.

Let’s start by declaring some constants. First, we can configure how many lines it takes to advance to the next level. Next, we can add an object in which we match each level to the number of milliseconds it takes for each drop:

// const.js
const LINES_PER_LEVEL = 10;

const LEVEL = {  
  0: 800,  
  1: 720,  
  2: 630,  
  3: 550,  
  // ...  
}
Object.freeze(LEVEL);

Another option here could be to increase the level of difficulty when we reach a certain amount of points. When you are done with this lesson, you can try implementing it!

Show levels

We should show the player which level they are currently on. The logic of keeping track and showing levels and lines is the same as for points. We initialize a value for the levels and lines, and when we start a new game, we have to reset them.

We can add it to the account object:

// main .js
let accountValues = {
  score: 0,
  lines: 0,
  level: 0
}

Good thing we made this logic extendable, so it’s easy adding new properties to it.

Reset game

When we finish a game, we need to reset the things that allow us to start the new game from scratch. We certainly need to reset all the values through our account proxy, and the time and board are reset from before.

To make it easier to find these values, we can move them out from the play() function and call the new function from there:

// main.js
function resetGame() {
  account.score = 0;
  account.lines = 0;
  account.level = 0;
  board = new Board(ctx);
  time = { start: performance.now(), elapsed: 0, level: LEVEL[0] };
}

Increasing points

With increasing levels, come more points for line clears. We multiply the points with the current level and add one since we start on level zero.

// board.js
getLineClearPoints(lines) {
    const lineClearPoints =
      lines === 1 ? POINTS.SINGLE : 
      lines === 2 ? POINTS.DOUBLE : 
      lines === 3 ? POINTS.TRIPLE : 
      lines === 4 ? POINTS.TETRIS : 
      0;

    return (account.level + 1) * lineClearPoints;
  }

We reach the next level when the lines are cleared as configured. We also need to update the speed of the level.

// board.js -> clearLines()
if (lines > 0) {
  // Add points if we cleared some lines.
  account.score += this.getLineClearPoints(lines);
  account.lines += lines

  // If we have reached the lines for the next level.
  if (account.lines >= LINES_PER_LEVEL) {
    // Goto next level
    account.level++;

    // Remove lines so we start working for the next level.
    account.lines -= LINES_PER_LEVEL;

    // Increase speed of the game.
    time.level = LEVEL[account.level];
  }
}

Now if we play and clear ten lines, we see the level increase, the points double, and of course, the game starts moving a bit faster.

Next piece

Let’s add something to help us get to the next level, the next tetromino. We add another canvas for this:

<h1>TETRIS</h1>
<p>Score: <span id="score">0</span></p>
<p>Lines: <span id="lines">0</span></p>
<p>Level: <span id="level">0</span></p>
<canvas id="next" class="next"></canvas>

Next, we can repeat the steps we used for our first canvas to get the canvas context and set the size:

const canvasNext = document.getElementById('next');  
const ctxNext = canvasNext.getContext('2d');

// Size canvas for four blocks.  
ctxNext.canvas.width = 4 * BLOCK_SIZE;  
ctxNext.canvas.height = 4 * BLOCK_SIZE;  
ctxNext.scale(BLOCK_SIZE, BLOCK_SIZE);

Now, we need to create some logic for the current piece and the next piece. Let’s add functions to set the next piece and the current piece.

We need to send the correct canvas context to the next piece and clean it before we draw the piece on it:

setNextPiece() {
  const { width, height } = this.ctxNext.canvas;
  this.nextPiece = new Piece(this.ctxNext);
  this.ctxNext.clearRect(0, 0, width, height);
  this.nextPiece.draw();
}

When we need a new piece, function should take the next piece that we already have, and when that is done, we can get a “new next piece.” We also need to set the starting x position so the tetromino starts in the middle:

setCurrentPiece() {
  this.piece = this.nextPiece;
  this.piece.ctx = this.ctx;
  this.piece.x = 3;
  this.setNextPiece();
}

Notice we also need to change the piece’s context so we draw it on the correct canvas.

We add the new context to our board class’s constructor so we can send it in and access it. Next, we initialize the board by creating the next piece and then that the current piece:

class Board {
  constructor(ctx, ctxNext) {
    this.ctx = ctx;
    this.ctxNext = ctxNext;
    this.grid = this.getEmptyBoard();
    this.setNextPiece();
    this.setCurrentPiece();
  }
  // ...
}

// new Board(ctx, ctxNext);

We need to remember to add the next canvas context when we create our board instance so that it’s not undefined.

Now that we have created the piece’s function it will be easy to change the drop() function. Instead of creating a new piece, we call the setCurrentPiece() function:

Now that we see which piece is coming next, we can be a bit more strategic in our gameplay.

Get hands-on with 1000+ tech skills courses.