JavaScript

【JavaScript】テトリスを作って解説してみた

takahide

おはようございます。タカヒデです。

本日はJavaScriptで「テトリス」を作ってみました。

STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。
(私が誰よりも初学者ですが…)

こんな人にオススメ
  • プログラミング初心者
  • 何から始めればよいか分からない
  • まずは簡単なゲームを作って興味を持ってみたい

ぜひ実際にコードを打ちながら作成してみてください。

Contents
  1. 「テトリス」完成イメージ
  2. STEP0:画像データのダウンロード
  3. STEP1:テトリスの背景を作成
  4. STEP2:テトリスのブロックを落とす
  5. STEP3:テトリスを移動や回転できるようにする
  6. STEP4:列を揃えたらテトリスを消す
  7. STEP5:次のテトリスとスコアの表示
  8. STEP6:ゲームオーバー表示とリセットボタンの作成
  9. 完成

「テトリス」完成イメージ

まずは、「テトリス」完成後の最終的なコードと完成イメージです。

まずはHTMLです。

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。

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

JavaScript
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で背景を描き、画像を読み込み

HTML
    <canvas id="canvas" width="400" height="800"></canvas>
    <img id="image" src="rotations.png" style="display: none" />
  • テトリスの盤面を描く「キャンバス」を用意
  • ブロック画像が入った「rotations.png」を読み込み
    • 「display:none」で画面には表示しない

手順②:ゲーム全体の設定

JavaScript
const imageSquareSize = 24;
const size = 40;
const framePerSecond = 24;
const gameSpeed = 5;
  • imageSquareSize
    • 画像の1マス
  • size
    • Canvas上で描く1マスのサイズ
  • framePerSecond
    • 1秒間の描画回数
  • gameSpeed
    • 1秒間の更新回数

手順③:ブロックの作成

JavaScript
const shapes = [
    new Tetris(0, 120, [
        [0, 1, 0],
        [0, 1, 0],
        [1, 1, 0],
    ]),
  • 「new Tetris(imageX, imageY, template) 」を複数用意し、テトリスのブロックを作成
  • 「template」が形となり、「1」の部分がブロックとして描画される

手順④:更新や描写の繰り返し処理

JavaScript
let gameLoop = () => {
  setInterval(update, 1000 / gameSpeed);
  setInterval(draw, 1000 / framePerSecond);
};

「gameLoop」でゲームの繰り返し更新を実施

  • update
    • 落下・当たり判定・ライン消去など
  • draw
    • 背景・盤面・ブロックの描画など

手順⑤:テトリスの背景の描画

JavaScript
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++)
    • 白い縦線・横線の描画

手順⑥:落下中のブロックの描写

JavaScript
let drawCurrentTetris = () => {
  ・・・
};

最初にダウンロードしたブロックの画像から「drawImage」で必要なパーツを切り出し描写

手順⑦:フレーム毎の画面作成

JavaScript
let draw = () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawBackground();
  drawSquares();
  drawCurrentTetris();
  drawNextShape();
  if (gameOver) drawGameOver();
};

毎フレーム、一度クリアして→背景や積みあがったブロックなどを重ねて表示

手順⑧:ランダムにブロックを生成

JavaScript
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」でランダムなブロックを生成

手順⑨:盤面を作ってゲームを開始

JavaScript
let resetVars = () => {
    ・・・
};

「10×20」の2次元配列を作り、以下を実施

  • スコア初期化
  • 現在ブロック、次ブロックを生成
  • 盤面セット

この時点で実行するとこんな感じ。
テトリスの背景のみ描写されています。

STEP2:テトリスのブロックを落とす

このSTEPでは、テトリスのブロックを落としていきます。

今回はHTMLは変更なしなので、JavaScriptのみ見ていきます。

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

手順①:ブロックに座標を持たせる

JavaScript
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」
    • 一番上からスタート

手順②:座標をマス目に揃えてズレを防止

JavaScript
getTruncedPosition() {
  return { x: Math.trunc(this.x), y: Math.trunc(this.y) };
}

「getTruncedPosition」によって描画や当たり判定でマス目のズレが生じないように、ブロックの座標を整数に正規化

手順③:落下できるか判定する

JavaScript
checkBottom() {
  ・・・
}
  • そのマスが盤面上のどこにいるか(realX, realY)を計算
  • 1マス下(realY+1)が盤面外なら落下不可
  • 1マス下にすでに固定ブロックがあれば落下不可
  • 全部OKなら落下可能

手順④:ブロックの落下からブロックの固定

JavaScript
let update = () => {
  ・・・
};

「update」で以下の処理を実施

  • 「checkBottom()」が「true」の間は「y += 1」で1マス落とす
  • 落下できない場合ブロックの固定
  • 次のブロックに切り替え

手順⑤:固定されたブロックの描画

JavaScript
let drawSquares = () => {
  ・・・
};
  • gameMap(10×20)を全部チェック
  • 空じゃないマス(imageX != -1)だけ描画

これで実行するとこんな感じ。
ランダムにブロックが落ちてきます。まだブロックを動かすことはできません。

STEP3:テトリスを移動や回転できるようにする

このSTEPでは、テトリスのブロックを移動や回転できるようにしていきます。

今回もHTMLは変更なしなので、JavaScriptのみ見ていきます。

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マス動けるか判定

JavaScript
checkLeft() {
  ・・・
}

checkRight() {
  ・・・
}

「checkLeft」で以下の処理を実行。なお、「checkRight」も同様の処理。

  • 左隣(realX – 1)が盤面外ならNG
  • 左隣に固定ブロックがあるならNG
  • 全部OKなら左移動可能

手順②:左右下へのブロック移動の実装

JavaScript
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座標分動かす処理を実装

手順③:テトリスの回転処理を実装

JavaScript
changeRotation() {
  ・・・
}

「changeRotation」でテトリスブロックの回転処理を実装。

JavaScript
let tempTemplate = []
for (let i = 0; i < this.template.length; i++)
  tempTemplate[i] = this.template[i].slice();

回転を実装する前に、失敗したとき、元に戻せるようにtemplateのバックアップを作る

JavaScript
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;
  }
}

配列をその場で回転させる。

JavaScript
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;」で元に戻す。

手順④:キーボードの入力処理を実装

JavaScript
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のみ見ていきます。

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

手順①:横一列がそろった場合に削除

JavaScript
let deleteCompleteRows = () => {
  ・・・
}

「deleteCompleteRows」でテトリスの横一列がそろった場合に削除する処理を実装。

JavaScript
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つでもあれば未完成
  • 全部埋まっていれば一列が完成
JavaScript
for (let k = i; k > 0; k--) {
  gameMap[k] = gameMap[k - 1];
}
  • 横一列が揃った場合にその行を削除して、その行より上のブロックを1段下に落とす
JavaScript
let temp = [];
for (let j = 0; j < squareCountX; j++) {
    temp.push({ imageX: -1, imageY: -1 });
}
gameMap[0] = temp;
  • 最上段のブロックに空の行を追加

これで横一列がそろった場合に削除する一連の流れが完成。

この処理を「update」の中に記載することを忘れずに。

手順②:ゲームオーバーの判定

JavaScript
currentShape = nextShape;
nextShape = getRandomShape();
if (!currentShape.checkBottom()) {  //STEP4追加
    gameOver = true;
}

「update」の中にゲームオーバー時の判定を追加。
新しいブロックを出した後、ブロックが積みあがって天井まで来ていたらゲームオーバーの判定を実施。

この時点で実行するとこんな感じ。
横一列が揃ったときにブロックが消えるか試してみてください。

STEP5:次のテトリスとスコアの表示

このSTEPでは、次のテトリスとスコアの表示を実装していきます。
完成までもう少し頑張りましょう。

HTML
<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>

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;   //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に次のブロック用とスコア表示用のキャンバスを追加

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を取得するのを忘れずに。

手順②:次に落ちてくるブロックの表示

JavaScript
let drawNextShape = () => {
  ・・・
}

「drawNextShape」で次の落ちてくるブロックの表示を実装。

  • canvasの背景を塗り直す
    • 前のフレームの残像を消すため
  • 「nextShape.template」を走査して、ブロックがあるマスだけ描画

手順③:スコアの表示

JavaScript
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
<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>

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

//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にリセットボタンとスタイルを追加

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>でアイコンにしているので、ここは面倒くさかったらコピペしてください。

手順②:ゲームオーバーの表示の作成

JavaScript
let drawGameOver = () => {
    ctx.font = "64px Poppins";
    ctx.fillStyle = "black";
    ctx.fillText("Game Over", 10, canvas.height / 2);
}

「drawGameOver」でゲームオーバー表示を作成。

ゲームオーバーになると画面中央にテキストが表示される。

実行するとこんな感じ。

リセットボタンが機能しているか。
ゲームオーバーが表示されるか試してみてください。

完成

以上でJavaScriptで作る「テトリス」の完成です。

ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。

ABOUT ME
タカヒデ
タカヒデ
ITを楽しく勉強中
通信会社の企画職で働く30代 非エンジニアでありながら半分趣味でITエンジニアリングやプログラミングを学習中 IT初心者が楽しいと思えるように発信することを目指しています ↓Xもやっているのでフォローしてね
記事URLをコピーしました