Skip to content

Instantly share code, notes, and snippets.

@straker
Last active Sep 24, 2022
Embed
What would you like to do?
Basic Tetris HTML and JavaScript Game

Basic Tetris HTML and JavaScript Game

This is a basic implementation of the game Tetris, but it's missing a few things intentionally and they're left as further exploration for the reader.

Further Exploration

Important note: I will answer questions about the code but will not add more features or answer questions about adding more features. This series is meant to give a basic outline of the game but nothing more.

License

(CC0 1.0 Universal) You're free to use this game and code in any project, personal or commercial. There's no need to ask permission before using these. Giving attribution is not required, but appreciated.

Other Basic Games

Support

Basic HTML Games are made possible by users like you. When you become a Patron, you get access to behind the scenes development logs, the ability to vote on which games I work on next, and early access to the next Basic HTML Game.

Top Patrons

  • Karar Al-Remahy
  • UnbrandedTech
<!DOCTYPE html>
<html>
<head>
<title>Basic Tetris HTML Game</title>
<meta charset="UTF-8">
<style>
html, body {
height: 100%;
margin: 0;
}
body {
background: black;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
border: 1px solid white;
}
</style>
</head>
<body>
<canvas width="320" height="640" id="game"></canvas>
<script>
// https://tetris.fandom.com/wiki/Tetris_Guideline
// get a random integer between the range of [min,max]
// @see https://stackoverflow.com/a/1527820/2124254
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// generate a new tetromino sequence
// @see https://tetris.fandom.com/wiki/Random_Generator
function generateSequence() {
const sequence = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'];
while (sequence.length) {
const rand = getRandomInt(0, sequence.length - 1);
const name = sequence.splice(rand, 1)[0];
tetrominoSequence.push(name);
}
}
// get the next tetromino in the sequence
function getNextTetromino() {
if (tetrominoSequence.length === 0) {
generateSequence();
}
const name = tetrominoSequence.pop();
const matrix = tetrominos[name];
// I and O start centered, all others start in left-middle
const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);
// I starts on row 21 (-1), all others start on row 22 (-2)
const row = name === 'I' ? -1 : -2;
return {
name: name, // name of the piece (L, O, etc.)
matrix: matrix, // the current rotation matrix
row: row, // current row (starts offscreen)
col: col // current col
};
}
// rotate an NxN matrix 90deg
// @see https://codereview.stackexchange.com/a/186834
function rotate(matrix) {
const N = matrix.length - 1;
const result = matrix.map((row, i) =>
row.map((val, j) => matrix[N - j][i])
);
return result;
}
// check to see if the new matrix/row/col is valid
function isValidMove(matrix, cellRow, cellCol) {
for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] && (
// outside the game bounds
cellCol + col < 0 ||
cellCol + col >= playfield[0].length ||
cellRow + row >= playfield.length ||
// collides with another piece
playfield[cellRow + row][cellCol + col])
) {
return false;
}
}
}
return true;
}
// place the tetromino on the playfield
function placeTetromino() {
for (let row = 0; row < tetromino.matrix.length; row++) {
for (let col = 0; col < tetromino.matrix[row].length; col++) {
if (tetromino.matrix[row][col]) {
// game over if piece has any part offscreen
if (tetromino.row + row < 0) {
return showGameOver();
}
playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
}
}
}
// check for line clears starting from the bottom and working our way up
for (let row = playfield.length - 1; row >= 0; ) {
if (playfield[row].every(cell => !!cell)) {
// drop every row above this one
for (let r = row; r >= 0; r--) {
for (let c = 0; c < playfield[r].length; c++) {
playfield[r][c] = playfield[r-1][c];
}
}
}
else {
row--;
}
}
tetromino = getNextTetromino();
}
// show the game over screen
function showGameOver() {
cancelAnimationFrame(rAF);
gameOver = true;
context.fillStyle = 'black';
context.globalAlpha = 0.75;
context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);
context.globalAlpha = 1;
context.fillStyle = 'white';
context.font = '36px monospace';
context.textAlign = 'center';
context.textBaseline = 'middle';
context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);
}
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 32;
const tetrominoSequence = [];
// keep track of what is in every cell of the game using a 2d array
// tetris playfield is 10x20, with a few rows offscreen
const playfield = [];
// populate the empty state
for (let row = -2; row < 20; row++) {
playfield[row] = [];
for (let col = 0; col < 10; col++) {
playfield[row][col] = 0;
}
}
// how to draw each tetromino
// @see https://tetris.fandom.com/wiki/SRS
const tetrominos = {
'I': [
[0,0,0,0],
[1,1,1,1],
[0,0,0,0],
[0,0,0,0]
],
'J': [
[1,0,0],
[1,1,1],
[0,0,0],
],
'L': [
[0,0,1],
[1,1,1],
[0,0,0],
],
'O': [
[1,1],
[1,1],
],
'S': [
[0,1,1],
[1,1,0],
[0,0,0],
],
'Z': [
[1,1,0],
[0,1,1],
[0,0,0],
],
'T': [
[0,1,0],
[1,1,1],
[0,0,0],
]
};
// color of each tetromino
const colors = {
'I': 'cyan',
'O': 'yellow',
'T': 'purple',
'S': 'green',
'Z': 'red',
'J': 'blue',
'L': 'orange'
};
let count = 0;
let tetromino = getNextTetromino();
let rAF = null; // keep track of the animation frame so we can cancel it
let gameOver = false;
// game loop
function loop() {
rAF = requestAnimationFrame(loop);
context.clearRect(0,0,canvas.width,canvas.height);
// draw the playfield
for (let row = 0; row < 20; row++) {
for (let col = 0; col < 10; col++) {
if (playfield[row][col]) {
const name = playfield[row][col];
context.fillStyle = colors[name];
// drawing 1 px smaller than the grid creates a grid effect
context.fillRect(col * grid, row * grid, grid-1, grid-1);
}
}
}
// draw the active tetromino
if (tetromino) {
// tetromino falls every 35 frames
if (++count > 35) {
tetromino.row++;
count = 0;
// place piece if it runs into anything
if (!isValidMove(tetromino.matrix, tetromino.row, tetromino.col)) {
tetromino.row--;
placeTetromino();
}
}
context.fillStyle = colors[tetromino.name];
for (let row = 0; row < tetromino.matrix.length; row++) {
for (let col = 0; col < tetromino.matrix[row].length; col++) {
if (tetromino.matrix[row][col]) {
// drawing 1 px smaller than the grid creates a grid effect
context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);
}
}
}
}
}
// listen to keyboard events to move the active tetromino
document.addEventListener('keydown', function(e) {
if (gameOver) return;
// left and right arrow keys (move)
if (e.which === 37 || e.which === 39) {
const col = e.which === 37
? tetromino.col - 1
: tetromino.col + 1;
if (isValidMove(tetromino.matrix, tetromino.row, col)) {
tetromino.col = col;
}
}
// up arrow key (rotate)
if (e.which === 38) {
const matrix = rotate(tetromino.matrix);
if (isValidMove(matrix, tetromino.row, tetromino.col)) {
tetromino.matrix = matrix;
}
}
// down arrow key (drop)
if(e.which === 40) {
const row = tetromino.row + 1;
if (!isValidMove(tetromino.matrix, row, tetromino.col)) {
tetromino.row = row - 1;
placeTetromino();
return;
}
tetromino.row = row;
}
});
// start the game
rAF = requestAnimationFrame(loop);
</script>
</body>
</html>
@straker
Copy link
Author

straker commented Dec 12, 2019

@DreadJaw Do you have an image or what's going on? I'm not sure what you mean by clips out the right side?

@kazutokirigiya1
Copy link

kazutokirigiya1 commented Dec 13, 2019

image

@kazutokirigiya1
Copy link

kazutokirigiya1 commented Dec 13, 2019

I increased the height and width by 100 and it didn't help anything, I may be that there is too many pixels in the space and so it clips out to coup with the lack of pixels.

@straker
Copy link
Author

straker commented Dec 13, 2019

@kazutokirigiya1 I'm not sure what the image is showing. What is clipping out in the image? The green tetis piece looks centered to the box.

@ChristianCrousser
Copy link

ChristianCrousser commented Dec 17, 2019

Ive ran into a problem when trying to run this. From what I gather whenever a line clears it lowers the top of the grid. After about 3 tetris line clears or when you clear enough lines, the game will end like it is hitting the top of the screen even if close to the bottom. I cant seem to think of a fix for this. I can link a screenshot if you are curious. Thank you

@ChristianCrousser
Copy link

ChristianCrousser commented Dec 17, 2019

Screen Shot 2019-12-17 at 11 57 49 AM

@straker
Copy link
Author

straker commented Dec 17, 2019

@ChristianCrousser I saw that bug like once when making it and could never reproduce it. I've looked over the code a ton but could never figure out how this happens...

@kazutokirigiya1
Copy link

kazutokirigiya1 commented Dec 18, 2019

@ChristianCrousser
Copy link

ChristianCrousser commented Dec 18, 2019

This doesnt happen for you? The only solution I can think of at this time is a function that adds rows to the top of the grid depending on how many rows were deleted. Say if you get a tetris then you add four rows and so on.

@straker
Copy link
Author

straker commented Dec 18, 2019

Wait, I think I finally figured it out! So the problem is when I clear a line, I make the row equal to the row above it (setting the two rows to the same array):

// drop every row above this one
for (let r = row; r >= 0; r--) {
  playfield[r] = playfield[r-1];
}

So after a couple of line clears, every row is now the same array, which means adding 1 cell to it adds 1 cell to every row. Instead the line clear should change the columns 1 by 1 to avoid this problem.

for (let r = row; r >= 0; r--) {
  for (let c = col; c < playfield[r].length; c++) {
    playfield[r][c] = playfield[r-1][c];
  }
}

@ChristianCrousser
Copy link

ChristianCrousser commented Dec 18, 2019

I think this is on the right track for sure. Your description of the problem makes a lot more sense than what I was thinking. I replaced that line of code with the new one and got a very strange result. The lines will not clear and shapes will no longer fall as well.
Screen Shot 2019-12-18 at 4 01 55 PM

@straker
Copy link
Author

straker commented Dec 18, 2019

Dang. I'll work on this some more later tonight and try to get an update out. Thanks for the help.

@straker
Copy link
Author

straker commented Dec 19, 2019

It helps if I start c at 0 instead of a non existent col.

for (let r = row; r >= 0; r--) {
  for (let c = 0; c < playfield[r].length; c++) {
    playfield[r][c] = playfield[r-1][c];
  }
}

@madbandit00
Copy link

madbandit00 commented Dec 19, 2019

Hi, can I have you guys twitter handle? I'm thinking of using this code for my website and would like to credit you guys :)

@ChristianCrousser
Copy link

ChristianCrousser commented Dec 19, 2019

It works! Thank you Steven! Im sorry that I had to bother you so much with this. I know that whenever I have a problem with my stuff it bothers me like no other until it is fixed. Keep up the good work! And @madbandit00 it was all him. I only saw a small problem

@straker
Copy link
Author

straker commented Dec 19, 2019

You're welcome! I don't enjoy having code with bugs in it posted for others. Thanks for finding it so others don't experience it.

@madbandit00 here's my twitter

@dragonmax2016
Copy link

dragonmax2016 commented Feb 14, 2020

can i add a delay to the locking so that you can do a classic tetris move

@Pro496951
Copy link

Pro496951 commented Apr 9, 2020

@Rajdabade2112
Copy link

Rajdabade2112 commented May 30, 2020

How to restart this game??

Copy link

ghost commented Jun 9, 2020

refresh the page

@hoo-svg
Copy link

hoo-svg commented Oct 26, 2020

How do you make it clip out by the right side?
I want mine to do that.

@hoo-svg
Copy link

hoo-svg commented Oct 26, 2020

This is the code:

<!DOCTYPE html>
<html>
<head>
  <title></title>
  <style>
  html, body {
    height: 100%;
    margin: 0;
  }

  body {
    background: black;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  canvas {
    border: 1px solid white;
  }
  </style>
</head>
<body>
<canvas width="320" height="640" id="game"></canvas>
<script>
// https://tetris.fandom.com/wiki/Tetris_Guideline

// get a random integer between the range of [min,max]
// @see https://stackoverflow.com/a/1527820/2124254
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);

  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// generate a new tetromino sequence
// @see https://tetris.fandom.com/wiki/Random_Generator
function generateSequence() {
  const sequence = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'];

  while (sequence.length) {
    const rand = getRandomInt(0, sequence.length - 1);
    const name = sequence.splice(rand, 1)[0];
    tetrominoSequence.push(name);
  }
}

// get the next tetromino in the sequence
function getNextTetromino() {
  if (tetrominoSequence.length === 0) {
    generateSequence();
  }

  const name = tetrominoSequence.pop();
  const matrix = tetrominos[name];

  // I and O start centered, all others start in left-middle
  const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);

  // I starts on row 21 (-1), all others start on row 22 (-2)
  const row = name === 'I' ? -1 : -2;

  return {
    name: name,      // name of the piece (L, O, etc.)
    matrix: matrix,  // the current rotation matrix
    row: row,        // current row (starts offscreen)
    col: col         // current col
  };
}

// rotate an NxN matrix 90deg
// @see https://codereview.stackexchange.com/a/186834
function rotate(matrix) {
  const N = matrix.length - 1;
  const result = matrix.map((row, i) =>
    row.map((val, j) => matrix[N - j][i])
  );

  return result;
}

// check to see if the new matrix/row/col is valid
function isValidMove(matrix, cellRow, cellCol) {
  for (let row = 0; row < matrix.length; row++) {
    for (let col = 0; col < matrix[row].length; col++) {
      if (matrix[row][col] && (
          // outside the game bounds
          cellCol + col < 0 ||
          cellCol + col >= playfield[0].length ||
          cellRow + row >= playfield.length ||
          // collides with another piece
          playfield[cellRow + row][cellCol + col])
        ) {
        return false;
      }
    }
  }

  return true;
}

// place the tetromino on the playfield
function placeTetromino() {
  for (let row = 0; row < tetromino.matrix.length; row++) {
    for (let col = 0; col < tetromino.matrix[row].length; col++) {
      if (tetromino.matrix[row][col]) {

        // game over if piece has any part offscreen
        if (tetromino.row + row < 0) {
          return showGameOver();
        }

        playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
      }
    }
  }

  // check for line clears starting from the bottom and working our way up
  for (let row = playfield.length - 1; row >= 0; ) {
    if (playfield[row].every(cell => !!cell)) {

      // drop every row above this one
      for (let r = row; r >= 0; r--) {
        for (let c = 0; c < playfield[r].length; c++) {
          playfield[r][c] = playfield[r-1][c];
        }
      }
    }
    else {
      row--;
    }
  }

  tetromino = getNextTetromino();
}

// show the game over screen
function showGameOver() {
  cancelAnimationFrame(rAF);
  gameOver = true;

  context.fillStyle = 'black';
  context.globalAlpha = 0.75;
  context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);

  context.globalAlpha = 1;
  context.fillStyle = 'white';
  context.font = '36px monospace';
  context.textAlign = 'center';
  context.textBaseline = 'middle';
  context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);
}

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 32;
const tetrominoSequence = [];

// keep track of what is in every cell of the game using a 2d array
// tetris playfield is 10x20, with a few rows offscreen
const playfield = [];

// populate the empty state
for (let row = -2; row < 20; row++) {
  playfield[row] = [];

  for (let col = 0; col < 10; col++) {
    playfield[row][col] = 0;
  }
}

// how to draw each tetromino
// @see https://tetris.fandom.com/wiki/SRS
const tetrominos = {
  'I': [
    [0,0,0,0],
    [1,1,1,1],
    [0,0,0,0],
    [0,0,0,0]
  ],
  'J': [
    [1,0,0],
    [1,1,1],
    [0,0,0],
  ],
  'L': [
    [0,0,1],
    [1,1,1],
    [0,0,0],
  ],
  'O': [
    [1,1],
    [1,1],
  ],
  'S': [
    [0,1,1],
    [1,1,0],
    [0,0,0],
  ],
  'Z': [
    [1,1,0],
    [0,1,1],
    [0,0,0],
  ],
  'T': [
    [0,1,0],
    [1,1,1],
    [0,0,0],
  ]
};

// color of each tetromino
const colors = {
  'I': 'cyan',
  'O': 'yellow',
  'T': 'purple',
  'S': 'green',
  'Z': 'red',
  'J': 'blue',
  'L': 'orange'
};

let count = 0;
let tetromino = getNextTetromino();
let rAF = null;  // keep track of the animation frame so we can cancel it
let gameOver = false;

// game loop
function loop() {
  rAF = requestAnimationFrame(loop);
  context.clearRect(0,0,canvas.width,canvas.height);

  // draw the playfield
  for (let row = 0; row < 20; row++) {
    for (let col = 0; col < 10; col++) {
      if (playfield[row][col]) {
        const name = playfield[row][col];
        context.fillStyle = colors[name];

        // drawing 1 px smaller than the grid creates a grid effect
        context.fillRect(col * grid, row * grid, grid-1, grid-1);
      }
    }
  }

  // draw the active tetromino
  if (tetromino) {

    // tetromino falls every 35 frames
    if (++count > 35) {
      tetromino.row++;
      count = 0;

      // place piece if it runs into anything
      if (!isValidMove(tetromino.matrix, tetromino.row, tetromino.col)) {
        tetromino.row--;
        placeTetromino();
      }
    }

    context.fillStyle = colors[tetromino.name];

    for (let row = 0; row < tetromino.matrix.length; row++) {
      for (let col = 0; col < tetromino.matrix[row].length; col++) {
        if (tetromino.matrix[row][col]) {

          // drawing 1 px smaller than the grid creates a grid effect
          context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);
        }
      }
    }
  }
}

// listen to keyboard events to move the active tetromino
document.addEventListener('keydown', function(e) {
  if (gameOver) return;

  // left and right arrow keys (move)
  if (e.which === 37 || e.which === 39) {
    const col = e.which === 37
      ? tetromino.col - 1
      : tetromino.col + 1;

    if (isValidMove(tetromino.matrix, tetromino.row, col)) {
      tetromino.col = col;
    }
  }

  // up arrow key (rotate)
  if (e.which === 38) {
    const matrix = rotate(tetromino.matrix);
    if (isValidMove(matrix, tetromino.row, tetromino.col)) {
      tetromino.matrix = matrix;
    }
  }

  // down arrow key (drop)
  if(e.which === 40) {
    const row = tetromino.row + 1;

    if (!isValidMove(tetromino.matrix, row, tetromino.col)) {
      tetromino.row = row - 1;

      placeTetromino();
      return;
    }

    tetromino.row = row;
  }
});

// start the game
rAF = requestAnimationFrame(loop);
</script>
</body>
</html>

@kampiler
Copy link

kampiler commented Jan 7, 2021

cool!!
my version is here - https://kampiler.ru/gamez/tetris.php
I will add features

@hbibrachel
Copy link

hbibrachel commented Feb 14, 2021

It helps if I start c at 0 instead of a non existent col.

for (let r = row; r >= 0; r--) {
  for (let c = 0; c < playfield[r].length; c++) {
    playfield[r][c] = playfield[r-1][c];
  }
}

but this code did not work on mobile why?

@hbibrachel
Copy link

hbibrachel commented Feb 14, 2021

@DreadJaw Do you have an image or what's going on? I'm not sure what you mean by clips out the right side?

but most of this games doesn't work on mobile would you add the mobile style id id but still not work

@straker
Copy link
Author

straker commented Feb 15, 2021

@hbibrachel If by doesn't work you mean you can't play it because it doesn't respond to touch, that is correct. Adding touch support is one of the further exploration activities.

@hbibrachel
Copy link

hbibrachel commented Feb 15, 2021

Yes , there are no way to move the arrow even the space key

@RealMaoMao
Copy link

RealMaoMao commented Jun 1, 2021

how can i add a score to this?

@HanoiWarrior
Copy link

HanoiWarrior commented Aug 28, 2021

Hey, can anyone teach me to make the game have a start button and can playing again without refresh the whole page? Thank you

@LuanaCorreiaMelo
Copy link

LuanaCorreiaMelo commented Jun 21, 2022

Nice but I am still facing issues with this. That's why I am taking some help from the experts of a website who are helping me with slot machines for the Austrian fireman and they tell me to browse this site where I can get the best information about this. Have you ever heard about this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment