Step by step Tetris on ES6 and Canvas
Tags: javascript, canvas, games, collab
Published: 2020-09-05 21:47:49

Some time ago I wrote a post called [a very _classy_ snake](https://dev.to/oinak/a-very-classy-snake-8e3), inspired by a YouTube video and to try to touch on ES6, canvas and game programming basics.

Shortly afterwards, as it usually does, _youtube_ started suggesting similar videos, and I found myself looking at this [tetris on C++](https://www.youtube.com/watch?v=8OK8_tHeCIA) video. C++ is not my thing lately, but I wanted an excuse to play some more with ES6 and canvas, so I though, why not combine the teachings from both videos to create a canvas tetris?


![Both](https://media.tenor.com/images/0e696d9152a0ee1c4c2c850cc9c581e9/tenor.gif)

1. [Boilerplate](#boilerplate)
2. [Playing Field](#playing-field)
3. [A single piece](#a-single-piece)
4. [Movement and collision](#movement-and-collision)
5. [Touchdown and new piece](#touchdown-and-new-piece)
6. [Clearing lines and scoring](#clearing-lines-and-scoring)

## 1. Boilerplate

In the begining, I just copied the html from the snake game, changing just the canvas dimensions to the proportions of the tetris pit (taken from the research the pal from the video did, and by _research_ I mean he counted the squares on a GameBoy, so I did not have to :-)

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>repl.it</title>
    <link href="style.css" rel="stylesheet" type="text/css" />
  </head>
  <body>
    <canvas id='field' width='240' height='360'>Loading...</div>
    <script src='script.js'></script> 
    <script>
      window.onload = () => { let game = new Game({canvasId: 'field'}); };  
    </script>
  </body>
</html>
```
Once we have this, we will copy over the skeleton of a game. What do I mean from _skeleton_. Most classic games have a very similar scaffolding, this is:

1. Capture user input
2. Calculate the new game state
3. Redraw the game GUI based on the new state

This is usually called _The game loop_ because it was, you guessed it, implemented into an infite loop, broken only by win and loss conditions.

As we are in javascript, we are making a slightly more asynchronous version of this, reading user inputs out of events, and executing the state recalculation and the screen redrawing with a `setInterval`.

```js
// jshint esnext:true

class Game {
  constructor({ canvasId }){
    // this.field = new Field({...});
    this.init();
  }

  init(){
    addEventListener('keydown', (e) => { this.userInput(e) } ); // User input
    setInterval( () => { this.game() }, 1000 / 8);              // Game loop 
  }

  userInput(e){
    // nothing here yet
  }
  
  game(){
     // nothing here yet
  }
}
```
Once you have this, you only have to fill in the gaps, and it is as easy as drawing an owl:

![Drawing an owl](https://thepracticaldev.s3.amazonaws.com/i/3y2pei0n5uk35dahltah.jpg)

## 2. Playing field

Now let's go for something you will be able to see at last. To that end, there are two bits we will rescue from the snake game:

* First, the canvas initialisation code:
```js
    let canvas = document.getElementById(canvasId);
    this.context = canvas.getContext('2d');
```

* Then the code to draw a single square on our imaginary grid:
```js
  // Draw a single tile (using canvas primitives)
  drawTile(x, y, color){
    this.context.fillStyle = color;
    this.context.fillRect(
      x * this.size,  // x tiles to the rigth
      y * this.size,  // y tiles down
      this.size - 1,  // almost as wide as a tile
      this.size - 1); // almost as tall
  }
```
We are using the [fillRect primitive](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillRect), it can only draw rectangles, but our Tetris game will have a _fat pixels_ aesthetic, so that will be enough for us.

We will create a new class, in charge of holding the game state and drawing the background screen.

```js
class Field{
  constructor({width, height, size, canvasId}){
    this.width = width;   // number of tiles sideways 
    this.height = height; // number of tiles downward
    this.size = size;     // size of a tile in pixels

    this.init(canvasId);  // initialize the field
  }

  init(canvasId){
    // first, set up the canvas context:
    let canvas = document.getElementById(canvasId);
    this.context = canvas.getContext('2d');

    // then set up the grid
    this.initTileGrid();
  }

  // Create the original grid of tiles composed of void and walls
  initTileGrid(){
    this.tiles = []; // a list of columns
    for(let x = 0; x < this.width; x += 1) {
      this.tiles[x] = []; // a single column
      for(let y = 0; y < this.height; y +=1 ) {
        this.tiles[x][y] = this.isWall(x, y) ? 'w' : ' ';
      }
    }
  }

  // Are these x,y coordinates part of a wall?
  // use for drawing and for wall-collision detection  
  isWall(x, y){
    return (x === 0 ||          // left wall
      x === (this.width - 1) || // right wall
      y === (this.height-1));   // floor
  }

  // For every tile in the grid, drwa a square of the apropriate color
  draw(){
    for(let x = 0; x < this.width; x += 1) {
      for(let y = 0; y < this.height; y +=1 ) {
        this.drawTile(x, y, this.colorFor(this.tiles[x][y]));
      }
    }    
  }

  // Draw a single tile (using canvas primitives)
  drawTile(x, y, color){
    this.context.fillStyle = color;
    this.context.fillRect(
      x * this.size,  // x tiles to the right
      y * this.size,  // y tiles down
      this.size - 1,  // almost as wide as a tile
      this.size - 1); // almost as tall
  }

  // Relate grid cell content constants with tile colors
  colorFor(content){
    let color = { w: 'grey' }[content];
    return color || 'black';
  }
}
```
This is ready to roll, but the Game class is not yet referring to it, so we need to do this small changes:

```js
class Game {
  constructor({ canvasId }){
    this.field = new Field({
      width: 12,         // number of tiles to the right
      height: 18,        // number of tiles downwards
      size: 20,          // side of the tile in pixels
      canvasId: canvasId // id of the cavnas tag
    });
    this.init();
  }

  // ... the rest remains unchanged
}
```
Once you have, you should be able to see something like this:

![The playing field](https://thepracticaldev.s3.amazonaws.com/i/f672zn5tt7o6y42xswkz.png)

**Things to observe:**

> We are using _cartesian_ (that is 2D) coordinates, but we are starting the count at the top-left corner of the canvas, because that is what canvas functions do, and it saves us from some conversion math.

## A single piece

A tetris piece or, as I learnt in the video, a _tetronimo_ can be represented as a 4x4 binary matrix of full and empty spaces.

```
// If you squint you see the 'L' piece:
[[' ','L',' ',' '],
 [' ','L',' ',' '],
 [' ','L','L',' '],
 [' ',' ',' ',' ']]
```
But if we concatenate those 4 lists it can be simplified as a list:
```js
[' ','L',' ',' ',' ','L',' ',' ',' ','L','L',' ',' ',' ',' ',' ']
```
where you use `(x,y) => { list[4*y + x] }` to _see_ each position as a cell.
And javascript being weakly typed allows you to do this with a string as well:
```js
' L   L   LL     '
```
The video uses A,B,C... letters to refer to (and draw) the pieces, I prefer to use the letters that remind me most of the tetromino's shape, thus the 'L' here.

Pieces have three main motions, sideways, downward and rotation. Sideways and downward motions can be easily figured adding units to the coordinates, so we will deal first with the more complex one, _rotation_.

### Rotation:

Let's draw the numbered positions from our strings in the position they will have in the 4x4 grid, and then, figure out (or copy from the video ;-) the math to have a matrix rotation:

```js
var grid = [
  0,  1,  2,  3,
  4,  5,  6,  7,
  8,  9,  10, 11,
  12, 13, 14, 15
];

var newGrid = [];
for (let i0 = 0; i0 < 16; i0++){
    // convert to x/y
    let x0 = i0 % 4;
    let y0 = Math.floor(i0 / 4);

    // find new x/y
    let x1 = 4 - y0 - 1;
    let y1 = x0;

    //convert back to index
    var i1 = y1 * 4 + x1;
    newGrid[i1] = grid[i0];
}

console.log(newGrid);
// [12, 8,  4, 0,
//  13, 9,  5, 1,
//  14, 10, 6, 2,
//  15, 11, 7, 3]
```

If you do this with a piece represented as a string you get:

```js
var grid = '  I   I   I   I '; 
// Visual help: this is the above as a 4x4 grid:
// [" ", " ", "I", " ",
//  " ", " ", "I", " ",
//  " ", " ", "I", " ",
//  " ", " ", "I", " "]

var newGrid = [];
for (let i0 = 0; i0 < 16; i0++){
    // convert to x/y
    let x0 = i0 % 4;
    let y0 = Math.floor(i0 / 4);

    // find new x/y
    let x1 = 4 - y0 - 1;
    let y1 = x0;

    //convert back to index
    var i1 = y1 * 4 + x1;
    newGrid[i1] = grid[i0];
}

console.log(newGrid);
// [" ", " ", " ", " ",
//  " ", " ", " ", " ",
//  "I", "I", "I", "I",
//  " ", " ", " ", " "]

console.log(newGrid.join(''));
// "        IIII    "
```

Let's build a new `Piece` class with this logic in it:


```js
class Piece{
  constructor({variant, x, y}){
    this.x = x;
    this.y = y;
    this.contents = this.variants()[variant];
  }
  variants(){
    return { // 16 chars = 4x4 char grid
      i: '  i   i   i   i ', // 4x1 column
      t: '  t  tt   t     ', // short 'T' shape
      l: ' l   l   ll     ', // L (short arm right)
      j: '  j   j  jj     ', // J (sort arm left)
      o: '     oo  oo     ', // square, centered or rotation would displace
      s: '  ss ss         ', // step climbing right
      z: ' zz   zz        '  // step climbing left
    };
  }

  rotate(){
    let newGrid = [];
    for (let i0 = 0; i0 < 16; i0++){
      // convert to x/y
      let x0 = i0 % 4;
      let y0 = Math.floor(i0 / 4);

      // find new x/y 
      let x1 = 4 - y0 - 1;
      let y1 = x0;

      //convert back to index
      var i1 = y1 * 4 + x1;
      newGrid[i1] = this.contents[i0];
    }
    this.contents = newGrid.join('');
  }

  reverse(){ // 1/4 left = 3/4 right
    rotate();
    rotate();
    rotate();
  }

  toString(){
    return [this.contents.slice(0 , 4),
     this.contents.slice(4 , 8),
     this.contents.slice(8 , 12),
     this.contents.slice(12, 16)].join("\n"); 
  }
}

let p = new Piece({variant: 'l', x: 5, y: 0})
console.log(`----\n${p.toString()}\n----`);
p.rotate();
console.log(`----\n${p.toString()}\n----`);
p.rotate();
console.log(`----\n${p.toString()}\n----`);
p.rotate();
console.log(`----\n${p.toString()}\n----`);
```
If you run this code, you get this output:
```js

"----
 L  
 L  
 LL 
    
----"
"----
    
 LLL
 L  
    
----"
"----
    
 LL 
  L 
  L 
----"
"----
    
  L 
LLL 
    
----"
```
Can you see the 'L' piece rotating clockwise?

The `.toString()` method is not needed for the game logic but it is useful for debugging, feel free to leave it there if it helps you.

Next step: draw it onto the canvas. The drawing logic is on the `Field` so we are going to add a method to draw the current piece.

*Changes to `Field`*

Initialize the current Piece:
```js
  init(canvasId){
    // (...) the rest of the method unchanged (...)
    this.currentPiece = new Piece({x: 4,y: 0});
  }
```

The `draw` method:
```js
  // For every tile in the grid, draw a square of the apropriate color
  draw(){
    // (...) the rest of the method unchanged (...)
    this.drawPiece(this.currentPiece);
  }
```

And a new  `drawPiece` function: 
```js
  drawPiece(piece){
    let tile = ' ';
    for(let x = 0; x < 4; x += 1){
      for(let y = 0; y < 4; y += 1){
        tile = piece.at(x,y) 
        if (tile !== ' '){
          this.drawTile(piece.x + x,
                        piece.y + y,
                        this.colorFor(tile));
        } // non empty
      } // column tiles
    } // piece columns
  }
```
As you see, we are still using the `colorFor` method to choose the color of the tiles, so now we need a coor for every piece, so we go [to the Tetris page on wikipedia](https://en.wikipedia.org/wiki/Tetris) to choose them:

![Tetrominoes](https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/Tetrominoes_IJLO_STZ_Worlds.svg/500px-Tetrominoes_IJLO_STZ_Worlds.svg.png)

```js
  // Relate grid cell content constants with tile colors
  colorFor(content){
    let color = {
      w: 'grey',
      i: 'lightblue',
      t: 'lightgreen',
      l: 'orange',
      j: 'blue',
      o: 'yellow',
      s: 'lime',
      z: 'red'
    }[content];
    return color || 'black';
  }
```
The final version of the `Piece` class has the ability to randomly choose a variant upon initialisation:

```js 

class Piece{
  constructor({x, y}){
    this.x = x;
    this.y = y;
    this.contents = this.chooseVariant();
  }

  // changed from variants to this, with the random logic
  chooseVariant(){
    // https://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
    let variants = {
      i: '  i   i   i   i ', // 16 chars = 4x4 char grid
      t: '  t  tt   t     ',
      l: ' l   l   ll     ',
      j: '  j   j  jj     ',
      o: '     oo  oo     ', // centered or rotation would displace
      s: '  ss ss         ',
      z: ' zz   zz        '
    };
    let keys = Object.keys(variants);
    return variants[keys[ keys.length * Math.random() << 0]]; // << 0 is shorcut for Math.round
  }
  
  at(x, y){
    return this.contents[(y * 4) + (x % 4)];
  }
  
  rotate(){
    let newGrid = [];
    for (let i0 = 0; i0 < 16; i0++){
      // convert to x/y
      let x0 = i0 % 4;
      let y0 = Math.floor(i0 / 4);

      // find new x/y 
      let x1 = 4 - y0 - 1;
      let y1 = x0;

      // convert back to index
      var i1 = y1 * 4 + x1;
      newGrid[i1] = this.contents[i0];
    }
    this.contents = newGrid.join('');
  }
  
  reverse(){ // 1/4 left = 3/4 right
    rotate();
    rotate();
    rotate();
  }
}
```
Once you have this code in place, you should be able to see something like this:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/80c7c5o6a3ql1egf7dx5.png)

Bear in mind it probably chose a different tetromino for you, and will choose a random one every time you run the code.

## Movement and collision

Now that we have a Playing field, and a piece on it, it is time to get interactive, so we are going to listen to player input and react to it.

Also we have walls, and they would not be worth such name it stuff just went through, right?.

So this is the strategy for this section:
1. Read user input
2. Create a displaced or rotated version of the piece
3. Check if the virtual piece fits (does not collide)
  - If it fits, it **becomes** the current piece
  - If it does not, movement gets blocked (for now, we will see what else later)

### Read user input

I am going to be totally lazy here and copy over from the snake game:
```js
  // on Game class
  userInput(event){
    const arrows = { left: 37, up: 38, right: 39, down: 40};
    const actions = {
      [arrows.left]:  'moveLeft',
      [arrows.up]:    'rotate',
      [arrows.right]: 'moveRight',
      [arrows.down]:  'moveDown'
    }
    if (actions[event.keyCode] !== undefined){ // ignore unmapped keys
      this.field.handle(actions[event.keyCode]);
    }
  }
```

### Create the virtual piece (we make it accept `contents` for this)

There is no deep cloning out of the box on ES6 so we just initialise a new Piece with the same properties and then apply the motion indicated by the user's input:

`Piece` class:

```js
class Piece{
  constructor(options = {}) {
    const defaults = { x: 0 , y: 0, contents: null };
    Object.assign(this, defaults, options);

    // accept contents for piece copying, select random for new pieces:
    this.contents = this.contents || this.chooseVariant();
  }

  chooseVariant(){
    // unmodified
  }

  //// User actions:
  moveRight(){
    this.x += 1;
  }
  
  moveLeft(){
    this.x -= 1;
  }
  
  moveDown(){
    this.y += 1;
  }

  rotate(){ 
    // unmodified
  }

  // return a copy of the object:
  dup(){
    return new Piece({x: this.x, y: this.y, contents: this.contents});
  }

```

And now the `handle` method in the `Field` class:
```js
  handle(action){
    // make a copy of the existing piece:
    let newPiece = this.currentPiece.dup();

    // effect the user selected change on the new piece:
    newPiece[action]();

    // temporal, to see the effect:
    this.currentPiece = newPiece;
    this.draw();
  }
```

After this, you should be able to move your piece sideways and downwards, but alas, it does not stop on walls.


### Detect collision

This `handle` function is not very smart, so we are going to add a check to see if a piece *can fit* in the place we are trying to send it to, before effectively doing the move:

```js
  handle(action){
    // make a copy of the existing piece:
    let newPiece = this.currentPiece.dup();

    newPiece[action](); // move or rotate according to userInput

    if (this.canFit(newPiece)){
      this.currentPiece = newPiece;
    } else {
      console.log('colision!');
      // touchDown?
    }
    this.draw();
  }
```
This is very similar to what we have before, but now, how do we know if the piece can indeed fit. We don't need 4x4 tiles free because tetronimos do not occuppy their full grid, to achieve the puzzle effect we only want to check if every tile on the piece grid is either empty on the piece or on the field, in either case there is no collision. Collosions happen when a non-empty cell from the piece is atop a non-empty cell of the field.

Let's translate all this jargon to code:
```js
  canFit(piece){ // for every overlap tile between the piece and the field:
    for(let x = 0; x < 4; x++){
      for(let y = 0; y < 4; y++){
        if (piece.at(x, y) !== ' ' &&                      // piece is not empty
            this.tiles[piece.x + x][piece.y + y] != ' ' ){ // field is not empty
          return false; //there is collision
        }
      }
    }
    return true; // if there are no collisions, it can fit
  }
```

After this, you can still move your pieces, but no longer overlap them with the walls or floor. The `console.log('collision!')` will be executed every time you go over a wall or the floor, but the piece won't move.

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/clg79fe4j0lxzp5ihku2.png)

Before going on, I noticed that the rotations had a strange symmetry. This is, the pieces rotating around different axis from what they do on the original game. First I fixed this on the square, going:
```
From this:    To this:
'oo  '        '    '
'oo  '        ' oo '
'    '        ' oo '
'    '        '    '
```
But that trick did not work for every piece. So I dug deeper, and I noticed I felt _uncomfortable_ about the literal 4's sprinkled all over the code, so I thought: what if different pieces are different sizes?

So I made these changes to `Piece`:

* Added a `length` and a `side` _getters_ to `Piece`, to use instead of 16 and 4 throughout the code.
* Edited every method using the Piece's length or side to use the new attributes.
* Once everything was working again, I changed the pieces strings to the smallest possible grids with the better symmetry I could get.

Here are the changed methods in piece:

```js
class Piece{
  constructor(options = {}) {
    const defaults = { x: 0 , y: 0, contents: null };
    Object.assign(this, defaults, options);
    this.contents = this.contents || this.chooseVariant();
  }

  chooseVariant(){
    // https://stackoverflow.com/questions/2532218/pick-random-property-from-a-javascript-object
    let variants = {
      i: '  i '+
         '  i '+
         '  i '+
         '  i ', // 16 chars = 4x4 char grid
      t: ' t '+ // 3x3
         'ttt'+
         '   ',
      l: 'l  '+
         'l  '+
         'll ',
      j: '  j'+
         '  j'+
         ' jj',
      o: 'oo'+ // 2x2
         'oo',
      s: ' ss'+
         'ss '+
         '   ',
      z: 'zz '+
         ' zz'+
         '   '
    };
    let keys = Object.keys(variants);
    this.variant = this.variant  || (keys[ keys.length * Math.random() << 0]);
    return variants[this.variant];
  }
  
  get length(){
    return this.contents.length;
  }
  
  get side(){
    return Math.sqrt(this.length);
  }
  
  at(x, y){
    return this.contents[(y * this.side + (x % this.side )) ];
  }

  // ... moveRight/Left/Down unmodified

  rotate(){
    let newGrid = [];
    for (let i0 = 0; i0 < this.length; i0++){
      // convert to x/y
      let x0 = i0 % this.side;
      let y0 = Math.floor(i0 / this.side);

      // find new x/y 
      let x1 = this.side - y0 - 1;
      let y1 = x0;

      // convert back to index
      var i1 = y1 * this.side + x1;
      newGrid[i1] = this.contents[i0];
    }
    this.contents = newGrid.join('');
  }

```
And here you have the changed methods outside of `Piece`, which are the two `Field` methods that received a `Piece` as argument, `canFit` and `drawPiece`:


```js
// Field class
  canFit(piece){ // for every overlap tile between the piece and the field:
    for(let x = 0; x < piece.side; x++){
      for(let y = 0; y < piece.side; y++){
        if (piece.at(x, y) !== ' ' &&                      // piece is not empty
            this.tiles[piece.x + x][piece.y + y] != ' ' ){ // field is not empty
          return false; //there is collision
        }
      }
    }
    return true; // if there are no collisions, it can fit
  }

  //...

  drawPiece(piece){
    let tile = ' ';
    for(let x = 0; x < piece.side; x += 1){
      for(let y = 0; y < piece.side; y += 1){
        tile = piece.at(x,y); 
        if (tile !== ' '){
          this.drawTile(piece.x + x,
                        piece.y + y,
                        this.colorFor(tile));
        } // non empty
      } // column tiles
    } // piece columns
  }
```

Once you have this, you have the original rotation on all pieces but the 4x1 column.

Time to start *piling pieces* and *clearing lines* now.

If you read all this, first of all, thank you very much! I hope you are having so much fun reading and, I hope, following along, as I had figuring out how to explain it.

Second, you might be curious how does this continue, but if you want to know that, you will have to jump to [Rocknrollesque's post](https://dev.to/rocknrollesque/step-by-step-tetris-on-es6-and-canvas) `#TODO: review the link`.

I created my **dev.to** account inspired by her, and I wanted to return the favour, so I challenged her to finish this post, so that she **had to** create a dev.to blog of her own.

So go now to find about:

## Touchdown and new piece

and

## Clearing lines and scoring