【JavaScript】テトリスを作って解説してみた
おはようございます。タカヒデです。
本日はJavaScriptで「テトリス」を作ってみました。
STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。
(私が誰よりも初学者ですが…)
- プログラミング初心者
- 何から始めればよいか分からない
- まずは簡単なゲームを作って興味を持ってみたい
ぜひ実際にコードを打ちながら作成してみてください。
「テトリス」完成イメージ
まずは、「テトリス」完成後の最終的なコードと完成イメージです。
まずはHTMLです。
<html>
<head>
<title>Tetris Game</title>
<style>
button:hover {
background-color: #bca0dc !important;
}
button:active {
background-color: #9969d0 !important;
}
</style>
</head>
<body>
<canvas id="canvas" width="400" height="800"></canvas>
<canvas id="nextShapeCanvas" width="200" height="200" style="position:absolute; top:10px"></canvas>
<canvas id="scoreCanvas" width="200" height="200" style="position:absolute; top:220px"></canvas>
<button style="width: 64px; height: 64px; border-radius: 50%; background-color: gray; border: 0px;"
onClick="resetVars()">
<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
<use class="ytp-svg-shadow" xlink:href="#ytp-id-54"></use>
<path class="ytp-svg-fill" fill="#ffffff"
d="M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z"
id="ytp-id-54"></path>
</svg>
</button>
<img id="image" src="rotations.png" style="display: none" />
<script src="tetris.js"></script>
</body>
</html>そしてJavaScript。
class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
this.x = squareCountX / 2;
this.y = 0;
}
checkBottom() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realY + 1 >= squareCountY) {
return false;
}
if (gameMap[realY + 1][realX].imageX != -1) {
return false;
}
}
}
return true;
}
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}
checkLeft() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX - 1 < 0) {
return false;
}
if (gameMap[realY][realX - 1].imageX != -1) return false;
}
}
return true;
}
checkRight() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX + 1 >= squareCountX) {
return false;
}
if (gameMap[realY][realX + 1].imageX != -1) return false;
}
}
return true;
}
moveRight() {
if (this.checkRight()) {
this.x += 1;
}
}
moveLeft() {
if (this.checkLeft()) {
this.x -= 1;
}
}
moveBottom() {
if (this.checkBottom()) {
this.y += 1;
score += 1;
}
}
changeRotation() {
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
tempTemplate[i] = this.template[i].slice();
let n = this.template.length;
for (let layer = 0; layer < n / 2; layer++) {
let first = layer;
let last = n - 1 - layer;
for (let i = first; i < last; i++) {
let offset = i - first;
let top = this.template[first][i];
this.template[first][i] = this.template[i][last];//top=right
this.template[i][last] = this.template[last][last - offset];//right=bottom
this.template[last][last - offset] = this.template[last - offset][first];//bottom=left
this.template[last - offset][first] = top;//left=top
}
}
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (
realX < 0 ||
realX >= squareCountX ||
realY < 0 ||
realY >= squareCountY
) {
this.template = tempTemplate;
return false;
}
if (gameMap[realY][realX].imageX != -1) {
this.template = tempTemplate;
return false;
}
}
}
}
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const nextShapeCanvas = document.getElementById("nextShapeCanvas");
const scoreCanvas = document.getElementById("scoreCanvas");
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const nctx = nextShapeCanvas.getContext("2d");
const sctx = scoreCanvas.getContext("2d");
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
let deleteCompleteRows = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i]
let isComplete = true;
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) isComplete = false;
}
if (isComplete) {
console.log("complete row");
score += 1000;
for (let k = i; k > 0; k--) {
gameMap[k] = gameMap[k - 1];
}
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 });
}
gameMap[0] = temp;
}
}
}
let update = () => {
if (gameOver) return;
if (currentShape.checkBottom()) {
currentShape.y += 1
} else {
for (let k = 0; k < currentShape.template.length; k++) {
for (let l = 0; l < currentShape.template.length; l++) {
if (currentShape.template[k][l] == 0) continue;
gameMap[currentShape.getTruncedPosition().y + l][
currentShape.getTruncedPosition().x + k
] = { imageX: currentShape.imageX, imageY: currentShape.imageY }
}
}
deleteCompleteRows();
currentShape = nextShape;
nextShape = getRandomShape();
if (!currentShape.checkBottom()) {
gameOver = true;
}
score += 100;
}
};
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
let drawSquares = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i];
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) continue;
ctx.drawImage(
image,
t[j].imageX,
t[j].imageY,
imageSquareSize,
imageSquareSize,
j * size,
i * size,
size,
size
);
}
}
};
let drawNextShape = () => {
nctx.fillStyle = "#bca0dc"
nctx.fillRect(0, 0, nextShapeCanvas.width, nextShapeCanvas.height);
for (let i = 0; i < nextShape.template.length; i++) {
for (let j = 0; j < nextShape.template.length; j++) {
if (nextShape.template[i][j] == 0) continue;
nctx.drawImage(
image,
nextShape.imageX,
nextShape.imageY,
imageSquareSize,
imageSquareSize,
size * i,
size * j + size,
size,
size
);
}
}
}
let drawScore = () => {
sctx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height);
sctx.font = "64px Poppins";
sctx.fillStyle = "black";
sctx.fillText(score, 10, 50);
};
let drawGameOver = () => {
ctx.font = "64px Poppins";
ctx.fillStyle = "black";
ctx.fillText("Game Over", 10, canvas.height / 2);
}
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
drawScore();
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
window.addEventListener("keydown", (event) => {
if (event.key == "ArrowLeft")
currentShape.moveLeft();
else if (event.key == "ArrowUp")
currentShape.changeRotation()
else if (event.key == "ArrowRight")
currentShape.moveRight()
else if (event.key == "ArrowDown")
currentShape.moveBottom()
})
resetVars();
gameLoop()
こんなゲームが作れます。
テトリスの回転は↑キーです。

ちなみにこのテトリスは以下のYouTubeを参考にしています。
そこから一部を修正し、独自の解説を付けたものになるのでご理解ください。
それでは始めましょう。
STEP0:画像データのダウンロード
このアプリでは、以下の画像データを使用します。

テトリスのブロックですね。
↓からダウンロードしておきましょう。
STEP1:テトリスの背景を作成
まずはテトリスの背景となる部分を作成します。
まだ使用しませんが、テトリスのブロックとなる部分も作成するため、最初に多くのコードを書いていきます。
<html>
<head>
<title>Tetris Game</title>
</head>
<body>
<canvas id="canvas" width="400" height="800"></canvas>
<img id="image" src="rotations.png" style="display: none" />
<script src="tetris.js"></script>
</body>
</html>class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
}
checkBottom() { }
checkLeft() { }
checkRight() { }
moveRight() { }
moveLeft() { }
moveBottom() { }
changeRotation() { }
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
let update = () => { };
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
let drawSquares = () => { }
let drawNextShape = () => { }
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
resetVars();
gameLoop()手順①:HTMLで背景を描き、画像を読み込み
<canvas id="canvas" width="400" height="800"></canvas>
<img id="image" src="rotations.png" style="display: none" />- テトリスの盤面を描く「キャンバス」を用意
- ブロック画像が入った「rotations.png」を読み込み
- 「display:none」で画面には表示しない
手順②:ゲーム全体の設定
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
- imageSquareSize
- 画像の1マス
- size
- Canvas上で描く1マスのサイズ
- framePerSecond
- 1秒間の描画回数
- gameSpeed
- 1秒間の更新回数
手順③:ブロックの作成
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
- 「new Tetris(imageX, imageY, template) 」を複数用意し、テトリスのブロックを作成
- 「template」が形となり、「1」の部分がブロックとして描画される
手順④:更新や描写の繰り返し処理
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
「gameLoop」でゲームの繰り返し更新を実施
- update
- 落下・当たり判定・ライン消去など
- draw
- 背景・盤面・ブロックの描画など
手順⑤:テトリスの背景の描画
let drawBackground = () => {
・・・
};- drawRect(0, 0, canvas.width, canvas.height, “#bca0dc”);
- 背景色の塗りつぶし
- for (let i = 0; i < squareCountX + 1; i++)
for (let i = 0; i < squareCountY + 1; i++)- 白い縦線・横線の描画
手順⑥:落下中のブロックの描写
let drawCurrentTetris = () => {
・・・
};最初にダウンロードしたブロックの画像から「drawImage」で必要なパーツを切り出し描写
手順⑦:フレーム毎の画面作成
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
if (gameOver) drawGameOver();
};毎フレーム、一度クリアして→背景や積みあがったブロックなどを重ねて表示
手順⑧:ランダムにブロックを生成
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};「getRandomShape」でランダムなブロックを生成
手順⑨:盤面を作ってゲームを開始
let resetVars = () => {
・・・
};「10×20」の2次元配列を作り、以下を実施
- スコア初期化
- 現在ブロック、次ブロックを生成
- 盤面セット
この時点で実行するとこんな感じ。
テトリスの背景のみ描写されています。

STEP2:テトリスのブロックを落とす
このSTEPでは、テトリスのブロックを落としていきます。
今回はHTMLは変更なしなので、JavaScriptのみ見ていきます。
class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
//STEP2追加
this.x = squareCountX / 2;
this.y = 0;
}
//STEP2変更
checkBottom() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realY + 1 >= squareCountY) {
return false;
}
if (gameMap[realY + 1][realX].imageX != -1) {
return false;
}
}
}
return true;
}
//STEP2追加
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}
checkLeft() { }
checkRight() { }
moveRight() { }
moveLeft() { }
moveBottom() { }
changeRotation() { }
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
//STEP2変更
let update = () => {
if (gameOver) return;
if (currentShape.checkBottom()) {
currentShape.y += 1
} else {
for (let k = 0; k < currentShape.template.length; k++) {
for (let l = 0; l < currentShape.template.length; l++) {
if (currentShape.template[k][l] == 0) continue;
gameMap[currentShape.getTruncedPosition().y + l][
currentShape.getTruncedPosition().x + k
] = { imageX: currentShape.imageX, imageY: currentShape.imageY }
}
}
currentShape = nextShape;
nextShape = getRandomShape();
}
};
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
//STEP2変更
let drawSquares = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i];
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) continue;
ctx.drawImage(
image,
t[j].imageX,
t[j].imageY,
imageSquareSize,
imageSquareSize,
j * size,
i * size,
size,
size
);
}
}
};
let drawNextShape = () => { }
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
resetVars();
gameLoop()手順①:ブロックに座標を持たせる
class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
//STEP2追加
this.x = squareCountX / 2;
this.y = 0;
}「x,y」はブロックの左上の位置です。
- 「x = squareCountX / 2」
- 盤面の真ん中あたりから出現
- 「y = 0」
- 一番上からスタート
手順②:座標をマス目に揃えてズレを防止
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}「getTruncedPosition」によって描画や当たり判定でマス目のズレが生じないように、ブロックの座標を整数に正規化
手順③:落下できるか判定する
checkBottom() {
・・・
}- そのマスが盤面上のどこにいるか(realX, realY)を計算
- 1マス下(realY+1)が盤面外なら落下不可
- 1マス下にすでに固定ブロックがあれば落下不可
- 全部OKなら落下可能
手順④:ブロックの落下からブロックの固定
let update = () => {
・・・
};「update」で以下の処理を実施
- 「checkBottom()」が「true」の間は「y += 1」で1マス落とす
- 落下できない場合ブロックの固定
- 次のブロックに切り替え
手順⑤:固定されたブロックの描画
let drawSquares = () => {
・・・
};- gameMap(10×20)を全部チェック
- 空じゃないマス(imageX != -1)だけ描画
これで実行するとこんな感じ。
ランダムにブロックが落ちてきます。まだブロックを動かすことはできません。

STEP3:テトリスを移動や回転できるようにする
このSTEPでは、テトリスのブロックを移動や回転できるようにしていきます。
今回もHTMLは変更なしなので、JavaScriptのみ見ていきます。
class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
this.x = squareCountX / 2;
this.y = 0;
}
checkBottom() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realY + 1 >= squareCountY) {
return false;
}
if (gameMap[realY + 1][realX].imageX != -1) {
return false;
}
}
}
return true;
}
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}
//STEP3変更
checkLeft() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX - 1 < 0) {
return false;
}
if (gameMap[realY][realX - 1].imageX != -1) return false;
}
}
return true;
}
//STEP3変更
checkRight() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX + 1 >= squareCountX) {
return false;
}
if (gameMap[realY][realX + 1].imageX != -1) return false;
}
}
return true;
}
//STEP3変更
moveRight() {
if (this.checkRight()) {
this.x += 1;
}
}
//STEP3変更
moveLeft() {
if (this.checkLeft()) {
this.x -= 1;
}
}
//STEP3変更
moveBottom() {
if (this.checkBottom()) {
this.y += 1;
}
}
//STEP3変更
changeRotation() {
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
tempTemplate[i] = this.template[i].slice();
let n = this.template.length;
for (let layer = 0; layer < n / 2; layer++) {
let first = layer;
let last = n - 1 - layer;
for (let i = first; i < last; i++) {
let offset = i - first;
let top = this.template[first][i];
this.template[first][i] = this.template[i][last];//top=right
this.template[i][last] = this.template[last][last - offset];//right=bottom
this.template[last][last - offset] = this.template[last - offset][first];//bottom=left
this.template[last - offset][first] = top;//left=top
}
}
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (
realX < 0 ||
realX >= squareCountX ||
realY < 0 ||
realY >= squareCountY
) {
this.template = tempTemplate;
return false;
}
if (gameMap[realY][realX].imageX != -1) {
this.template = tempTemplate;
return false;
}
}
}
}
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
let update = () => {
if (gameOver) return;
if (currentShape.checkBottom()) {
currentShape.y += 1
} else {
for (let k = 0; k < currentShape.template.length; k++) {
for (let l = 0; l < currentShape.template.length; l++) {
if (currentShape.template[k][l] == 0) continue;
gameMap[currentShape.getTruncedPosition().y + l][
currentShape.getTruncedPosition().x + k
] = { imageX: currentShape.imageX, imageY: currentShape.imageY }
}
}
currentShape = nextShape;
nextShape = getRandomShape();
}
};
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
let drawSquares = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i];
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) continue;
ctx.drawImage(
image,
t[j].imageX,
t[j].imageY,
imageSquareSize,
imageSquareSize,
j * size,
i * size,
size,
size
);
}
}
};
let drawNextShape = () => { }
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
//STEP3追加
window.addEventListener("keydown", (event) => {
if (event.key == "ArrowLeft")
currentShape.moveLeft();
else if (event.key == "ArrowUp")
currentShape.changeRotation()
else if (event.key == "ArrowRight")
currentShape.moveRight()
else if (event.key == "ArrowDown")
currentShape.moveBottom()
})
resetVars();
gameLoop()
手順①:左右に1マス動けるか判定
checkLeft() {
・・・
}
checkRight() {
・・・
}「checkLeft」で以下の処理を実行。なお、「checkRight」も同様の処理。
- 左隣(realX – 1)が盤面外ならNG
- 左隣に固定ブロックがあるならNG
- 全部OKなら左移動可能
手順②:左右下へのブロック移動の実装
moveRight() {
if (this.checkRight()) {
this.x += 1;
}
}
moveLeft() {
if (this.checkLeft()) {
this.x -= 1;
}
}
moveBottom() {
if (this.checkBottom()) {
this.y += 1;
}
}「moveRight」「moveLeft」「moveBottom」それぞれで左右と下に1座標分動かす処理を実装
手順③:テトリスの回転処理を実装
changeRotation() {
・・・
}「changeRotation」でテトリスブロックの回転処理を実装。
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
tempTemplate[i] = this.template[i].slice();回転を実装する前に、失敗したとき、元に戻せるようにtemplateのバックアップを作る
let n = this.template.length;
for (let layer = 0; layer < n / 2; layer++) {
let first = layer;
let last = n - 1 - layer;
for (let i = first; i < last; i++) {
let offset = i - first;
let top = this.template[first][i];
this.template[first][i] = this.template[i][last];
this.template[i][last] = this.template[last][last - offset];
this.template[last][last - offset] = this.template[last - offset][first];
this.template[last - offset][first] = top;
}
}
配列をその場で回転させる。
if (realX < 0 || realX >= squareCountX || realY < 0 || realY >= squareCountY) {
this.template = tempTemplate;
return false;
}
if (gameMap[realY][realX].imageX != -1) {
this.template = tempTemplate;
return false;
}
回転時に壁や他のブロックにはみ出していないかを判定。
回転できない場合は「this.template = tempTemplate;」で元に戻す。
手順④:キーボードの入力処理を実装
window.addEventListener("keydown", (event) => {
if (event.key == "ArrowLeft") currentShape.moveLeft();
else if (event.key == "ArrowUp") currentShape.changeRotation()
else if (event.key == "ArrowRight") currentShape.moveRight()
else if (event.key == "ArrowDown") currentShape.moveBottom()
})
キーボード操作でマスの移動や回転をできるようにボタン処理とメソッドを紐づける。
これでテトリスブロックの移動と回転ができるようになりました。
テトリスらしく遊べるようになってきましたね。

STEP4:列を揃えたらテトリスを消す
このSTEPでは、列を揃えたらテトリスを削除する処理を実装していきます。
テトリスで一番気持ち良い部分ですね。
今回もHTMLは変更なしなので、JavaScriptのみ見ていきます。
class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
this.x = squareCountX / 2;
this.y = 0;
}
checkBottom() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realY + 1 >= squareCountY) {
return false;
}
if (gameMap[realY + 1][realX].imageX != -1) {
return false;
}
}
}
return true;
}
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}
checkLeft() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX - 1 < 0) {
return false;
}
if (gameMap[realY][realX - 1].imageX != -1) return false;
}
}
return true;
}
checkRight() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX + 1 >= squareCountX) {
return false;
}
if (gameMap[realY][realX + 1].imageX != -1) return false;
}
}
return true;
}
moveRight() {
if (this.checkRight()) {
this.x += 1;
}
}
moveLeft() {
if (this.checkLeft()) {
this.x -= 1;
}
}
moveBottom() {
if (this.checkBottom()) {
this.y += 1;
}
}
changeRotation() {
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
tempTemplate[i] = this.template[i].slice();
let n = this.template.length;
for (let layer = 0; layer < n / 2; layer++) {
let first = layer;
let last = n - 1 - layer;
for (let i = first; i < last; i++) {
let offset = i - first;
let top = this.template[first][i];
this.template[first][i] = this.template[i][last];//top=right
this.template[i][last] = this.template[last][last - offset];//right=bottom
this.template[last][last - offset] = this.template[last - offset][first];//bottom=left
this.template[last - offset][first] = top;//left=top
}
}
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (
realX < 0 ||
realX >= squareCountX ||
realY < 0 ||
realY >= squareCountY
) {
this.template = tempTemplate;
return false;
}
if (gameMap[realY][realX].imageX != -1) {
this.template = tempTemplate;
return false;
}
}
}
}
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
//STEP4追加
let deleteCompleteRows = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i]
let isComplete = true;
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) isComplete = false;
}
if (isComplete) {
console.log("complete row");
for (let k = i; k > 0; k--) {
gameMap[k] = gameMap[k - 1];
}
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 });
}
gameMap[0] = temp;
}
}
}
let update = () => {
if (gameOver) return;
if (currentShape.checkBottom()) {
currentShape.y += 1
} else {
for (let k = 0; k < currentShape.template.length; k++) {
for (let l = 0; l < currentShape.template.length; l++) {
if (currentShape.template[k][l] == 0) continue;
gameMap[currentShape.getTruncedPosition().y + l][
currentShape.getTruncedPosition().x + k
] = { imageX: currentShape.imageX, imageY: currentShape.imageY }
}
}
deleteCompleteRows(); //STEP4追加
currentShape = nextShape;
nextShape = getRandomShape();
if (!currentShape.checkBottom()) { //STEP4追加
gameOver = true;
}
}
};
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
let drawSquares = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i];
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) continue;
ctx.drawImage(
image,
t[j].imageX,
t[j].imageY,
imageSquareSize,
imageSquareSize,
j * size,
i * size,
size,
size
);
}
}
};
let drawNextShape = () => { }
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
window.addEventListener("keydown", (event) => {
if (event.key == "ArrowLeft")
currentShape.moveLeft();
else if (event.key == "ArrowUp")
currentShape.changeRotation()
else if (event.key == "ArrowRight")
currentShape.moveRight()
else if (event.key == "ArrowDown")
currentShape.moveBottom()
})
resetVars();
gameLoop()
手順①:横一列がそろった場合に削除
let deleteCompleteRows = () => {
・・・
}「deleteCompleteRows」でテトリスの横一列がそろった場合に削除する処理を実装。
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i]
let isComplete = true;
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) isComplete = false;
}- 横一列のブロックをチェックし、空マス(imageX == -1)が1つでもあれば未完成
- 全部埋まっていれば一列が完成
for (let k = i; k > 0; k--) {
gameMap[k] = gameMap[k - 1];
}- 横一列が揃った場合にその行を削除して、その行より上のブロックを1段下に落とす
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 });
}
gameMap[0] = temp;- 最上段のブロックに空の行を追加
これで横一列がそろった場合に削除する一連の流れが完成。
この処理を「update」の中に記載することを忘れずに。
手順②:ゲームオーバーの判定
currentShape = nextShape;
nextShape = getRandomShape();
if (!currentShape.checkBottom()) { //STEP4追加
gameOver = true;
}「update」の中にゲームオーバー時の判定を追加。
新しいブロックを出した後、ブロックが積みあがって天井まで来ていたらゲームオーバーの判定を実施。
この時点で実行するとこんな感じ。
横一列が揃ったときにブロックが消えるか試してみてください。

STEP5:次のテトリスとスコアの表示
このSTEPでは、次のテトリスとスコアの表示を実装していきます。
完成までもう少し頑張りましょう。
<html>
<head>
<title>Tetris Game</title>
</head>
<body>
<canvas id="canvas" width="400" height="800"></canvas>
<!-- STEP5追加 -->
<canvas id="nextShapeCanvas" width="200" height="200" style="position:absolute; top:10px"></canvas>
<canvas id="scoreCanvas" width="200" height="200" style="position:absolute; top:220px"></canvas>
<img id="image" src="rotations.png" style="display: none" />
<script src="tetris.js"></script>
</body>
</html>class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
this.x = squareCountX / 2;
this.y = 0;
}
checkBottom() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realY + 1 >= squareCountY) {
return false;
}
if (gameMap[realY + 1][realX].imageX != -1) {
return false;
}
}
}
return true;
}
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}
checkLeft() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX - 1 < 0) {
return false;
}
if (gameMap[realY][realX - 1].imageX != -1) return false;
}
}
return true;
}
checkRight() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX + 1 >= squareCountX) {
return false;
}
if (gameMap[realY][realX + 1].imageX != -1) return false;
}
}
return true;
}
moveRight() {
if (this.checkRight()) {
this.x += 1;
}
}
moveLeft() {
if (this.checkLeft()) {
this.x -= 1;
}
}
moveBottom() {
if (this.checkBottom()) {
this.y += 1;
score += 1; //STEP5追加
}
}
changeRotation() {
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
tempTemplate[i] = this.template[i].slice();
let n = this.template.length;
for (let layer = 0; layer < n / 2; layer++) {
let first = layer;
let last = n - 1 - layer;
for (let i = first; i < last; i++) {
let offset = i - first;
let top = this.template[first][i];
this.template[first][i] = this.template[i][last];//top=right
this.template[i][last] = this.template[last][last - offset];//right=bottom
this.template[last][last - offset] = this.template[last - offset][first];//bottom=left
this.template[last - offset][first] = top;//left=top
}
}
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (
realX < 0 ||
realX >= squareCountX ||
realY < 0 ||
realY >= squareCountY
) {
this.template = tempTemplate;
return false;
}
if (gameMap[realY][realX].imageX != -1) {
this.template = tempTemplate;
return false;
}
}
}
}
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const nextShapeCanvas = document.getElementById("nextShapeCanvas"); //STEP5追加
const scoreCanvas = document.getElementById("scoreCanvas"); //STEP5追加
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const nctx = nextShapeCanvas.getContext("2d"); //STEP5追加
const sctx = scoreCanvas.getContext("2d"); //STEP5追加
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
let deleteCompleteRows = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i]
let isComplete = true;
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) isComplete = false;
}
if (isComplete) {
console.log("complete row");
score += 1000; //STEP5追加
for (let k = i; k > 0; k--) {
gameMap[k] = gameMap[k - 1];
}
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 });
}
gameMap[0] = temp;
}
}
}
let update = () => {
if (gameOver) return;
if (currentShape.checkBottom()) {
currentShape.y += 1
} else {
for (let k = 0; k < currentShape.template.length; k++) {
for (let l = 0; l < currentShape.template.length; l++) {
if (currentShape.template[k][l] == 0) continue;
gameMap[currentShape.getTruncedPosition().y + l][
currentShape.getTruncedPosition().x + k
] = { imageX: currentShape.imageX, imageY: currentShape.imageY }
}
}
deleteCompleteRows();
currentShape = nextShape;
nextShape = getRandomShape();
if (!currentShape.checkBottom()) {
gameOver = true;
}
score += 100; //STEP5追加
}
};
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
let drawSquares = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i];
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) continue;
ctx.drawImage(
image,
t[j].imageX,
t[j].imageY,
imageSquareSize,
imageSquareSize,
j * size,
i * size,
size,
size
);
}
}
};
//STEP5変更
let drawNextShape = () => {
nctx.fillStyle = "#bca0dc"
nctx.fillRect(0, 0, nextShapeCanvas.width, nextShapeCanvas.height);
for (let i = 0; i < nextShape.template.length; i++) {
for (let j = 0; j < nextShape.template.length; j++) {
if (nextShape.template[i][j] == 0) continue;
nctx.drawImage(
image,
nextShape.imageX,
nextShape.imageY,
imageSquareSize,
imageSquareSize,
size * i,
size * j + size,
size,
size
);
}
}
}
//STEP5追加
let drawScore = () => {
sctx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height);
sctx.font = "64px Poppins";
sctx.fillStyle = "black";
sctx.fillText(score, 10, 50);
};
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
drawScore(); //STEP5追加
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
window.addEventListener("keydown", (event) => {
if (event.key == "ArrowLeft")
currentShape.moveLeft();
else if (event.key == "ArrowUp")
currentShape.changeRotation()
else if (event.key == "ArrowRight")
currentShape.moveRight()
else if (event.key == "ArrowDown")
currentShape.moveBottom()
})
resetVars();
gameLoop()手順①:HTMLに次のブロック用とスコア表示用のキャンバスを追加
<canvas id="nextShapeCanvas" width="200" height="200" style="position:absolute; top:10px"></canvas>
<canvas id="scoreCanvas" width="200" height="200" style="position:absolute; top:220px"></canvas>
「canvas」を使って、次のブロック用とスコア表示用のキャンバスを追加。
JavaScript側で「getElementById」によってIDを取得するのを忘れずに。
手順②:次に落ちてくるブロックの表示
let drawNextShape = () => {
・・・
}「drawNextShape」で次の落ちてくるブロックの表示を実装。
- canvasの背景を塗り直す
- 前のフレームの残像を消すため
- 「nextShape.template」を走査して、ブロックがあるマスだけ描画
手順③:スコアの表示
let drawScore = () => {
sctx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height);
sctx.font = "64px Poppins";
sctx.fillStyle = "black";
sctx.fillText(score, 10, 50);
};「drawScore」でスコアを表示。
以下のポイント加算処理を「moveBottom」「update」「deleteCompleteRows」メソッドでそれぞれ追記する。
- ↓キーを押す(+1)
- ブロックを設置(+100)
- 1行消す(+1,000)
実行するとこんな感じ。
次のブロック表示やスコア加算がされているか確認しましょう。

STEP6:ゲームオーバー表示とリセットボタンの作成
このSTEPでは、ゲームオーバー表示とリセットボタンの作成していきます。
これが最後のSTEPです。
<html>
<head>
<title>Tetris Game</title>
<!-- STEP6追加 -->
<style>
button:hover {
background-color: #bca0dc !important;
}
button:active {
background-color: #9969d0 !important;
}
</style>
</head>
<body>
<canvas id="canvas" width="400" height="800"></canvas>
<canvas id="nextShapeCanvas" width="200" height="200" style="position:absolute; top:10px"></canvas>
<canvas id="scoreCanvas" width="200" height="200" style="position:absolute; top:220px"></canvas>
<!-- STEP6追加 -->
<button style="width: 64px; height: 64px; border-radius: 50%; background-color: gray; border: 0px;"
onClick="resetVars()">
<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
<use class="ytp-svg-shadow" xlink:href="#ytp-id-54"></use>
<path class="ytp-svg-fill" fill="#ffffff"
d="M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z"
id="ytp-id-54"></path>
</svg>
</button>
<img id="image" src="rotations.png" style="display: none" />
<script src="tetris.js"></script>
</body>
</html>class Tetris {
constructor(imageX, imageY, template) {
this.imageY = imageY;
this.imageX = imageX;
this.template = template;
this.x = squareCountX / 2;
this.y = 0;
}
checkBottom() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realY + 1 >= squareCountY) {
return false;
}
if (gameMap[realY + 1][realX].imageX != -1) {
return false;
}
}
}
return true;
}
getTruncedPosition() {
return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}
checkLeft() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX - 1 < 0) {
return false;
}
if (gameMap[realY][realX - 1].imageX != -1) return false;
}
}
return true;
}
checkRight() {
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (realX + 1 >= squareCountX) {
return false;
}
if (gameMap[realY][realX + 1].imageX != -1) return false;
}
}
return true;
}
moveRight() {
if (this.checkRight()) {
this.x += 1;
}
}
moveLeft() {
if (this.checkLeft()) {
this.x -= 1;
}
}
moveBottom() {
if (this.checkBottom()) {
this.y += 1;
score += 1;
}
}
changeRotation() {
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
tempTemplate[i] = this.template[i].slice();
let n = this.template.length;
for (let layer = 0; layer < n / 2; layer++) {
let first = layer;
let last = n - 1 - layer;
for (let i = first; i < last; i++) {
let offset = i - first;
let top = this.template[first][i];
this.template[first][i] = this.template[i][last];//top=right
this.template[i][last] = this.template[last][last - offset];//right=bottom
this.template[last][last - offset] = this.template[last - offset][first];//bottom=left
this.template[last - offset][first] = top;//left=top
}
}
for (let i = 0; i < this.template.length; i++) {
for (let j = 0; j < this.template.length; j++) {
if (this.template[i][j] == 0) continue;
let realX = i + this.getTruncedPosition().x;
let realY = j + this.getTruncedPosition().y;
if (
realX < 0 ||
realX >= squareCountX ||
realY < 0 ||
realY >= squareCountY
) {
this.template = tempTemplate;
return false;
}
if (gameMap[realY][realX].imageX != -1) {
this.template = tempTemplate;
return false;
}
}
}
}
}
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
const canvas = document.getElementById("canvas");
const nextShapeCanvas = document.getElementById("nextShapeCanvas");
const scoreCanvas = document.getElementById("scoreCanvas");
const image = document.getElementById("image");
const ctx = canvas.getContext("2d");
const nctx = nextShapeCanvas.getContext("2d");
const sctx = scoreCanvas.getContext("2d");
const squareCountX = canvas.width / size;
const squareCountY = canvas.height / size;
const shapes = [
new Tetris(0, 120, [
[0, 1, 0],
[0, 1, 0],
[1, 1, 0],
]),
new Tetris(0, 96, [
[0, 0, 0],
[1, 1, 1],
[0, 1, 0],
]),
new Tetris(0, 72, [
[0, 1, 0],
[0, 1, 0],
[0, 1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[0, 1, 1],
[1, 1, 0],
]),
new Tetris(0, 24, [
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
]),
new Tetris(0, 0, [
[1, 1],
[1, 1],
]),
new Tetris(0, 48, [
[0, 0, 0],
[1, 1, 0],
[0, 1, 1],
]),
];
let gameMap;
let gameOver;
let currentShape;
let nextShape;
let score;
let initialTwoDArr;
let whiteLineThickness = 4
let gameLoop = () => {
setInterval(update, 1000 / gameSpeed);
setInterval(draw, 1000 / framePerSecond);
};
let deleteCompleteRows = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i]
let isComplete = true;
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) isComplete = false;
}
if (isComplete) {
console.log("complete row");
score += 1000;
for (let k = i; k > 0; k--) {
gameMap[k] = gameMap[k - 1];
}
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 });
}
gameMap[0] = temp;
}
}
}
let update = () => {
if (gameOver) return;
if (currentShape.checkBottom()) {
currentShape.y += 1
} else {
for (let k = 0; k < currentShape.template.length; k++) {
for (let l = 0; l < currentShape.template.length; l++) {
if (currentShape.template[k][l] == 0) continue;
gameMap[currentShape.getTruncedPosition().y + l][
currentShape.getTruncedPosition().x + k
] = { imageX: currentShape.imageX, imageY: currentShape.imageY }
}
}
deleteCompleteRows();
currentShape = nextShape;
nextShape = getRandomShape();
if (!currentShape.checkBottom()) {
gameOver = true;
}
score += 100;
}
};
let drawRect = (x, y, width, height, color) => {
ctx.fillStyle = color;
ctx.fillRect(x, y, width, height);
}
let drawBackground = () => {
drawRect(0, 0, canvas.width, canvas.height, "#bca0dc");
for (let i = 0; i < squareCountX + 1; i++) {
drawRect(
size * i - whiteLineThickness,
0,
whiteLineThickness,
canvas.height,
"white"
);
}
for (let i = 0; i < squareCountY + 1; i++) {
drawRect(
0,
size * i - whiteLineThickness,
canvas.width,
whiteLineThickness,
"white"
);
}
};
let drawCurrentTetris = () => {
for (let i = 0; i < currentShape.template.length; i++) {
for (let j = 0; j < currentShape.template.length; j++) {
if (currentShape.template[i][j] == 0) continue;
ctx.drawImage(
image,
currentShape.imageX,
currentShape.imageY,
imageSquareSize,
imageSquareSize,
Math.trunc(currentShape.x) * size + size * i,
Math.trunc(currentShape.y) * size + size * j,
size,
size,
);
}
}
};
let drawSquares = () => {
for (let i = 0; i < gameMap.length; i++) {
let t = gameMap[i];
for (let j = 0; j < t.length; j++) {
if (t[j].imageX == -1) continue;
ctx.drawImage(
image,
t[j].imageX,
t[j].imageY,
imageSquareSize,
imageSquareSize,
j * size,
i * size,
size,
size
);
}
}
};
let drawNextShape = () => {
nctx.fillStyle = "#bca0dc"
nctx.fillRect(0, 0, nextShapeCanvas.width, nextShapeCanvas.height);
for (let i = 0; i < nextShape.template.length; i++) {
for (let j = 0; j < nextShape.template.length; j++) {
if (nextShape.template[i][j] == 0) continue;
nctx.drawImage(
image,
nextShape.imageX,
nextShape.imageY,
imageSquareSize,
imageSquareSize,
size * i,
size * j + size,
size,
size
);
}
}
}
let drawScore = () => {
sctx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height);
sctx.font = "64px Poppins";
sctx.fillStyle = "black";
sctx.fillText(score, 10, 50);
};
//STEP6追加
let drawGameOver = () => {
ctx.font = "64px Poppins";
ctx.fillStyle = "black";
ctx.fillText("Game Over", 10, canvas.height / 2);
}
let draw = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBackground();
drawSquares();
drawCurrentTetris();
drawNextShape();
drawScore();
if (gameOver) {
drawGameOver();
}
};
let getRandomShape = () => {
const base = shapes[Math.floor(Math.random() * shapes.length)];
return new Tetris(
base.imageX,
base.imageY,
base.template.map(row => row.slice())
);
};
let resetVars = () => {
initialTwoDArr = [];
for (let i = 0; i < squareCountY; i++) {
let temp = [];
for (let j = 0; j < squareCountX; j++) {
temp.push({ imageX: -1, imageY: -1 })
}
initialTwoDArr.push(temp)
}
score = 0;
gameOver = false;
currentShape = getRandomShape();
nextShape = getRandomShape();
gameMap = initialTwoDArr;
};
window.addEventListener("keydown", (event) => {
if (event.key == "ArrowLeft")
currentShape.moveLeft();
else if (event.key == "ArrowUp")
currentShape.changeRotation()
else if (event.key == "ArrowRight")
currentShape.moveRight()
else if (event.key == "ArrowDown")
currentShape.moveBottom()
})
resetVars();
gameLoop()手順①:HTMLにリセットボタンとスタイルを追加
<head>
<style>
button:hover {
background-color: #bca0dc !important;
}
button:active {
background-color: #9969d0 !important;
}
</style>
</head>
<body>
<button style="width: 64px; height: 64px; border-radius: 50%; background-color: gray; border: 0px;"
onClick="resetVars()">
<svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
<use class="ytp-svg-shadow" xlink:href="#ytp-id-54"></use>
<path class="ytp-svg-fill" fill="#ffffff"
d="M 18,11 V 7 l -5,5 5,5 v -4 c 3.3,0 6,2.7 6,6 0,3.3 -2.7,6 -6,6 -3.3,0 -6,-2.7 -6,-6 h -2 c 0,4.4 3.6,8 8,8 4.4,0 8,-3.6 8,-8 0,-4.4 -3.6,-8 -8,-8 z"
id="ytp-id-54"></path>
</svg>
</button>
</body>HTMLにリセットボタンとスタイルを追加。
ボタンを押すことで「resetVars」が呼び出される。
ボタンの中身は<svg>でアイコンにしているので、ここは面倒くさかったらコピペしてください。
手順②:ゲームオーバーの表示の作成
let drawGameOver = () => {
ctx.font = "64px Poppins";
ctx.fillStyle = "black";
ctx.fillText("Game Over", 10, canvas.height / 2);
}「drawGameOver」でゲームオーバー表示を作成。
ゲームオーバーになると画面中央にテキストが表示される。
実行するとこんな感じ。
リセットボタンが機能しているか。
ゲームオーバーが表示されるか試してみてください。

完成
以上でJavaScriptで作る「テトリス」の完成です。
ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。
