お久しぶりです。前回投稿が2022-08-15なので、サボりすぎですね…

情報のインプットは好奇心から頑張れるというか知識を得るのが楽しいのでやるんですが、情報のアウトプットって既に知っている知識を使うことなので気が進まないものですよね…定着させるには大事なんですが、特に定着させるモチベーションもなく…

ただ、やっとそのアウトプットを勝手にやってくれる時代がやってきました。そうChatGPTです。こやつにコンテンツを作ってもらう取り組みがどれくらい続くかチャレンジをやってみようと思い、ChatGTP Plus課金しちゃいました!

こやつを使って今日はテトリス作っちゃいます!

完成形

テトリスって、どんなブロックがあってどういうふうに回転してどこで壁にぶつかってとか、まじめに実装すると考えることいっぱいあるのですが、そんなことを考えずに一瞬で作ってくれました。天才です。

が、それを作ってから手直ししていく過程が大変だったので、会話の過程をご覧いただけたらと思います。

会話highlight

一瞬でコード書けるスーパーエンジニア

## USER
# 命令書:
あなたはフロントエンドエンジニアです
以下の制約条件と入力文をもとにゲームのソースコードを出力してください

# 制約条件:
* HTMLCSSJavascriptのみで動く

# 入力文
テトリス

# 出力文

深津式プロンプト(https://chatgpt-lab.com/n/ne1643b7f10e6)を使ってみようとしたのですが、大して制約条件を書くことなく出力しちゃいました。

index.html を作って動かしたらこれ

もうエンジニアいらんやん…

すぐ解決策に走る初心者エンジニア

なんか右の画面いっぱいまでブロックが動かせなくて質問するのですが…

これやっても直りませんでした。どこが問題かを分析せず解決策に走る初心者エンジニアみたいです。でも、根気よく質問すると…

ちゃんとうごきました!

「コード見せて」といってくるベテランエンジニア

「次のブロックを表示」という機能追加もちょっとバグっていて、いろいろ質問して直そうとしたのですが

なかなか直らないとみたやコード全体を要求してきました。今度はこっちがなかなかバグを治せない初心者エンジニアみたいです…

かわいい

最後にお礼にコードレビューしてみると…?

GPT-4の使用上限にひっかかって、GPT-3.5になりました。。。コンテキストをちゃんと解釈してくれてない回答感ありますよね…KY…

ChatGPTと喋った内容

Takaaki

# 命令書:
あなたはフロントエンドエンジニアです
以下の制約条件と入力文をもとにゲームのソースコードを出力してください

# 制約条件:
* HTMLCSSJavascriptのみで動く

# 入力文
テトリス

# 出力文

ChatGPT

以下は、HTML、CSS、Javascriptを使用してテトリスゲームを実装するための簡単なソースコードです。このコードをhtmlファイルにコピーして実行することができます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>テトリス</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        canvas {
            border: 1px solid #000;
        }
    </style>
</head>
<body>
    <canvas id="game" width="320" height="640"></canvas>
    <script>
        const canvas = document.getElementById("game");
        const context = canvas.getContext("2d");
        const scale = 20;

        context.scale(scale, scale);

        const arenaSweep = () => {
            outer: for (let y = arena.length - 1; y > 0; y--) {
                for (let x = 0; x < arena[y].length; x++) {
                    if (!arena[y][x]) {
                        continue outer;
                    }
                }
                const row = arena.splice(y, 1)[0].fill(0);
                arena.unshift(row);
                ++y;
            }
        };

        const collide = (arena, player) => {
            const [m, o] = [player.matrix, player.pos];
            for (let y = 0; y < m.length; ++y) {
                for (let x = 0; x < m[y].length; ++x) {
                    if (m[y][x] && (arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) {
                        return true;
                    }
                }
            }
            return false;
        };

        const createMatrix = (w, h) => {
            const matrix = [];
            while (h--) {
                matrix.push(new Array(w).fill(0));
            }
            return matrix;
        };

        const draw = () => {
            context.fillStyle = "#000";
            context.fillRect(0, 0, canvas.width, canvas.height);

            drawMatrix(arena, { x: 0, y: 0 });
            drawMatrix(player.matrix, player.pos);
        };

        const drawMatrix = (matrix, offset) => {
            matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value) {
                        context.fillStyle = "red";
                        context.fillRect(x + offset.x, y + offset.y, 1, 1);
                    }
                });
            });
        };

        const merge = (arena, player) => {
            player.matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value) {
                        arena[y + player.pos.y][x + player.pos.x] = value;
                    }
                });
            });
        };

        const playerDrop = () => {
            player.pos.y++;
            if (collide(arena, player)) {
                player.pos.y--;
                merge(arena, player);
                playerReset();
                arenaSweep();
            }
            dropCounter = 0;
        };

        const playerMove = (offset) => {
            player.pos.x += offset;
            if (collide(

#### Takaaki
続けてください

#### ChatGPT
arena, player)) {
                player.pos.x -= offset;
            }
        };

        const playerReset = () => {
            const pieces = "TJLOSZI";
            player.matrix = createPiece(pieces[(pieces.length * Math.random()) | 0]);
            player.pos.y = 0;
            player.pos.x = ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

            if (collide(arena, player)) {
                arena.forEach(row => row.fill(0));
                alert("ゲームオーバー");
            }
        };

        const playerRotate = (dir) => {
            const pos = player.pos.x;
            let offset = 1;
            rotate(player.matrix, dir);

            while (collide(arena, player)) {
                player.pos.x += offset;
                offset = -(offset + (offset > 0 ? 1 : -1));

                if (offset > player.matrix[0].length) {
                    rotate(player.matrix, -dir);
                    player.pos.x = pos;
                    return;
                }
            }
        };

        const rotate = (matrix, dir) => {
            for (let y = 0; y < matrix.length; ++y) {
                for (let x = 0; x < y; ++x) {
                    [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
                }
            }

            if (dir > 0) {
                matrix.forEach(row => row.reverse());
            } else {
                matrix.reverse();
            }
        };

        const createPiece = (type) => {
            if (type === "T") {
                return [
                    [1, 1, 1],
                    [0, 1, 0],
                    [0, 0, 0]
                ];
            } else if (type === "J") {
                return [
                    [0, 1, 0],
                    [0, 1, 0],
                    [1, 1, 0]
                ];
            } else if (type === "L") {
                return [
                    [0, 1, 0],
                    [0, 1, 0],
                    [0, 1, 1]
                ];
            } else if (type === "O") {
                return [
                    [1, 1],
                    [1, 1]
                ];
            } else if (type === "S") {
                return [
                    [0, 1, 1],
                    [1, 1, 0],
                    [0, 0, 0]
                ];
            } else if (type === "Z") {
                return [
                    [1, 1, 0],
                    [0, 1, 1],
                    [0, 0, 0]
                ];
            } else if (type === "I") {
                return [
                    [0, 1, 0, 0],
                    [0, 1, 0, 0],
                    [0, 1, 0, 0],
                    [0, 1, 0, 0]
                ];
            }
        };

        let dropCounter = 0;
        let dropInterval = 1000;
        let lastTime = 0;

        const update = (time = 0) => {
            const deltaTime = time - lastTime;
            lastTime = time;

            dropCounter += deltaTime;
            if (dropCounter > dropInterval) {
                playerDrop();
            }

            draw();
            requestAnimationFrame(update);
        };



#### Takaaki
続けてください

#### ChatGPT
const arena = createMatrix(10, 20);

        const player = {
            pos: { x: 0, y: 0 },
            matrix: null
        };

        document.addEventListener("keydown", event => {
            if (event.keyCode === 37) {
                playerMove(-1);
            } else if (event.keyCode === 39) {
                playerMove(1);
            } else if (event.keyCode === 40) {
                playerDrop();
            } else if (event.keyCode === 81) {
                playerRotate(-1);
            } else if (event.keyCode === 87 || event.keyCode === 38) {
                playerRotate(1);
            }
        });

        playerReset();
        update();
    </script>
</body>
</html>

このコードは、テトリスゲームの基本的な機能を実装しています。プレイヤーは矢印キーを使ってピースを操作し、Qキーで反時計回りに回転、Wキーまたは上矢印キーで時計回りに回転させることができます。行が埋まると消え、ゲームが続行します。ピースがスタックして画面上部に達すると、ゲームオーバーになります。このコードを自由にカスタマイズして、さらに機能を追加したり、見た目を改善したりすることができます。

Takaaki

早速ありがとうございます、コードを貼り付けて動かしてみるとちゃんとブロックが落ちてきたので感動しています!

1点、ブロックが左から10列の範囲しか動かせず行が埋まることがなくなってしまっているのですがどこをどうやって直せばいいでしょうか?

ChatGPT

お力になれてうれしいです!ご指摘の問題について、playerMove() 関数内の衝突判定が適切に行われていないことが原因と思われます。collide() 関数を呼び出す前に、移動後のプレイヤーの位置が適切な範囲にあることを確認するために、追加の条件を追加してください。以下に修正を加えた playerMove() 関数を示します。

const playerMove = (offset) => {
    player.pos.x += offset;
    if (collide(arena, player) || player.pos.x < 0 || player.pos.x + player.matrix[0].length > arena[0].length) {
        player.pos.x -= offset;
    }
};

この修正により、プレイヤーがブロックを左右に動かすときに、画面外や既存のブロックとの衝突が適切に判定されます。これでブロックが10列の範囲内で適切に動作し、行が埋まるようになるはずです。

Takaaki

あれ、おっしゃった修正を加えると壁に衝突していないのに左に動かせない場合がでてきまいました…

これでブロックが10列の範囲内で適切に動作し、行が埋まるようになるはずです。

ゲーム画面は10列以上あるようにみえていて、右にブロックが置けない範囲があるのを直したいです!

ChatGPT

すみません、私の解釈が誤っていました。画面の幅に関する問題について、createMatrix() 関数で作成された arena の幅が適切でない可能性があります。10 列の代わりに、画面全体をカバーするために幅を 16 列に変更してみてください。

const arena = createMatrix(16, 20);

また、playerMove()関数の修正に関しては、以下のように、壁に衝突していない場合だけ左右に動かせるようになります。

const playerMove = (offset) => {
    player.pos.x += offset;
    if (collide(arena, player)) {
        player.pos.x -= offset;
    }
};

これで、ゲーム画面が16列になり、左右の壁にぶつからずにブロックを動かせるはずです。また、行が埋まるようになります。

Takaaki

ちゃんと動きました、ありがとうございます〜!

ちょっと実際にゲームしていると次にどんなブロックが来るかわからないのが辛くて、次にどんなブロックが落ちてくるのか表示するようにしてもらえませんか?

ChatGPT

もちろんです!次に来るブロックを表示するために、次の手順を実行してください。

  1. 次のブロック用のキャンバスを追加します。
  2. 次のブロックを生成し、表示するための関数を追加します。
  3. ブロックがリセットされるたびに、次のブロックを表示します。

それでは、以下の手順に従ってコードを修正していきましょう。

Step 1: 次のブロック用のキャンバスを追加

まず、HTML部分に次のブロック用のキャンバスを追加し、CSSで適切にスタイルを調整します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>テトリス</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        canvas {
            border: 1px solid #000;
        }

        #game {
            margin-right: 20px;
        }

        .container {
            display: flex;
            align-items: center;
        }
    </style>
</head>
<body>
    <div class="container">
        <canvas id="game" width="320" height="640"></canvas>
        <div>
            <p>次のブロック:</p>
            <canvas id="next" width="80" height="80"></canvas>
        </div>
    </div>
    <script>
        // 続くJavaScriptコード
    </script>
</body>
</html>

Step 2: 次のブロックを生成し、表示するための関数を追加

次に、JavaScript部分に次のブロックを生成し、表示するための関数を追加します。

const nextCanvas = document.getElementById("next");
const nextContext = nextCanvas.getContext("2d");
nextContext.scale(scale, scale);

let nextPiece = null;

const generateNextPiece = () => {
    const pieces = "TJLOSZI";
    nextPiece = createPiece(pieces[(pieces.length * Math.random()) | 0]);
};

const drawNextPiece = () => {
    nextContext.fillStyle = "#000";
    nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
    drawMatrix(nextPiece, { x: (nextCanvas.width / scale - nextPiece[0].length) / 2, y: (nextCanvas.height / scale - nextPiece.length) / 2 }, nextContext);
};

const drawMatrix = (matrix, offset, ctx = context) => {
    matrix.forEach((row, y) => {
        row.forEach((value, x) => {
            if (value) {
                ctx.fillStyle = "red";
                ctx.fillRect(x + offset.x, y + offset.y, 1, 1);
            }
        });
    });
};

Step 3: ブロックがリセットされるたびに、次のブロックを表示

最後に

Takaaki

続けてください

ChatGPT

最後に、playerReset() 関数を修正して、ブロックがリセットされるたびに次のブロックを表示し、新しい次のブロックを生成します。

const playerReset = () => {
    if (!nextPiece) {
        generateNextPiece();
    }
    player.matrix = nextPiece;
    generateNextPiece();
    drawNextPiece();

    player.pos.y = 0;
    player.pos.x = ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

    if (collide(arena, player)) {
        arena.forEach(row => row.fill(0));
        alert("ゲームオーバー");
    }
};

これで、ゲーム画面の横に次に落ちてくるブロックが表示されるようになりました。ゲームが始まるときに最初の次のブロックを生成するために、playerReset() を初期化前に一度呼び出してください。

playerReset();
update();

これで、次にどんなブロックが落ちてくるかが表示されるようになりました。この機能により、プレイヤーは次のブロックを考慮して戦略を立てることができます。

Takaaki

やってみたのですが、次のブロックの箱は表示されるのですが中になんのブロックも表示されず…

ChatGPT

すみません、コードに不足がありました。drawNextPiece() 関数で nextPiecenull の場合に対応する必要があります。以下のように drawNextPiece() 関数を修正してください。

const drawNextPiece = () => {
    nextContext.fillStyle = "#000";
    nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);

    if (nextPiece) {
        drawMatrix(nextPiece, { x: (nextCanvas.width / scale - nextPiece[0].length) / 2, y: (nextCanvas.height / scale - nextPiece.length) / 2 }, nextContext);
    }
};

さらに、最初の次のブロックを表示するために、update() 関数の前に drawNextPiece() を呼び出して初期化してください。

playerReset();
drawNextPiece();
update();

これで、次のブロックが表示されるはずです。もしまだ問題がある場合はお知らせください。

Takaaki

あれ、まだ表示されませんでした…

ChatGPT

申し訳ありません。理由がわかりました。playerReset()関数内でplayer.matrixnextPieceを代入した後で、generateNextPiece()を呼び出すことで、次のブロックを生成する前にnextPiecenullになってしまっています。

playerReset()関数を以下のように修正してください。

const playerReset = () => {
    if (!nextPiece) {
        generateNextPiece();
    }
    player.matrix = nextPiece;
    player.pos.y = 0;
    player.pos.x = ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

    if (collide(arena, player)) {
        arena.forEach(row => row.fill(0));
        alert("ゲームオーバー");
    }

    generateNextPiece();
    drawNextPiece();
};

これで、次のブロックが正しく表示されるはずです。もしまだ問題がある場合は、お知らせください。

Takaaki

1つまえの修正は戻した方がいいです?

ChatGPT

はい、前の修正に戻して、playerReset() 関数の修正に集中しましょう。以下の修正を行ってください。

  1. drawNextPiece() 関数の修正を元に戻す(nextPiecenull の場合の処理を削除する)。
  2. playerReset() 関数を修正し、次のブロックを生成し、次のブロックを表示する順序を適切に設定する。

修正後の関数は以下のようになります。

const drawNextPiece = () => {
    nextContext.fillStyle = "#000";
    nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
    drawMatrix(nextPiece, { x: (nextCanvas.width / scale - nextPiece[0].length) / 2, y: (nextCanvas.height / scale - nextPiece.length) / 2 }, nextContext);
};

const playerReset = () => {
    if (!nextPiece) {
        generateNextPiece();
    }
    player.matrix = nextPiece;
    player.pos.y = 0;
    player.pos.x = ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

    if (collide(arena, player)) {
        arena.forEach(row => row.fill(0));
        alert("ゲームオーバー");
    }

    generateNextPiece();
    drawNextPiece();
};

これで、次のブロックが正しく表示されるはずです。もしまだ問題がある場合はお知らせください。

Takaaki

んー表示されないです…ステップバイステップで修正方法を考えてもらえますか?

ChatGPT

申し訳ありません。もう一度見直して、ステップバイステップで修正方法を提案させていただきます。

Step 1: 次のブロック用の変数と関数を追加

まず、次のブロック用の変数と関数を追加します。nextPiece 変数を定義し、generateNextPiece() 関数を作成します。

let nextPiece = null;

const generateNextPiece = () => {
    const pieces = "TJLOSZI";
    nextPiece = createPiece(pieces[(pieces.length * Math.random()) | 0]);
};

Step 2: 次のブロックを表示する関数を追加

次に、次のブロックを表示するための関数を追加します。drawNextPiece() 関数を作成し、nextPiecenull でない場合に描画を行います。

const drawNextPiece = () => {
    nextContext.fillStyle = "#000";
    nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);

    if (nextPiece) {
        drawMatrix(nextPiece, { x: (nextCanvas.width / scale - nextPiece[0].length) / 2, y: (nextCanvas.height / scale - nextPiece.length) / 2 }, nextContext);
    }
};

Step 3: playerReset() 関数を修正

playerReset() 関数を修正して、現在の nextPiece をプレイヤーのブロックに設定し、新しい次のブロックを生成して表示します。また、最初の次のブロックを生成するために、playerReset() を初期化前に一度呼び出します。

const playerReset = () => {
    if (!nextPiece) {
        generateNextPiece();
    }
    player.matrix = nextPiece;
    player.pos.y = 0;
    player.pos.x = ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

    if (collide(arena, player)) {
        arena.forEach(row => row.fill(0));
        alert("ゲームオーバー");
    }

    generateNextPiece();
    drawNextPiece();
};

playerReset();
update();

Step 4: draw() 関数を修正

draw() 関数を修正して、drawNextPiece() 関数が正しく呼び出されるようにします。draw() 関数内で drawNextPiece() を呼び出すことで、次のブロックが常に更新されます。

const draw = () => {
    context.fillStyle = "#000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    drawMatrix(arena, { x: 0, y: 0 });
    drawMatrix(player.matrix, player.pos);
    drawNextPiece();
};

これらの修正を適用すると、次のブロックが正しく表示されるはずです。もしまだ問

Takaaki

あ、コードの変更ではなくチェックして欲しい箇所を教えてください

ChatGPT

失礼いたしました。既存のコードをチェックして問題がないかを確認させていただきます。

以下の箇所をチェックしてみてください:

  1. nextCanvas および nextContext の定義が正しいか確認してください。以下のように定義されていることを確認してください。
const nextCanvas = document.getElementById("next");
const nextContext = nextCanvas.getContext("2d");
nextCanvas.width = 4 * scale;
nextCanvas.height = 4 * scale;
nextContext.scale(scale, scale);
  1. generateNextPiece() 関数で、次のブロックが正しく生成されていることを確認してください。
const generateNextPiece = () => {
    const pieces = "TJLOSZI";
    nextPiece = createPiece(pieces[(pieces.length * Math.random()) | 0]);
};
  1. drawNextPiece() 関数が正しく実装されていることを確認してください。
const drawNextPiece = () => {
    nextContext.fillStyle = "#000";
    nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
    drawMatrix(nextPiece, { x: (nextCanvas.width / scale - nextPiece[0].length) / 2, y: (nextCanvas.height / scale - nextPiece.length) / 2 }, nextContext);
};
  1. playerReset() 関数内で、次のブロックをプレイヤーのブロックに設定し、新しい次のブロックを生成して表示していることを確認してください。
const playerReset = () => {
    if (!nextPiece) {
        generateNextPiece();
    }
    player.matrix = nextPiece;
    player.pos.y = 0;
    player.pos.x = ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

    if (collide(arena, player)) {
        arena.forEach(row => row.fill(0));
        alert("ゲームオーバー");
    }

    generateNextPiece();
    drawNextPiece();
};
  1. draw() 関数内で、drawNextPiece() 関数が呼び出されていることを確認してください。
const draw = () => {
    context.fillStyle = "#000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    drawMatrix(arena, { x: 0, y: 0 });
    drawMatrix(player.matrix, player.pos);
    drawNextPiece();
};

これらの箇所が正しく実装されていれば、次のブロックが表示されるはずです。もし問題が解決しない場合は、お手数ですがコード全体をお知らせいただけると、より具体的な解決策を提案できるかと思います。

Takaaki

あ、次のブロックが出てきたんですけど描画位置が違いますね…

すいませんどこがおかしいですか…?

      const canvas = document.getElementById("game");
      const context = canvas.getContext("2d");
      const scale = 20;
      context.scale(scale, scale);

      const nextCanvas = document.getElementById("next");
      const nextContext = nextCanvas.getContext("2d");
      nextCanvas.width = 4 * scale;
      nextCanvas.height = 4 * scale;
      nextContext.scale(scale, scale);

      const arenaSweep = () => {
        outer: for (let y = arena.length - 1; y > 0; y--) {
          for (let x = 0; x < arena[y].length; x++) {
            if (!arena[y][x]) {
              continue outer;
            }
          }
          const row = arena.splice(y, 1)[0].fill(0);
          arena.unshift(row);
          ++y;
        }
      };

      const collide = (arena, player) => {
        const [m, o] = [player.matrix, player.pos];
        for (let y = 0; y < m.length; ++y) {
          for (let x = 0; x < m[y].length; ++x) {
            if (m[y][x] && (arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) {
              return true;
            }
          }
        }
        return false;
      };

      const createMatrix = (w, h) => {
        const matrix = [];
        while (h--) {
          matrix.push(new Array(w).fill(0));
        }
        return matrix;
      };

      const draw = () => {
        context.fillStyle = "#000";
        context.fillRect(0, 0, canvas.width, canvas.height);

        drawMatrix(arena, { x: 0, y: 0 });
        drawMatrix(player.matrix, player.pos);
        drawNextPiece();
      };

      const drawMatrix = (matrix, offset) => {
        matrix.forEach((row, y) => {
          row.forEach((value, x) => {
            if (value) {
              context.fillStyle = "red";
              context.fillRect(x + offset.x, y + offset.y, 1, 1);
            }
          });
        });
      };

      const merge = (arena, player) => {
        player.matrix.forEach((row, y) => {
          row.forEach((value, x) => {
            if (value) {
              arena[y + player.pos.y][x + player.pos.x] = value;
            }
          });
        });
      };

      const playerDrop = () => {
        player.pos.y++;
        if (collide(arena, player)) {
          player.pos.y--;
          merge(arena, player);
          playerReset();
          arenaSweep();
        }
        dropCounter = 0;
      };

      const playerMove = (offset) => {
        player.pos.x += offset;
        if (collide(arena, player)) {
          player.pos.x -= offset;
        }
      };

      let nextPiece = null;

      const generateNextPiece = () => {
        const pieces = "TJLOSZI";
        nextPiece = createPiece(pieces[(pieces.length * Math.random()) | 0]);
      };

      const drawNextPiece = () => {
        nextContext.fillStyle = "#000";
        nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
        drawMatrix(
          nextPiece,
          {
            x: (nextCanvas.width / scale - nextPiece[0].length) / 2,
            y: (nextCanvas.height / scale - nextPiece.length) / 2,
          },
          nextContext
        );
      };

      const playerReset = () => {
        if (!nextPiece) {
          generateNextPiece();
        }
        player.matrix = nextPiece;
        player.pos.y = 0;
        player.pos.x =
          ((arena[0].length / 2) | 0) - ((player.matrix[0].length / 2) | 0);

        if (collide(arena, player)) {
          arena.forEach((row) => row.fill(0));
          alert("ゲームオーバー");
        }

        generateNextPiece();
        drawNextPiece();
      };

      const playerRotate = (dir) => {
        const pos = player.pos.x;
        let offset = 1;
        rotate(player.matrix, dir);

        while (collide(arena, player)) {
          player.pos.x += offset;
          offset = -(offset + (offset > 0 ? 1 : -1));

          if (offset > player.matrix[0].length) {
            rotate(player.matrix, -dir);
            player.pos.x = pos;
            return;
          }
        }
      };

      const rotate = (matrix, dir) => {
        for (let y = 0; y < matrix.length; ++y) {
          for (let x = 0; x < y; ++x) {
            [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
          }
        }

        if (dir > 0) {
          matrix.forEach((row) => row.reverse());
        } else {
          matrix.reverse();
        }
      };

      const createPiece = (type) => {
        if (type === "T") {
          return [
            [1, 1, 1],
            [0, 1, 0],
            [0, 0, 0],
          ];
        } else if (type === "J") {
          return [
            [0, 1, 0],
            [0, 1, 0],
            [1, 1, 0],
          ];
        } else if (type === "L") {
          return [
            [0, 1, 0],
            [0, 1, 0],
            [0, 1, 1],
          ];
        } else if (type === "O") {
          return [
            [1, 1],
            [1, 1],
          ];
        } else if (type === "S") {
          return [
            [0, 1, 1],
            [1, 1, 0],
            [0, 0, 0],
          ];
        } else if (type === "Z") {
          return [
            [1, 1, 0],
            [0, 1, 1],
            [0, 0, 0],
          ];
        } else if (type === "I") {
          return [
            [0, 1, 0, 0],
            [0, 1, 0, 0],
            [0, 1, 0, 0],
            [0, 1, 0, 0],
          ];
        }
      };

      let dropCounter = 0;
      let dropInterval = 1000;
      let lastTime = 0;

      const update = (time = 0) => {
        const deltaTime = time - lastTime;
        lastTime = time;

        dropCounter += deltaTime;
        if (dropCounter > dropInterval) {
          playerDrop();
        }

        draw();
        requestAnimationFrame(update);
      };

      const arena = createMatrix(16, 20);
      const player = {
        pos: { x: 0, y: 0 },
        matrix: null,
      };

      document.addEventListener("keydown", (event) => {
        if (event.keyCode === 37) {
          playerMove(-1);
        } else if (event.keyCode === 39) {
          playerMove(1);
        } else if (event.keyCode === 40) {
          playerDrop();
        } else if (event.keyCode === 81) {
          playerRotate(-1);
        } else if (event.keyCode === 87 || event.keyCode === 38) {
          playerRotate(1);
        }
      });

      playerReset();
      update();

ChatGPT

コードを確認させていただきました。drawMatrix関数の呼び出し時に、nextContextが渡されていないために、次のブロックが正しく描画されていないようです。drawNextPiece()関数内のdrawMatrix()関数の呼び出しで、nextContextを渡すように変更してみてください。

以下のように修正してみてください。

const drawNextPiece = () => {
    nextContext.fillStyle = "#000";
    nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
    drawMatrix(
        nextPiece,
        {
            x: (nextCanvas.width / scale - nextPiece[0].length) / 2,
            y: (nextCanvas.height / scale - nextPiece.length) / 2,
        },
        nextContext
    );
};

そして、drawMatrix 関数のシグネチャを以下のように変更してください。

const drawMatrix = (matrix, offset, context) => {
    // 以前のコードはここに残ります。
};

これで次のブロックが正しく表示されるはずです。お試しください。

Takaaki

おー次のブロックのようなものが表示されました! が、落ちてくるブロックが表示されなくなっちゃいました…

ChatGPT

失礼しました。draw()関数内のdrawMatrix()関数の呼び出しにもcontextを渡す必要があります。以下のように修正してみてください。

const draw = () => {
    context.fillStyle = "#000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    drawMatrix(arena, { x: 0, y: 0 }, context);
    drawMatrix(player.matrix, player.pos, context);
    drawNextPiece();
};

これで、ゲーム画面に落ちてくるブロックが再び表示されるはずです。お試しください。

Takaaki

いけました! いやーなかなか大変でしたね笑

ChatGPT

うまくいってよかったです!お手伝いできてうれしいです。確かに、コードの修正やデバッグは時々大変ですね。しかし、うまく機能するときはとても達成感があります!

今後も何か質問があれば、どんなことでもお気軽にお聞きください。お手伝いできることがあれば喜んで対応いたします。お楽しみください!

Takaaki

画面の一番下までブロックが落ちないのですがどこか設定を変更する必要がありますか?

ChatGPT

すみません、問題が発生していることに気づかなかったです。アリーナの高さが20に設定されているので、キャンバスの高さもそれに合わせて調整する必要があります。canvasの高さを変更することで、ブロックが画面の一番下まで落ちるようになります。

以下のように、canvasの高さを20倍のスケールに設定してみてください。

const canvas = document.getElementById("game");
const context = canvas.getContext("2d");
const scale = 20;
canvas.width = 16 * scale;
canvas.height = 20 * scale; // ここを変更します
context.scale(scale, scale);

これで、ブロックが画面の一番下まで落ちるはずです。お試しください。

Takaaki

あ、画面全体の大きさはさっきのままがいいです!

ChatGPT

失礼しました。画面全体の大きさを維持しつつ、ブロックが画面の一番下まで落ちるようにするには、アリーナの高さを調整する必要があります。

createMatrix関数でアリーナを作成する際の高さを、現在のキャンバスの高さに合わせましょう。以下のようにcreateMatrix関数を呼び出す際に、高さをcanvas.height / scaleに設定してみてください。

const arena = createMatrix(16, canvas.height / scale); // ここを変更します

これで、ブロックが画面の一番下まで落ちるはずです。お試しください。

Takaaki

できました! 最後に、ブロックを動かしている時にどこ列にいるのかわかりずらいのでいい感じにデザイン変えてもらえますか?

ChatGPT

もちろんです!ブロックがどの列にあるかをわかりやすくするために、ゲーム画面にグリッド線を追加してみましょう。以下のdrawGrid関数を追加し、draw関数内で呼び出してください。

const drawGrid = () => {
    context.strokeStyle = "rgba(255, 255, 255, 0.1)"; // グリッド線の色と透明度を設定
    context.lineWidth = 0.05; // グリッド線の太さを設定

    // 縦線を描画
    for (let x = 1; x < 16; x++) {
        context.beginPath();
        context.moveTo(x, 0);
        context.lineTo(x, canvas.height / scale);
        context.stroke();
    }

    // 横線を描画
    for (let y = 1; y < canvas.height / scale; y++) {
        context.beginPath();
        context.moveTo(0, y);
        context.lineTo(16, y);
        context.stroke();
    }
};

const draw = () => {
    context.fillStyle = "#000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    drawGrid(); // グリッド線を描画
    drawMatrix(arena, { x: 0, y: 0 });
    drawMatrix(player.matrix, player.pos);
    drawNextPiece();
};

この変更により、ゲーム画面にグリッド線が表示され、ブロックがどの列にあるかがわかりやすくなるはずです。お試しください。

Takaaki

グリッド戦は表示されたのですが、落ちてくるブロックがまた消えちゃいました…

ChatGPT

すみません、グリッド線の描画がブロックの描画を上書きしてしまっているようです。drawGrid関数をdraw関数の最初に移動して、背景とグリッド線を先に描画してからブロックを描画するようにしましょう。

const draw = () => {
    context.fillStyle = "#000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    drawGrid(); // グリッド線を描画
    drawMatrix(arena, { x: 0, y: 0 });
    drawMatrix(player.matrix, player.pos);
    drawNextPiece();
};

これで落ちてくるブロックが表示されるはずです。お試しください。

Takaaki

あれ、表示されないです…

      const drawGrid = () => {
        context.strokeStyle = "rgba(255, 255, 255, 0.1)"; // グリッド線の色と透明度を設定
        context.lineWidth = 0.05; // グリッド線の太さを設定

        // 縦線を描画
        for (let x = 1; x < 16; x++) {
          context.beginPath();
          context.moveTo(x, 0);
          context.lineTo(x, canvas.height / scale);
          context.stroke();
        }

        // 横線を描画
        for (let y = 1; y < canvas.height / scale; y++) {
          context.beginPath();
          context.moveTo(0, y);
          context.lineTo(16, y);
          context.stroke();
        }
      };

      const draw = () => {
        context.fillStyle = "#000";
        context.fillRect(0, 0, canvas.width, canvas.height);

        drawGrid(); // グリッド線を描画
        drawMatrix(arena, { x: 0, y: 0 });
        drawMatrix(player.matrix, player.pos);
        drawNextPiece();
      };

であってますか?

ChatGPT

申し訳ありません、私の説明が不十分でした。drawMatrix関数を呼び出す前に、context.save()を追加して、描画状態を保存し、drawMatrix関数が終わった後にcontext.restore()を呼び出して、描画状態を元に戻しましょう。

以下のように変更してください。

const draw = () => {
    context.fillStyle = "#000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    drawGrid(); // グリッド線を描画

    context.save(); // 描画状態を保存
    drawMatrix(arena, { x: 0, y: 0 });
    drawMatrix(player.matrix, player.pos);
    context.restore(); // 描画状態を元に戻す

    drawNextPiece();
};

これでブロックが表示されるはずです。お試しください。

Takaaki

そんなメソッドは実装していないので動かなさそうですね…

ChatGPT

申し訳ありません。私がコードを見直したところ、誤解があったようです。drawGrid内でcontext.lineWidthを設定していたことが原因でした。ブロックの描画に影響が出ていました。context.lineWidthdrawGridの最後で元の値に戻すことで、問題が解決するはずです。

drawGrid関数を以下のように修正してください。

const drawGrid = () => {
    context.strokeStyle = "rgba(255, 255, 255, 0.1)"; // グリッド線の色と透明度を設定
    const originalLineWidth = context.lineWidth;
    context.lineWidth = 0.05; // グリッド線の太さを設定

    // 縦線を描画
    for (let x = 1; x < 16; x++) {
        context.beginPath();
        context.moveTo(x, 0);
        context.lineTo(x, canvas.height / scale);
        context.stroke();
    }

    // 横線を描画
    for (let y = 1; y < canvas.height / scale; y++) {
        context.beginPath();
        context.moveTo(0, y);
        context.lineTo(16, y);
        context.stroke();
    }

    context.lineWidth = originalLineWidth; // 元の lineWidth に戻す
};

これでブロックが表示されるはずです。再度お試しください。

Takaaki

だめでした…

      const drawGrid = () => {
        context.strokeStyle = "rgba(255, 255, 255, 0.1)"; // グリッド線の色と透明度を設定
        const originalLineWidth = context.lineWidth;
        context.lineWidth = 0.05; // グリッド線の太さを設定

        // 縦線を描画
        for (let x = 1; x < 16; x++) {
          context.beginPath();
          context.moveTo(x, 0);
          context.lineTo(x, canvas.height / scale);
          context.stroke();
        }

        // 横線を描画
        for (let y = 1; y < canvas.height / scale; y++) {
          context.beginPath();
          context.moveTo(0, y);
          context.lineTo(16, y);
          context.stroke();
        }

        context.lineWidth = originalLineWidth; // 元の lineWidth に戻す
      };

      const draw = () => {
        context.fillStyle = "#000";
        context.fillRect(0, 0, canvas.width, canvas.height);

        drawGrid(); // グリッド線を描画
        drawMatrix(arena, { x: 0, y: 0 });
        drawMatrix(player.matrix, player.pos);
        drawNextPiece();
      };

ChatGPT

申し訳ありません。問題を見つけました。context.lineWidthは問題ではなかったようです。drawMatrix関数を呼び出す際に、nextContextを引数として渡す必要があります。drawNextPiece関数を以下のように修正してください。

const drawNextPiece = () => {
  nextContext.fillStyle = "#000";
  nextContext.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
  drawMatrix(
    nextPiece,
    {
      x: (nextCanvas.width / scale - nextPiece[0].length) / 2,
      y: (nextCanvas.height / scale - nextPiece.length) / 2,
    },
    nextContext
  );
};

これで、ブロックが表示されるはずです。再度お試しください。

Takaaki

だめですね…

チェックポイントを教えてください!

ChatGPT

大変失礼しました。drawMatrix関数の定義を修正する必要があります。第3引数として ctx を追加してください。そして、ctx.fillStylectx.fillRect を使用して、drawMatrix内での描画を適切なコンテキストで行います。

以下のように修正してください。

const drawMatrix = (matrix, offset, ctx = context) => {
  matrix.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        ctx.fillStyle = "red";
        ctx.fillRect(x + offset.x, y + offset.y, 1, 1);
      }
    });
  });
};

これで、落ちてくるブロックが表示されるはずです。もう一度お試しください。

Takaaki

お、表示されました! 何が原因だったですか?

ChatGPT

素晴らしいです!表示されて良かったです。

原因は、drawMatrix関数にデフォルトのcontextが設定されていなかったことです。このため、drawNextPiece()関数でdrawMatrix()を呼び出す際に、nextContextを渡していましたが、draw()関数内でdrawMatrix()を呼び出す際に、コンテキストが指定されていませんでした。

修正により、drawMatrix関数にctxという第3引数が追加され、デフォルト値としてcontextが設定されました。これによって、draw()関数内でdrawMatrix()を呼び出す際にコンテキストが適切に設定されるようになり、落ちてくるブロックが正常に表示されるようになりました。

Takaaki

なるほど! デフォルトの引数でグローバル変数を渡すのは怖いので、draw関数内でdrawMatrixを呼び出す時に明示的にcontextを渡してあげた方が安全かなと思ったのですが、難しそうですか?

ChatGPT

関数内で引数としてコンテキストを渡すことは可能ですが、この場合、呼び出し元で引数としてコンテキストを渡さなければなりません。これは、関数を使う側にも知識を要求するため、あまり良い方法ではありません。また、コンテキストを渡すことが忘れられた場合や、誤ったコンテキストが渡された場合、エラーが発生する可能性があります。

代わりに、draw関数内で、グローバルなコンテキスト変数を参照する方法もあります。これにより、呼び出し元がコンテキストを指定する必要がなくなります。

以下は、この方法を使用したdrawMatrix関数の例です。

const drawMatrix = (matrix, offset) => {
  matrix.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value) {
        context.fillStyle = "red";
        context.fillRect(x + offset.x, y + offset.y, 1, 1);
      }
    });
  });
};

このように、グローバル変数であるcontextを使用することで、コンテキストを関数呼び出し元で渡す必要がなくなります。

まとめ

こやつにコンテンツ生成してもらう取り組みいつまで続くのか…Plus契約したから頑張りたい…