見出し画像

GPT-4にGPT-3.5で解決できなかったゲームのバグを解決できるか調査してみた話

以前GPT-3.5を利用してこちらの簡単なブロック崩しゲームをサクッと作らせ、こちらの記事で公開しました。その記事で、GPT-3.5に反射角を変化させる機能を実装させようとしたところ、バグが発生して実現できなかったと触れました。そのバグをGPT-4で解決できるようになったか調査しました。

本記事の作成目的

  • GPT-4のゲームプログラミング能力の限界に関する1つの知識の共有
    具体的には、単純なブロック崩しゲームにおいて、反射角を変化させる機能を実装しようとした時に発生したGPT-3.5で解決できなかった単純なバグをGPT-4で解決できるか調査した結果に関する知識を共有することです。

対象読者

  • ゲーム開発者

  • ChatGPTに興味がある人

使用言語モデル

本記事の全ての回答はGPT-4により生成させたものとなります。

バグについて

本バグ発生時のプレイ動画

画面右端でボールがバーの右端に衝突した時、
ボールが反射されずに落下します。
最終的には全く反射しなくなりました。

以前公開したこちらの単純なブロック崩しゲームに対して、GPT-3.5にボールとバーが当たる位置によって反射角を変化させる機能を実装させようとしました。

しかし、画面右端でボールがバーの右端に衝突した時、ボールが反射されずに落下するというバグが発生したため、その改善をGPTに依頼しました。それに対して、反射角度の計算式を少し変化させたコードを何度もGPTは提示してきて一向に実装できませんでした。最終的には全く反射しなくなりました。

1. 全く反射しないバグ修正

質問

Q. 以下はブロック崩しゲームのプログラムです。
ボールがバーにぶつかっても反射しない原因を教えてください。 

<div id="score"></div>
<canvas id="game" width="500" height="500"></canvas>
<script>
    // canvas要素を取得
    var canvas = document.getElementById("game");
    var ctx = canvas.getContext("2d");
    // ボールとバーのオブジェクトを定義
    var ball = {
        x: 250,
        // x: 0,
        y: 450,
        radius: 10,
        xSpeed: 5,
        ySpeed: -5
    };
    var bar = {
        x: 250,
        y: 480,
        width: 100,
        height: 10
    };
    // ブロックの配列を定義
    var blocks = [];
    for (var i = 0; i < 5; i++) {
        for (var j = 0; j < 5; j++) {
            blocks.push({
                x: i * 100,
                y: j * 20,
                width: 90,
                height: 10
            });
        }
    }
    var intervalId;
    var score = 0;
    var scoreLabel = document.getElementById("score");
    function updateScore() {
        score++;
    }
    function drawScore() {
        scoreLabel.innerHTML = "Score: " + score;
    }
    function startGame() {
        score = 0;
        // ゲームの初期設定
        ball = {
            // x: 250,
            x: 350,
            y: 450,
            radius: 10,
            xSpeed: 5,
            ySpeed: -5
        };
        bar = {
            x: 250,
            y: 480,
            width: 100,
            height: 10
        };
        blocks = [];
        for (var i = 0; i < 5; i++) {
            for (var j = 0; j < 5; j++) {
                blocks.push({
                    x: i * 100,
                    y: j * 20,
                    width: 90,
                    height: 10
                });
            }
        }
        intervalId = setInterval(update, 10);
    }
    startGame();
    // ゲームループ
    function update() {
        // ボールを移動
        ball.x += ball.xSpeed;
        ball.y += ball.ySpeed;
        // ボールが画面外に出たら反転
        if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
            ball.xSpeed = -ball.xSpeed;
        }
        if (ball.y - ball.radius < 0) {
            ball.ySpeed = -ball.ySpeed;
        }
        // // ボールがバーに当たったら反転
        // if (ball.x > bar.x && ball.x < bar.x + bar.width && ball.y + ball.radius > bar.y) {
        //     var ballSpeed = 5;
        //     // バーの左端からの割合
        //     var hitPosition = (ball.x - (bar.x - ball.radius)) / bar.width;
        //     // 反射角(ラジアン)
        //     // var angle = -(Math.PI / 3) * hitPosition + Math.PI / 6;
        //     // 反射角(ラジアン)
        //     // var angle = -(Math.PI / 3) * (hitPosition - 0.5) + Math.PI / 6;
        //     // 反射角(ラジアン)
        //     var angle = -(Math.PI / 4) * hitPosition + Math.PI / 4;
        

        //     ball.xSpeed = ballSpeed * Math.cos(angle);
        //     ball.ySpeed = -ballSpeed * Math.sin(angle);
        // }
        // バーの右端に当たった場合
        if (ball.x > bar.x + bar.width - ball.radius && ball.y + ball.radius > bar.y) {
            var ballSpeed = 5;
            var angle = -Math.PI / 4;
            ball.xSpeed = ballSpeed * Math.cos(angle);
            ball.ySpeed = -ballSpeed * Math.sin(angle);
        }










        // ボールがブロックに当たったらブロックを削除
        for (var i = 0; i < blocks.length; i++) {
            if (ball.x + ball.radius > blocks[i].x && ball.x - ball.radius < blocks[i].x + blocks[i].width && ball.y - ball.radius < blocks[i].y + blocks[i].height && ball.y + ball.radius > blocks[i].y) {
                blocks.splice(i, 1);
                updateScore();
                ball.ySpeed = -ball.ySpeed;
                break;
            }
        }
        // 描画
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillRect(bar.x, bar.y, bar.width, bar.height);
        for (var i = 0; i < blocks.length; i++) {
            ctx.fillRect(blocks[i].x, blocks[i].y, blocks[i].width, blocks[i].height);
        }
        ctx.strokeStyle = "black";
        ctx.strokeRect(0,0,canvas.width,canvas.height);
        // バーを移動
        document.onmousemove = function(e) {
            bar.x = e.clientX - canvas.offsetLeft - bar.width / 2;
        }
        // document.onmousemove = function(e) {
        //     var mouseX = e.clientX - canvas.offsetLeft;
        //     if (mouseX > 0 && mouseX < canvas.width) {
        //         bar.x = mouseX - bar.width / 2;
        //     }
        // }
        // document.onmousemove = function(e) {
        //     var mouseX = e.clientX - canvas.offsetLeft;
        //     if (mouseX > bar.width / 2 && mouseX < canvas.width - bar.width / 2) {
        //         bar.x = mouseX - bar.width / 2;
        //     }
        // }
        document.onmousemove = function(e) {
            var mouseX = e.clientX - canvas.offsetLeft;
            if (mouseX < bar.width / 2) {
                bar.x = 0;
            } else if (mouseX > canvas.width - bar.width / 2) {
                bar.x = canvas.width - bar.width;
            } else {
                bar.x = mouseX - bar.width / 2;
            }
        }
        // ゲームオーバー時の処理
        if (ball.y + ball.radius > canvas.height) {
            alert("Game Over!");
            clearInterval(intervalId);
            var restart = confirm("Would you like to restart the game?");
            if (restart) {
                startGame();
            }
        }
        // ゲームクリア判定
        if (blocks.length == 0) {
            alert("Congratulations! You cleared the game!");
            clearInterval(intervalId);
            var restart = confirm("Would you like to restart the game?");
            if (restart) {
                startGame();
            }
        }
        drawScore();
    }
</script>

回答

A. ボールがバーにぶつかっても反射しない問題は、バーとの衝突判定のコードがコメントアウトされているためです。以下のようにコメントを外して、衝突判定のコードを有効化しましょう。

// ボールがバーに当たったら反転
if (ball.x > bar.x && ball.x < bar.x + bar.width && ball.y + ball.radius > bar.y) {
    var ballSpeed = 5;
    // バーの左端からの割合
    var hitPosition = (ball.x - (bar.x - ball.radius)) / bar.width;
    // 反射角(ラジアン)
    // var angle = -(Math.PI / 3) * hitPosition + Math.PI / 6;
    // 反射角(ラジアン)
    // var angle = -(Math.PI / 3) * (hitPosition - 0.5) + Math.PI / 6;
    // 反射角(ラジアン)
    var angle = -(Math.PI / 4) * hitPosition + Math.PI / 4;
    

    ball.xSpeed = ballSpeed * Math.cos(angle);
    ball.ySpeed = -ballSpeed * Math.sin(angle);
}

また、バーの右端に当たった場合の判定のコードは不要ですので、削除してください。

// バーの右端に当たった場合
/* if (ball.x > bar.x + bar.width - ball.radius && ball.y + ball.radius > bar.y) {
    var ballSpeed = 5;
    var angle = -Math.PI / 4;
    ball.xSpeed = ballSpeed * Math.cos(angle);
    ball.ySpeed = -ballSpeed * Math.sin(angle);
} */

結果

画面右端で反射されないバグは残ったままです。

反射しない原因は反射処理がコメントアウトされているという単なる凡ミスでした。修正後は取り敢えず反射するようにはなりました。

しかし、画面右端でボールがバーの右端に衝突した時、ボールが反射されずに落下するというバグは相変わらず残っています。

2. 画面右端でボールが反射しないバグ修正

質問

Q. 画面右端でボールがバーの右端に衝突した時、ボールが反射されずに落下します。このバグの修正をお願いします。

回答

A. 下記がバグを修正したscript.jsのコードです。

var canvas = document.getElementById("game");
var ctx = canvas.getContext("2d");
var ball = {
    x: 250,
    y: 450,
    radius: 10,
    xSpeed: 5,
    ySpeed: -5
};
var bar = {
    x: 250,
    y: 480,
    width: 100,
    height: 10
};
var blocks = [];
for (var i = 0; i < 5; i++) {
    for (var j = 0; j < 5; j++) {
        blocks.push({
            x: i * 100,
            y: j * 20,
            width: 90,
            height: 10
        });
    }
}
var intervalId;
var score = 0;
var scoreLabel = document.getElementById("score");

// ... (省略)

function startGame() {
    // ... (省略)
}

startGame();

function update() {
    ball.x += ball.xSpeed;
    ball.y += ball.ySpeed;
    if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
        ball.xSpeed = -ball.xSpeed;
    }
    if (ball.y - ball.radius < 0) {
        ball.ySpeed = -ball.ySpeed;
    }
    if (ball.x > bar.x && ball.x < bar.x + bar.width && ball.y + ball.radius >= bar.y) {
        var ballSpeed = 5;
        var hitPosition = (ball.x - (bar.x - ball.radius)) / bar.width;
        var angle = -(Math.PI / 4) * hitPosition + Math.PI / 4;
        ball.xSpeed = ballSpeed * Math.cos(angle);
        ball.ySpeed = -ballSpeed * Math.sin(angle);
    }
    // ... (省略)
}

バグが発生していた原因は、バーにボールが衝突したときの条件判定が厳密でなかったためです。`ball.y + ball.radius > bar.y`から`ball.y + ball.radius >= bar.y`に変更し、条件を厳密にしました。この変更によりバグが修正され、ボールが正しく反射されるようになります。

結果

改善されませんでした。

3. 再度バグ修正を依頼

質問

Q. 相変わらず改善されません。

回答

A. すみません、こちらで修正したコードを確認します。

function update() {
    ball.x += ball.xSpeed;
    ball.y += ball.ySpeed;
    if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
        ball.xSpeed = -ball.xSpeed;
    }
    if (ball.y - ball.radius < 0) {
        ball.ySpeed = -ball.ySpeed;
    }
    if (ball.x >= bar.x - ball.radius && ball.x <= bar.x + bar.width + ball.radius && ball.y + ball.radius >= bar.y) {
        var ballSpeed = 5;
        var hitPosition = (ball.x - bar.x) / bar.width;
        var angle = -(Math.PI / 4) * hitPosition + Math.PI / 4;
        ball.xSpeed = ballSpeed * Math.cos(angle);
        ball.ySpeed = -ballSpeed * Math.sin(angle);
    }

    // ... (省略)
}

上記のコードで、`update`関数内の衝突判定を修正しています。具体的には、`if (ball.x > bar.x...`から`if (ball.x >= bar.x - ball.radius...`へと変更しました。これにより、画面の右端および左端でバーとボールが衝突した際に正しく反射されるようになります。

結果

相変わらず改善されませんでした。

この後も同様の質問をしましたが、同様にバーにボールが衝突したときの条件判定式を微妙に変化させた式を提示してくるのみで、一向にバグは改善しませんでした。

4. 別の実装方法で1から作り直す

質問

Q. 改善できませんでした。同様の仕様のブロック崩しゲームを別のコードで再作成してもらえますか?

回答

A. 承知しました。以下が新たに作成したブロック崩しゲームのコードです。

<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            margin: 0px;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: black;
        }

        canvas {
            border: 1px solid white;
        }

        #score {
            color: white;
            position: fixed;
            top: 20px;
            left: 20px;
            font-family: Arial, sans-serif;
        }
    </style>
</head>
<body>
    <div id="score"></div>
    <canvas id="game" width="500" height="500"></canvas>
    <script src="script.js"></script>
</body>
</html>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const scoreLabel = document.getElementById("score");

const ball = {
    x: 250,
    y: 450,
    radius: 10,
    xSpeed: 4,
    ySpeed: -4
};

const bar = {
    x: 250,
    y: 480,
    width: 100,
    height: 10
};

const blocks = [];
for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 5; j++) {
        blocks.push({
            x: i * 100,
            y: j * 20,
            width: 90,
            height: 10
        });
    }
}

let intervalId;
let score = 0;

function updateScore() {
    score++;
}

function drawScore() {
    scoreLabel.innerHTML = "Score: " + score;
}

function startGame() {
    score = 0;
    resetBall();
    drawBall();
    drawBar();
    drawBlocks();
    intervalId = setInterval(update, 10);
}

function resetBall() {
    ball.x = 250;
    ball.y = 450;
    ball.xSpeed = 4;
    ball.ySpeed = -4;
}

function drawBall() {
    ctx.beginPath();
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fillStyle = "green";
    ctx.fill();
    ctx.closePath();
}

function drawBar() {
    ctx.fillStyle = "green";
    ctx.fillRect(bar.x, bar.y, bar.width, bar.height);
}

function drawBlocks() {
    ctx.fillStyle = "green";
    for (let block of blocks) {
        ctx.fillRect(block.x, block.y, block.width, block.height);
    }
}

function update() {
    clearCanvas();
    drawBall();
    drawBar();
    drawBlocks();
    drawScore();
    detectCollision();
    
    ball.x += ball.xSpeed;
    ball.y += ball.ySpeed;

    if (ball.x < ball.radius || ball.x + ball.radius > canvas.width) {
        ball.xSpeed = -ball.xSpeed;
    }
    if (ball.y < ball.radius) {
        ball.ySpeed = -ball.ySpeed;
    }
    if (ball.y + ball.radius > canvas.height) {
        clearInterval(intervalId);
        gameOver();
        return;
    }
}

function gameOver() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "40px Arial";
    ctx.fillStyle = "white";
    ctx.textAlign = "center";
    ctx.fillText("Game Over!", canvas.width / 2, canvas.height / 2 - 50);
    ctx.fillText("Score: " + score, canvas.width / 2, canvas.height / 2);
    scoreLabel.style.display = "none";
    document.onkeydown = e => {
        if (e.code === "Space") {
            startGame();
        }
    };
}

function detectCollision() {
    if (ball.y + ball.radius >= bar.y && ball.y <= bar.y &&
        ball.x + ball.radius >= bar.x && ball.x - ball.radius <= bar.x + bar.width
    ) {
        ball.ySpeed = -ball.ySpeed;
    }

    for (let i = 0; i < blocks.length; i++) {
        if (ball.x + ball.radius >= blocks[i].x &&
            ball.x - ball.radius <= blocks[i].x + blocks[i].width &&
            ball.y - ball.radius <= blocks[i].y + blocks[i].height &&
            ball.y + ball.radius >= blocks[i].y
        ) {
            blocks.splice(i, 1);
            updateScore();
            ball.ySpeed = -ball.ySpeed;
            break;
        }
    }
}

function clearCanvas() {
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}
  
canvas.addEventListener("mousemove", e => {
    bar.x = e.clientX - canvas.offsetLeft - (bar.width / 2);
});
  
startGame();

この新しいコードでは、衝突判定を綺麗に再作成し、さまざまなバグも修正しています。確認して問題が解決するかどうかをお知らせください。

結果

一から再作成させたゲームの画面右端での反射時の画面(1)。真上にしか反射しません。
同一ゲームの画面右端での反射時の画面(2)
同一ゲームの画面右端での反射時の画面(3)
同一ゲームの画面右端での反射時の画面(4)

上記4の回答のコードに対して、多少の修正依頼(ボールとバーが当たる位置によって反射角を変化させる機能の実装、クリア機能の実装、色変更など)を出し、最終的には上図のようなゲームが生成されました。このゲームはこちらからプレイ可能です(ソースコードも閲覧可能です)。

画面右端で反射しなくなるバグは消えたようですが、残念ながら、反射後に画面右端にボールが埋まってしまい、その後は真上にしかボールが反射しなくなるという新たなバグが代わりに発生しました。

これらの結果を持って、GPT-4のゲームプログラミング能力は万能ではなく、本バグ(画面右端でボールがバーの右端に衝突した時、正しくボールが反射されない)のような単純なバグを解決できない場合があると判断します。

まとめ

GPT-4でも、本記事の「バグについて」で触れた単純なバグを解決することはできませんでした。まだまだ、GPT-4のプログラミング能力には限界があり、GPT-4ができないことを人間が補っていく必要があるようです。

関連記事

ChatGPTを利用してブロック崩しゲームを作った話

GPT-4に6つのゲームをサクッと作らせた話


この記事が気に入ったらサポートをしてみませんか?