Vanilla JavaScript Tetris, because everybody should build a Tetris game
Talking about Tetris game, Game development and Javascript.
Today I am showing you a Tetris game written in vanilla JavaScript.
Everybody should be able to build a Tetris game, but, beware that it is not as simple as it seems.
In my fist version, there are three important things missing: wall kicking, Super Rotation System and empty lines above the celing.
Wall kicking is a game mechanic that allows a tetromino (a falling piece) to rotate even when it is partially obstructed by a wall or another block. Instead of the rotation failing, the game attempts to “kick” the piece away from the obstruction by shifting it slightly in one or more directions.
Super Rotation System defines how each tetromino rotates and includes wall kicks to allow smoother movement near walls or obstacles, this way:
All tetrominoes except the O-piece rotate around a central “rotation point.”
Clockwise and counterclockwise rotations use predefined tests for wall kicks.
Wall kicks attempt to shift the piece left, right, or up when the rotation is blocked.
My prototype just rotates arrays.
Empty lines above the ceiling prevent instant game over if a tetromino spawns partially inside a stack of blocks, giving the player a chance to move or rotate it before it locks in place.
A little harder than you imagined, I suppose.
At the moment, here is my prototype in action:
Use ARROW KEYS to move, rotate or drop a piece. You can also play from this page.
And this is the source code, all in one page:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetris</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #222;
margin: 0;
}
canvas {
background-color: black;
border: 2px solid white;
}
</style>
</head>
<body>
<canvas id="thegame"></canvas>
<script>
// get canvas
const canvas = document.getElementById('thegame');
const context = canvas.getContext('2d');
// global constants
const boardRows = 20; // board rows
const boardColumns = 10; // board column
const tileSize = 16; // tile size, in pixels
const board = []; // game board
let dropInterval = 500; // drop interval, in milliseconds
// size and scale canvas according to tile and board size
canvas.width = boardColumns * tileSize;
canvas.height = boardRows * tileSize;
context.scale(tileSize, tileSize);
// array with all tetrominoes
const tetrominoes = [
// I piece
{
color : '#00FFFF',
shape: [[1, 1, 1, 1]]
},
// O piece
{
color : '#FFFF00',
shape: [[1, 1], [1, 1]]
},
// T piece
{
color : '#800080',
shape: [[0, 1, 0], [1, 1, 1]]
},
// S piece
{
color : '#00A000',
shape: [[0, 1, 1], [1, 1, 0]]
},
// Z piece
{
color : '#A00000',
shape: [[1, 1, 0], [0, 1, 1]]
},
// J piece
{
color : '#0000A0',
shape : [[1, 0, 0], [1, 1, 1]]
},
// L pieace
{
color : '#F0A000',
shape:[[0, 0, 1], [1, 1, 1]]
}
];
// get a random tetromino
let randomTetromino = getRandomTetromino();
let currentTetromino = randomTetromino.shape;
let currentColor = randomTetromino.color;
let tetrominoPosition = {
row : 0,
column : 3
};
// drop timer
let dropTimer = 0;
// time since previous frame
let lastTime = 0;
// initialize the board
initializeBoard();
// call update method
update();
// function to initialize the board, setting all values to zero
function initializeBoard() {
for (let i = 0; i < boardRows; i ++) {
board[i] = [];
for (let j = 0; j < boardColumns; j ++) {
board[i][j] = 0;
}
}
}
// game loop
// time: amount of milliseconds passed since script beginning
function update(time = 0) {
// elapsed time since previous time
const deltaTime = time - lastTime;
lastTime = time;
dropTimer += deltaTime;
// if drop timer is bigger than drop interval, it's time to make the tetromino drop
if (dropTimer > dropInterval) {
// reset drop timer
dropTimer = 0;
// move tetromino down
tetrominoPosition.row ++;
// is tetromino colloding?
if (isTetrominoColliding()) {
// move tetromino up
tetrominoPosition.row --;
// merge tetromino
mergeTetromino();
// clear lines if needed
clearLines();
// place a new tetromino
randomTetromino = getRandomTetromino();
currentTetromino = randomTetromino.shape;
currentColor = randomTetromino.color;
tetrominoPosition = {
row : 0,
column : 3
};
// is newborn tetromino already colliding?
if (isTetrominoColliding()) {
// GAME OVER!! (just clear the board in this case)
initializeBoard();
}
}
}
// draw the board
drawBoard();
// draw the falling tetromino
drawTetromino();
// request new frame and call update function (this one)
requestAnimationFrame(update);
}
// function to get a random tetromino
function getRandomTetromino() {
return tetrominoes[Math.floor(Math.random() * tetrominoes.length)];
}
// function to rotate a tetromino
// clockwise: rotate clockwise if true
// it just creates a new array inverting rows with columns
function rotateTetromino(tetromino, clockwise) {
const rows = tetromino.length;
const columns = tetromino[0].length;
let rotatedTetromino = [];
if (clockwise) {
for (let i = 0; i < columns; i ++) {
rotatedTetromino[i] = [];
for (let j = rows - 1; j >= 0; j --) {
rotatedTetromino[i].push(tetromino[j][i]);
}
}
}
else {
for (let i = columns - 1; i >= 0; i --) {
let newRow = [];
for (let j = 0; j < rows; j ++) {
newRow.push(tetromino[j][i]);
}
rotatedTetromino.push(newRow);
}
}
return rotatedTetromino;
}
// functions to merge a tetromino with game board
function mergeTetromino() {
for (let i = 0; i < currentTetromino.length; i ++) {
for (let j = 0; j < currentTetromino[i].length; j ++) {
if (currentTetromino[i][j] != 0) {
board[tetrominoPosition.row + i][tetrominoPosition.column + j] = currentColor;
}
}
}
}
// function to check if a tetromino is colliding
function isTetrominoColliding() {
for (let i = 0; i < currentTetromino.length; i ++) {
for (let j = 0; j < currentTetromino[i].length; j ++) {
if (currentTetromino[i][j] != 0) {
const row = tetrominoPosition.row + i;
const column = tetrominoPosition.column + j;
if (row < 0 || column < 0 || row >= boardRows || column >= boardColumns || board[row][column] != 0) {
return true;
}
}
}
}
return false;
}
// function to clear lines and make above lines fall down
function clearLines() {
for (let i = boardRows - 1; i >= 0; i --) {
let remove = true;
for (let j = 0; j < boardColumns; j ++) {
if (board[i][j] == 0) {
remove = false;
break;
}
}
// make above lines fall down
if (remove) {
for (let k = i; k >= 0; k --) {
for (let j = 0; j < boardColumns; j ++) {
if (k == 0) {
board[k][j] = 0;
}
else {
board[k][j] = board[k - 1][j];
}
}
}
i ++;
}
}
}
// function to draw the board
function drawBoard() {
context.fillStyle = '#000';
context.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < boardRows; i ++) {
for (let j = 0; j < boardColumns; j ++) {
if (board[i][j] != 0) {
context.fillStyle = board[i][j];
context.fillRect(j, i, 1, 1);
}
}
}
}
// function to draw the tetromino
function drawTetromino() {
context.fillStyle = currentColor;
for (let i = 0; i < currentTetromino.length; i ++) {
for (let j = 0; j < currentTetromino[i].length; j ++) {
if (currentTetromino[i][j] != 0) {
context.fillRect(tetrominoPosition.column + j, tetrominoPosition.row + i, 1, 1);
}
}
}
}
// keydown listener
document.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowLeft' :
tetrominoPosition.column --;
if (isTetrominoColliding()) {
tetrominoPosition.column ++;
}
break;
case 'ArrowRight' :
tetrominoPosition.column ++;
if (isTetrominoColliding()) {
tetrominoPosition.column --;
}
break;
case 'ArrowUp' :
currentTetromino = rotateTetromino(currentTetromino, false);
if (isTetrominoColliding()) {
currentTetromino = rotateTetromino(currentTetromino, true);
}
break;
case 'ArrowDown' :
dropInterval = 20;
break;
}
})
// keyup listener
document.addEventListener('keyup', (event) => {
if (event.key == 'ArrowDown') {
dropInterval = 500;
}
});
</script>
</body>
</html>
Will I develop all missing features? Obviously, still using vanilla JS and maybe other languages.
Copy/paste in your projects and have fun.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.