Skip to content

Instantly share code, notes, and snippets.

@straker
Last active November 10, 2024 13:06
Show Gist options
  • Save straker/3c98304f8a6a9174efd8292800891ea1 to your computer and use it in GitHub Desktop.
Save straker/3c98304f8a6a9174efd8292800891ea1 to your computer and use it in GitHub Desktop.
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
  • Innkeeper Games
  • Nezteb
<!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 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

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

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

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

@Pro496951
Copy link

@Rajdabade2112
Copy link

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

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

@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

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

Copy link

ghost commented Jun 1, 2021

how can i add a score to this?

@HanoiWarrior
Copy link

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

@LakeIsCool
Copy link

Can i change Game Over font?
If yes then show how.

@wextin
Copy link

wextin commented Feb 28, 2023

I created a pause for the game, a mobile version, added vertical and horizontal grids, set background music and a sound when the line is cleared. Made it so that you can change everything through the settings. You can see here http://a0783055.xsph.ru/

@kampiler
Copy link

kampiler commented Mar 1, 2023

You can see here http://a0783055.xsph.ru/

на компе не работают стрелки - только пробел - влево, музыка фоном, а вот звуков действий нет ((

@wextin
Copy link

wextin commented Mar 3, 2023

You can see here http://a0783055.xsph.ru/

на компе не работают стрелки - только пробел - влево, музыка фоном, а вот звуков действий нет ((

Почему звуков нет? При очищение линии и при проигрыше включается звук, если в настройках включены "Звуковые эффекты"

@stemisruler
Copy link

stemisruler commented May 8, 2023

if you want tetris block jump to bottom, add this code in the

document.addEventListener("keydown", function (e) {
}

    if (e.which === 32) {
      // Space bar pressed - move tetromino to bottom
      let row = tetromino.row;
      while (isValidMove(tetromino.matrix, row + 1, tetromino.col)) {
        row++;
      }
      tetromino.row = row;
      placeTetromino();
      return;
    }

and i am searching how i make restart button, who can help me?

@dogukansahil
Copy link

It works but interestingly the canvas size is very difficult to set.

@warrenkc
Copy link

if you want tetris block jump to bottom, add this code in the

document.addEventListener("keydown", function (e) { }

    if (e.which === 32) {
      // Space bar pressed - move tetromino to bottom
      let row = tetromino.row;
      while (isValidMove(tetromino.matrix, row + 1, tetromino.col)) {
        row++;
      }
      tetromino.row = row;
      placeTetromino();
      return;
    }

and i am searching how i make restart button, who can help me?

To add a "Restart Game" button and the necessary code to make it work, you will need to do the following steps:

  1. Add a button element to your HTML, preferably just above or below your canvas element.
  2. Write a JavaScript function that resets the game to its initial state.
  3. Attach an event listener to the button that calls the restart function when clicked.

Here is how you can implement these steps:

Step 1: Add the button to your HTML

You can add the button right after your canvas element in the HTML:

<canvas width="320" height="640" id="game"></canvas>
<button id="restartButton">Restart Game</button>

Step 2: Write the JavaScript restart function

The restart function should reset all the variables to their initial states and start the game loop again. This function should look something like this:

function restartGame() {
  cancelAnimationFrame(rAF);  // Cancel the current animation frame
  gameOver = false;            // Set the game over flag to false

  // Reset the playfield
  for (let row = -2; row < playfield.length; row++) {
    for (let col = 0; col < playfield[row].length; col++) {
      playfield[row][col] = 0;
    }
  }

  // Get a new tetromino sequence
  tetrominoSequence.length = 0;
  tetromino = getNextTetromino();

  // Start the game loop again
  rAF = requestAnimationFrame(loop);
}

You will add this function within your <script> tag in the HTML file.

Step 3: Attach the event listener to the button

This will bind your restart function to the click event of the button:

document.getElementById('restartButton').addEventListener('click', restartGame);

This code should also be placed within your <script> tag, typically at the bottom, to ensure that the HTML elements have loaded before you try to bind events to them.

Once you implement these changes in your index.htm file, your "Restart Game" button should be functional, allowing players to start a new game without refreshing the entire page.

@kampiler
Copy link

kampiler commented Nov 13, 2023

я быстрый сброс фигуры реализовал так:

  // space key (fast drop)
  if(e.which === 32) {
     while(!downOne()){
     }
  }
  // down arrow key (drop)
  if(e.which === 40) {
     downOne();
  }

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

      placeTetromino();
      return true;
     }
   tetromino.row = row;
   return false;
  }

@GL1TCHTheCoder
Copy link

i used this for my game

@GittyUpDev
Copy link

Thank you SOOOOOO much! Here is the game I made :)

@EeveEthan
Copy link

How would you make it so a sound effect play when the player clears a line, I'm trying to do it myself but it won't work.

@yoyasmine
Copy link

comment ajouter le gestion de vie avec un bouton

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