Skip to content

Instantly share code, notes, and snippets.

@straker
Last active April 10, 2025 10:58
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>
@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

@S0-C4lled-RYO
Copy link

Can i make it mobile compatible ?

@007cikcak
Copy link

I have an html code like the one below. I added the coustume html code section to the block on my wordpress site and pasted the github game code below into it, but it doesn't work on the game wordpress page. :(/ What do you think is the problem? How can I run this without breaking my wordpress page? The code below really works, but when I copy the code to Advance Custom HTML or custom html, the game doesn't work. How do you think I can solve this problem quickly and without any problems?

@aTramelKHS
Copy link

i gotta feeling that i have to rewite some code in order to make the next piece preview work

@S0-C4lled-RYO
Copy link

i made it mobile compatible and also added a neon theme to it here it is

<title>Futuristic Tetris</title> <style> html, body { height: 100%; margin: 0; overflow: hidden; -webkit-touch-callout: none; /* Disable callout */ -webkit-user-select: none; /* Disable text selection */ user-select: none; /* Disable text selection */ }
body {
  background: black;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-image: url('https://raw.githubusercontent.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/main/a-beautiful-cyberpunk-city-in-night-with_k6eRYYtaRkmdN415_b_BLw_9NwrjupXSjeVj4Rn5g-gYw.jpeg');
  background-size: cover;
  background-position: center;
}

canvas {
  border: 2px solid rgba(0, 255, 255, 0.5); /* Neon border for the playfield */
  max-width: 100%;
  max-height: 60vh; /* Adjusted to fit score text */
  background: #001f3f; /* Deep dark blue for the playfield */
  box-shadow: 0 0 20px rgba(0, 255, 255, 0.7); /* Glow effect */
}

.controls {
  display: grid;
  grid-template-areas:
    ". rotate ."
    "left . right"
    ". drop .";
  gap: 10px;
  margin-top: 20px;
}

.controls button {
  padding: 10px;
  font-size: 16px; /* Smaller buttons */
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #00ffcc; /* Neon cyan */
  border: 2px solid #00ffcc; /* Neon cyan border */
  border-radius: 10px;
  cursor: pointer;
  outline: none; /* Remove outline */
  -webkit-tap-highlight-color: transparent; /* Disable tap highlight */
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(0, 255, 255, 0.7); /* Glow effect */
}

.controls button:active {
  background: #00ffcc; /* Neon cyan */
  color: black;
  transform: scale(0.95);
}

#rotate { grid-area: rotate; }
#left { grid-area: left; }
#right { grid-area: right; }
#drop { grid-area: drop; }

#pause {
  position: absolute;
  top: 10px;
  left: 10px;
  padding: 10px;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #ff00ff; /* Neon pink */
  border: 2px solid #ff00ff; /* Neon pink border */
  border-radius: 5px;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(255, 0, 255, 0.7); /* Glow effect */
}

#pause:active {
  background: #ff00ff; /* Neon pink */
  color: black;
  transform: scale(0.95);
}

#reset {
  position: absolute;
  top: 60px;
  left: 10px;
  padding: 10px;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #ff00ff; /* Neon pink */
  border: 2px solid #ff00ff; /* Neon pink border */
  border-radius: 5px;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  display: none; /* Hidden by default */
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(255, 0, 255, 0.7); /* Glow effect */
}

#reset:active {
  background: #ff00ff; /* Neon pink */
  color: black;
  transform: scale(0.95);
}

#mute {
  position: absolute;
  top: 10px;
  right: 10px;
  padding: 10px;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #ff00ff; /* Neon pink */
  border: 2px solid #ff00ff; /* Neon pink border */
  border-radius: 5px;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(255, 0, 255, 0.7); /* Glow effect */
}

#mute:active {
  background: #ff00ff; /* Neon pink */
  color: black;
  transform: scale(0.95);
}
</style>
← ↻ → ↓
Pause Reset Mute <script> // Background music const bgMusic = new Audio('https://raw.githubusercontent.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/main/Nostalgic%20Whirl%20ext%20v2.mp3'); bgMusic.loop = true; // Loop the music bgMusic.volume = 0.5; // Set volume to 50% bgMusic.play(); // Start playing automatically
// Sound effects
const blockCollisionSound = new Audio('https://raw.githubusercontent.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/main/-clinking-sound-of-two-metallic-blocks-gently-colliding-1_Lsbe3pb6.mp3');
const lineClearSound = new Audio('https://github.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/raw/main/WWWWWWWWW.mp3');
const gameOverSound = new Audio('https://github.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/raw/main/LOSING.mp3');

// Increase volume of SFX
blockCollisionSound.volume = 1.0;
lineClearSound.volume = 1.0;
gameOverSound.volume = 1.0;

// Mute button functionality
const muteButton = document.getElementById('mute');
let isMuted = false;

muteButton.addEventListener('click', () => {
  isMuted = !isMuted;
  bgMusic.muted = isMuted;
  muteButton.textContent = isMuted ? 'Unmute' : 'Mute';
});

// Tetromino shapes
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]]
};

// https://tetris.fandom.com/wiki/Tetris_Guideline

// get a random integer between the range of [min,max]
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
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];
  const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);
  const row = name === 'I' ? -1 : -2;
  return {
    name: name,
    matrix: matrix,
    row: row,
    col: col
  };
}

// rotate an NxN matrix 90deg
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] && (
          cellCol + col < 0 ||
          cellCol + col >= playfield[0].length ||
          cellRow + row >= playfield.length ||
          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]) {
        if (tetromino.row + row < 0) {
          gameOverSound.play();
          return showGameOver();
        }
        playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
        blockCollisionSound.currentTime = 0; // Reset sound to start
        blockCollisionSound.play(); // Play block collision sound
      }
    }
  }

  // check for line clears
  for (let row = playfield.length - 1; row >= 0; ) {
    if (playfield[row].every(cell => !!cell)) {
      for (let r = row; r >= 0; r--) {
        for (let c = 0; c < playfield[r].length; c++) {
          playfield[r][c] = playfield[r-1][c];
        }
      }
      score += 100;
      lineClearSound.currentTime = 0; // Reset sound to start
      lineClearSound.play(); // Play line clear sound
    } else {
      row--;
    }
  }

  tetromino = getNextTetromino();
}

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

  if (score > highScore) {
    highScore = score;
    localStorage.setItem('tetrisHighScore', highScore);
  }

  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);

  // Show reset button
  document.getElementById('reset').style.display = 'block';
}

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 32;
const tetrominoSequence = [];
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;
  }
}

// Neon block colors
const colors = {
  'I': '#00ffff', // Neon cyan
  'O': '#ffff00', // Neon yellow
  'T': '#ff00ff', // Neon pink
  'S': '#00ff00', // Neon green
  'Z': '#ff0000', // Neon red
  'J': '#0000ff', // Neon blue
  'L': '#ffa500'  // Neon orange
};

let count = 0;
let tetromino = getNextTetromino();
let rAF = null;
let gameOver = false;
let paused = false;
let score = 0;
let highScore = localStorage.getItem('tetrisHighScore') || 0;

// game loop
function loop() {
  if (paused || gameOver) return;

  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]) {
        context.fillStyle = colors[playfield[row][col]];
        context.fillRect(col * grid, row * grid, grid-1, grid-1);

        // Add glow effect
        context.shadowBlur = 20;
        context.shadowColor = colors[playfield[row][col]];
      }
    }
  }

  // Reset shadow settings
  context.shadowBlur = 0;
  context.shadowColor = 'transparent';

  // draw the active tetromino
  if (tetromino) {
    if (++count > 35) {
      tetromino.row++;
      count = 0;

      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]) {
          context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);

          // Add glow effect
          context.shadowBlur = 20;
          context.shadowColor = colors[tetromino.name];
        }
      }
    }
  }

  // Reset shadow settings
  context.shadowBlur = 0;
  context.shadowColor = 'transparent';

  // draw the score and high score
  context.fillStyle = '#00ffcc'; /* Neon cyan */
  context.font = '18px monospace';
  context.textAlign = 'left';
  context.textBaseline = 'top';
  context.fillText('Score: ' + score, 10, 10);
  context.fillText('High Score: ' + highScore, 10, 30);
}

// keyboard controls
document.addEventListener('keydown', function(e) {
  if (gameOver || paused) return;

  if (e.which === 37) { // left
    moveLeft();
  } else if (e.which === 39) { // right
    moveRight();
  } else if (e.which === 38) { // rotate
    rotateTetromino();
  } else if (e.which === 40) { // drop
    hardDrop();
  } else if (e.which === 80) { // P key for pause
    togglePause();
  }
});

// touch controls (tap-to-move)
document.getElementById('left').addEventListener('click', moveLeft);
document.getElementById('right').addEventListener('click', moveRight);
document.getElementById('rotate').addEventListener('click', rotateTetromino);
document.getElementById('drop').addEventListener('click', hardDrop);

// pause functionality
document.getElementById('pause').addEventListener('click', togglePause);

// reset functionality
document.getElementById('reset').addEventListener('click', resetGame);

function moveLeft() {
  if (gameOver || paused) return;
  const col = tetromino.col - 1;
  if (isValidMove(tetromino.matrix, tetromino.row, col)) {
    tetromino.col = col;
  }
}

function moveRight() {
  if (gameOver || paused) return;
  const col = tetromino.col + 1;
  if (isValidMove(tetromino.matrix, tetromino.row, col)) {
    tetromino.col = col;
  }
}

function rotateTetromino() {
  if (gameOver || paused) return;
  const matrix = rotate(tetromino.matrix);
  if (isValidMove(matrix, tetromino.row, tetromino.col)) {
    tetromino.matrix = matrix;
  }
}

function hardDrop() {
  if (gameOver || paused) return;
  while (isValidMove(tetromino.matrix, tetromino.row + 1, tetromino.col)) {
    tetromino.row++;
  }
  placeTetromino();
}

function togglePause() {
  paused = !paused;
  document.getElementById('pause').textContent = paused ? 'Resume' : 'Pause';
  if (!paused) loop();
}

function resetGame() {
  // Reset game state
  for (let row = -2; row < 20; row++) {
    for (let col = 0; col < 10; col++) {
      playfield[row][col] = 0;
    }
  }
  tetrominoSequence.length = 0;
  tetromino = getNextTetromino();
  score = 0;
  gameOver = false;
  paused = false;
  document.getElementById('reset').style.display = 'none';
  document.getElementById('pause').textContent = 'Pause';
  loop();
}

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

@S0-C4lled-RYO
Copy link

`

<title>Futuristic Tetris</title> <style> html, body { height: 100%; margin: 0; overflow: hidden; -webkit-touch-callout: none; /* Disable callout */ -webkit-user-select: none; /* Disable text selection */ user-select: none; /* Disable text selection */ }
body {
  background: black;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-image: url('https://raw.githubusercontent.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/main/a-beautiful-cyberpunk-city-in-night-with_k6eRYYtaRkmdN415_b_BLw_9NwrjupXSjeVj4Rn5g-gYw.jpeg');
  background-size: cover;
  background-position: center;
}

canvas {
  border: 2px solid rgba(0, 255, 255, 0.5); /* Neon border for the playfield */
  max-width: 100%;
  max-height: 60vh; /* Adjusted to fit score text */
  background: #001f3f; /* Deep dark blue for the playfield */
  box-shadow: 0 0 20px rgba(0, 255, 255, 0.7); /* Glow effect */
}

.controls {
  display: grid;
  grid-template-areas:
    ". rotate ."
    "left . right"
    ". drop .";
  gap: 10px;
  margin-top: 20px;
}

.controls button {
  padding: 10px;
  font-size: 16px; /* Smaller buttons */
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #00ffcc; /* Neon cyan */
  border: 2px solid #00ffcc; /* Neon cyan border */
  border-radius: 10px;
  cursor: pointer;
  outline: none; /* Remove outline */
  -webkit-tap-highlight-color: transparent; /* Disable tap highlight */
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(0, 255, 255, 0.7); /* Glow effect */
}

.controls button:active {
  background: #00ffcc; /* Neon cyan */
  color: black;
  transform: scale(0.95);
}

#rotate { grid-area: rotate; }
#left { grid-area: left; }
#right { grid-area: right; }
#drop { grid-area: drop; }

#pause {
  position: absolute;
  top: 10px;
  left: 10px;
  padding: 10px;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #ff00ff; /* Neon pink */
  border: 2px solid #ff00ff; /* Neon pink border */
  border-radius: 5px;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(255, 0, 255, 0.7); /* Glow effect */
}

#pause:active {
  background: #ff00ff; /* Neon pink */
  color: black;
  transform: scale(0.95);
}

#reset {
  position: absolute;
  top: 60px;
  left: 10px;
  padding: 10px;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #ff00ff; /* Neon pink */
  border: 2px solid #ff00ff; /* Neon pink border */
  border-radius: 5px;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  display: none; /* Hidden by default */
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(255, 0, 255, 0.7); /* Glow effect */
}

#reset:active {
  background: #ff00ff; /* Neon pink */
  color: black;
  transform: scale(0.95);
}

#mute {
  position: absolute;
  top: 10px;
  right: 10px;
  padding: 10px;
  font-size: 16px;
  background: rgba(0, 0, 0, 0.7); /* Semi-transparent black */
  color: #ff00ff; /* Neon pink */
  border: 2px solid #ff00ff; /* Neon pink border */
  border-radius: 5px;
  cursor: pointer;
  outline: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.3s, transform 0.2s;
  box-shadow: 0 0 10px rgba(255, 0, 255, 0.7); /* Glow effect */
}

#mute:active {
  background: #ff00ff; /* Neon pink */
  color: black;
  transform: scale(0.95);
}
</style>
← ↻ → ↓
Pause Reset Mute <script> // Background music const bgMusic = new Audio('https://raw.githubusercontent.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/main/Nostalgic%20Whirl%20ext%20v2.mp3'); bgMusic.loop = true; // Loop the music bgMusic.volume = 0.5; // Set volume to 50% bgMusic.play(); // Start playing automatically
// Sound effects
const blockCollisionSound = new Audio('https://raw.githubusercontent.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/main/-clinking-sound-of-two-metallic-blocks-gently-colliding-1_Lsbe3pb6.mp3');
const lineClearSound = new Audio('https://github.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/raw/main/WWWWWWWWW.mp3');
const gameOverSound = new Audio('https://github.com/S0-C4lled-RYO/PROJEKT-KLASSIKAL/raw/main/LOSING.mp3');

// Increase volume of SFX
blockCollisionSound.volume = 1.0;
lineClearSound.volume = 1.0;
gameOverSound.volume = 1.0;

// Mute button functionality
const muteButton = document.getElementById('mute');
let isMuted = false;

muteButton.addEventListener('click', () => {
  isMuted = !isMuted;
  bgMusic.muted = isMuted;
  muteButton.textContent = isMuted ? 'Unmute' : 'Mute';
});

// Tetromino shapes
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]]
};

// https://tetris.fandom.com/wiki/Tetris_Guideline

// get a random integer between the range of [min,max]
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
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];
  const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);
  const row = name === 'I' ? -1 : -2;
  return {
    name: name,
    matrix: matrix,
    row: row,
    col: col
  };
}

// rotate an NxN matrix 90deg
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] && (
          cellCol + col < 0 ||
          cellCol + col >= playfield[0].length ||
          cellRow + row >= playfield.length ||
          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]) {
        if (tetromino.row + row < 0) {
          gameOverSound.play();
          return showGameOver();
        }
        playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
        blockCollisionSound.currentTime = 0; // Reset sound to start
        blockCollisionSound.play(); // Play block collision sound
      }
    }
  }

  // check for line clears
  for (let row = playfield.length - 1; row >= 0; ) {
    if (playfield[row].every(cell => !!cell)) {
      for (let r = row; r >= 0; r--) {
        for (let c = 0; c < playfield[r].length; c++) {
          playfield[r][c] = playfield[r-1][c];
        }
      }
      score += 100;
      lineClearSound.currentTime = 0; // Reset sound to start
      lineClearSound.play(); // Play line clear sound
    } else {
      row--;
    }
  }

  tetromino = getNextTetromino();
}

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

  if (score > highScore) {
    highScore = score;
    localStorage.setItem('tetrisHighScore', highScore);
  }

  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);

  // Show reset button
  document.getElementById('reset').style.display = 'block';
}

const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
const grid = 32;
const tetrominoSequence = [];
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;
  }
}

// Neon block colors
const colors = {
  'I': '#00ffff', // Neon cyan
  'O': '#ffff00', // Neon yellow
  'T': '#ff00ff', // Neon pink
  'S': '#00ff00', // Neon green
  'Z': '#ff0000', // Neon red
  'J': '#0000ff', // Neon blue
  'L': '#ffa500'  // Neon orange
};

let count = 0;
let tetromino = getNextTetromino();
let rAF = null;
let gameOver = false;
let paused = false;
let score = 0;
let highScore = localStorage.getItem('tetrisHighScore') || 0;

// game loop
function loop() {
  if (paused || gameOver) return;

  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]) {
        context.fillStyle = colors[playfield[row][col]];
        context.fillRect(col * grid, row * grid, grid-1, grid-1);

        // Add glow effect
        context.shadowBlur = 20;
        context.shadowColor = colors[playfield[row][col]];
      }
    }
  }

  // Reset shadow settings
  context.shadowBlur = 0;
  context.shadowColor = 'transparent';

  // draw the active tetromino
  if (tetromino) {
    if (++count > 35) {
      tetromino.row++;
      count = 0;

      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]) {
          context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);

          // Add glow effect
          context.shadowBlur = 20;
          context.shadowColor = colors[tetromino.name];
        }
      }
    }
  }

  // Reset shadow settings
  context.shadowBlur = 0;
  context.shadowColor = 'transparent';

  // draw the score and high score
  context.fillStyle = '#00ffcc'; /* Neon cyan */
  context.font = '18px monospace';
  context.textAlign = 'left';
  context.textBaseline = 'top';
  context.fillText('Score: ' + score, 10, 10);
  context.fillText('High Score: ' + highScore, 10, 30);
}

// keyboard controls
document.addEventListener('keydown', function(e) {
  if (gameOver || paused) return;

  if (e.which === 37) { // left
    moveLeft();
  } else if (e.which === 39) { // right
    moveRight();
  } else if (e.which === 38) { // rotate
    rotateTetromino();
  } else if (e.which === 40) { // drop
    hardDrop();
  } else if (e.which === 80) { // P key for pause
    togglePause();
  }
});

// touch controls (tap-to-move)
document.getElementById('left').addEventListener('click', moveLeft);
document.getElementById('right').addEventListener('click', moveRight);
document.getElementById('rotate').addEventListener('click', rotateTetromino);
document.getElementById('drop').addEventListener('click', hardDrop);

// pause functionality
document.getElementById('pause').addEventListener('click', togglePause);

// reset functionality
document.getElementById('reset').addEventListener('click', resetGame);

function moveLeft() {
  if (gameOver || paused) return;
  const col = tetromino.col - 1;
  if (isValidMove(tetromino.matrix, tetromino.row, col)) {
    tetromino.col = col;
  }
}

function moveRight() {
  if (gameOver || paused) return;
  const col = tetromino.col + 1;
  if (isValidMove(tetromino.matrix, tetromino.row, col)) {
    tetromino.col = col;
  }
}

function rotateTetromino() {
  if (gameOver || paused) return;
  const matrix = rotate(tetromino.matrix);
  if (isValidMove(matrix, tetromino.row, tetromino.col)) {
    tetromino.matrix = matrix;
  }
}

function hardDrop() {
  if (gameOver || paused) return;
  while (isValidMove(tetromino.matrix, tetromino.row + 1, tetromino.col)) {
    tetromino.row++;
  }
  placeTetromino();
}

function togglePause() {
  paused = !paused;
  document.getElementById('pause').textContent = paused ? 'Resume' : 'Pause';
  if (!paused) loop();
}

function resetGame() {
  // Reset game state
  for (let row = -2; row < 20; row++) {
    for (let col = 0; col < 10; col++) {
      playfield[row][col] = 0;
    }
  }
  tetrominoSequence.length = 0;
  tetromino = getNextTetromino();
  score = 0;
  gameOver = false;
  paused = false;
  document.getElementById('reset').style.display = 'none';
  document.getElementById('pause').textContent = 'Pause';
  loop();
}

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

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