I am creating Tetris in javascript. I have finished the basic mechanics. I would appreciate some feedback on how I could improve it.
Down, left, and right arrow keys move the piece while the up arrow key rotates it. It works best in full page mode.
"use strict";
const pieceData = [
{ // O
builds: [
[
[0, 0],
[1, 0],
[0, 1],
[1, 1],
],
],
type: "other",
},
{ // T
builds: [
[
[0, 1],
[1, 1],
[1, 2],
[2, 1],
],
[
[1, 0],
[1, 1],
[2, 1],
[1, 2],
],
[
[1, 0],
[0, 1],
[1, 1],
[2, 1],
],
[
[1, 0],
[1, 1],
[1, 2],
[0, 1],
],
],
type: "other",
},
{ // long bar
builds: [
[
[0, 0],
[1, 0],
[2, 0],
[3, 0],
],
[
[1, -1],
[1, 0],
[1, 1],
[1, 2],
],
],
type: "other",
},
{ // Z
builds: [
[
[0, 0],
[1, 0],
[1, 1],
[2, 1],
],
[
[1, -1],
[1, 0],
[0, 0],
[0, 1],
],
],
type: "left",
},
{ // S
builds: [
[
[1, 0],
[0, 0],
[0, 1],
[-1, 1],
],
[
[0, -1],
[0, 0],
[1, 0],
[1, 1],
],
],
type: "right",
},
{ // L
builds: [
[
[0, 0],
[0, 1],
[0, 2],
[1, 2],
],
[
[-1, 1],
[0, 1],
[1, 1],
[1, 0],
],
[
[-1, 0],
[0, 0],
[0, 1],
[0, 2],
],
[
[-1, 2],
[-1, 1],
[0, 1],
[1, 1],
],
],
type: "right",
},
{ // J
builds: [
[
[1, 0],
[1, 1],
[1, 2],
[0, 2],
],
[
[0, 0],
[0, 1],
[1, 1],
[2, 1],
],
[
[1, 0],
[0, 0],
[0, 1],
[0, 2],
],
[
[-1, 1],
[0, 1],
[1, 1],
[1, 2],
]
],
type: "left",
},
];
const canvas = document.getElementById("display");
const nextPieceDisplay = document.getElementById("next");
const npctx = nextPieceDisplay.getContext("2d");
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const npw = nextPieceDisplay.width;
const nph = nextPieceDisplay.height;
const colors = {
"left": "#118c8b",
"right": "#f14d49",
"other": "#bca18d",
}
class Block {
constructor(x, y, size, type) {
this.x = x;
this.y = y;
this.size = size;
this.type = type;
}
draw(loc) {
loc.fillStyle = colors[this.type];
loc.fillRect(this.x, this.y, this.size, this.size);
loc.strokeRect(this.x, this.y, this.size, this.size);
}
}
class Piece {
constructor(x, y, size, type, builds) {
this.x = x;
this.y = y;
this.size = size;
this.type = type;
this.builds = builds;
this.blocks = [];
this.current = randInt(0, this.builds.length);
this.swap(0);
}
draw(loc) {
this.blocks.forEach(block => {
block.draw(loc);
});
}
swap(dir) {
this.current += dir;
this.current = mod(this.current, this.builds.length);
let newBuild = [];
for (let i = 0; i < this.builds[this.current].length; ++i) {
let pos = this.builds[this.current][i];
newBuild.push(new Block(this.x + pos[0] * this.size, this.y + pos[1] * this.size, this.size, this.type));
}
this.blocks = newBuild;
}
move(x, y) {
this.x += x * this.size;
this.y += y * this.size;
this.blocks.forEach(block => {
block.x += x * this.size;
block.y += y * this.size;
});
}
}
class Board {
constructor(w, h, gridSize) {
this.active = undefined;
this.blocks = [];
this.grid = gridSize;
this.w = w;
this.h = h;
this.shouldLock = false;
this.score = 0;
this.level = 0;
this.nextPieceId = randInt(0, pieceData.length);
}
draw() {
ctx.clearRect(0, 0, this.w, this.h);
this.active.draw(ctx);
this.blocks.forEach(block => {
block.draw(ctx);
});
}
newPiece() {
let data = pieceData[this.nextPieceId];
this.active = new Piece(Math.round(this.w / 2) - this.grid, 0, this.grid, data.type, data.builds);
if (this.pieceBlocked(0, 0)) {
clearInterval(gameLoop);
alert(`Final score: ${this.score}`);
location.reload();
}
this.nextPieceId = randInt(0, pieceData.length);
let tempData = pieceData[this.nextPieceId];
let tempPiece = new Piece(50, 50, 25, tempData.type, tempData.builds);
npctx.clearRect(0, 0, npw, nph);
tempPiece.draw(npctx);
}
findXs(y) {
let xs = [];
for (let i = 0; i < this.blocks.length; ++i) {
if (this.blocks[i].y === y) {
xs.push(this.blocks[i].x);
}
}
return xs;
}
next() {
this.active.swap(1);
if (this.pieceBlocked(0, 0)) {
this.active.swap(-1);
}
else {
this.shouldLock = false;
}
}
findFulls() {
let fulls = [];
for (let y = 0; y < height; y += this.grid) {
let xs = this.findXs(y);
if (xs.length >= this.w / this.grid) {
fulls.push(y);
}
}
return fulls;
}
shift(ys) {
if (ys.length) {
let grid = this.grid;
this.blocks.forEach(block => {
let below = ys.filter(a => a > block.y);
block.y += below.length * grid;
});
}
}
update() {
let noFall = this.pieceBlocked(0, 1);
if (this.shouldLock) {
if (noFall) {
this.lock();
this.newPiece();
return;
}
this.shouldLock = false;
}
if (noFall) {
this.shouldLock = true;
}
else {
this.active.move(0, 1);
}
let fulls = this.findFulls();
for (let i = 0; i < this.blocks.length; ++i) {
if (fulls.includes(this.blocks[i].y)) {
this.blocks[i] = undefined;
}
}
let len = fulls.length;
this.blocks = this.blocks.filter(a => a);
this.shift(fulls);
if (len) {
this.score += len ** 2 * 100;
}
this.level = Math.floor(Math.sqrt(this.score / 1000));
document.getElementById("score").innerText = "Score: " + this.score.toString();
document.getElementById("level").innerText = "Level: " + this.level.toString();
this.draw();
}
handleMovement(x, y) {
if (!this.pieceBlocked(x, y)) {
this.active.move(x, y);
if (x === 0 && y === 1) {
this.score++;
this.shouldLock = true;
}
}
}
blocked(block, xVel, yVel) {
let x = block.x + xVel * this.grid;
let y = block.y + yVel * this.grid;
if (x < 0 || x >= this.w || y >= this.h) {
return true;
}
for (let i = 0; i < this.blocks.length; ++i) {
if (x == this.blocks[i].x && y == this.blocks[i].y) {
return true;
}
}
}
pieceBlocked(xVel, yVel) {
for (let i = 0; i < this.active.blocks.length; ++i) {
if (this.blocked(this.active.blocks[i], xVel, yVel)) {
return true;
}
}
return false;
}
lock() {
this.blocks = this.blocks.concat(this.active.blocks);
this.active = undefined;
this.score += 10;
}
move(x, y) {
if (!this.pieceBlocked(x, y)) {
this.active.move(x, y);
}
}
}
function mod(n, m) {
return ((n % m) + m) % m;
}
function randChoice(arr) {
let rand = randInt(0, arr.length);
return arr[rand];
}
function randInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
const board = new Board(width, height, 25);
let lastLevel = 0;
board.newPiece();
function game() {
board.update();
if (board.level != lastLevel) {
clearInterval(gameLoop);
lastLevel = board.level;
gameLoop = setInterval(game, (1 / lastLevel) * 500)
}
}
let gameLoop = setInterval(game, 500);
addEventListener("keydown", e => {
switch (e.code) {
case "ArrowUp":
board.next();
break;
case "ArrowDown":
board.handleMovement(0, 1);
break;
case "ArrowLeft":
board.handleMovement(-1, 0);
break;
case "ArrowRight":
board.handleMovement(1, 0);
break;
}
board.draw();
});
html {
height: 100%;
display: grid;
}
body {
margin: auto;
}
#display {
border: 3px solid black;
float: left;
}
#info {
float: right;
margin: 10px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Tetris</title>
</head>
<body>
<canvas id="display" width="250" height="450"></canvas>
<div id="info">
<p id="score">Score: 0</p>
<p id="level">Level: 0</p>
<p>Next piece: </p>
<canvas id="next" width="200" height="200"></canvas>
</div>
</body>
</html>