|
<!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> |
To add a "Restart Game" button and the necessary code to make it work, you will need to do the following steps:
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:
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:
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:
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.