🎮

ChatGPTにゲームアイデアを考えてもらい、ついでにノーコードで開発してみた

2023/04/21に公開1

Key Visual

概要

初めまして。MESON.incというXRを専門としている企業のTech Blogですが、
本記事は、MESONディレクター(=非プログラマ/非エンジニア)によるChatGPTを使った開発体験の記事になります!
(筆者略歴:MESON入社して約半年。プロマネ初心者。大学時代に少しコードを書いたことがある程度)

そんな自分が、ChatGPTにゲームを考えてもらって、さらにそこからChatGPTと一緒にノーコードで開発する過程を書いています🙌

出来上がったゲームはこちらから遊べます。
https://gami0901.github.io/chatgpt_game_nocord/final/index.html

動画はこちら!
https://twitter.com/renew_s1/status/1646749256378720258

改めてこの記事では、非プログラマ/非エンジニア視点でのChatGPTを使った開発の様子をお届けしています。
途中失敗だらけですので、AIにお任せすればなんでも出来る!というトーンではないです。

むしろ何ができて何ができないのか、自然言語での開発体験のご参考になれば幸いです!

ゲームアイデア作り

まずはChatGPTにゲームを考えてもらうところからです。

最初のインプット

ChatGPTにはたった一言「一般的なオフィスにある物(例えば文房具など)を使って二人で遊べるゲームを考えてください。」とだけ、指示しています。
結果出来上がったのが、上記の「ペンタワーバトル」です。

Pen Tower Battle

オフィスにある物とした理由は、巷で見かけるChatGPTへの依頼がデジタル世界にて完結するものが多かったため、物理世界を前提としたアイデアが欲しく、こういった指示にしました。

初期ルール

さて、実際にこのルールで試してみると、色々と疑問点が湧いてきます。
「いつ線を引くの?」であったり、「線は何本引いて良いの?」であったり、です。

Q and A

質問をしてみると、ルールが色々と返ってきます。
個人的には「点を繋げるのは、自分の既存の点と新しく打った点の間だけ」と「プレイヤーは自分のターンで必ず線を引く必要はありません」あたりのルールが面白いと感じました。
実際にプレイした感想としては、前者はプレイに制約を与え、後者はプレイに駆け引きを与えてくれました。

このやり取りの時点で、ルールは完成していたと思われます。ルールはこちらです。

  • プレイヤーAとプレイヤーBが交互にゲームボードに点を打つ
  • プレイヤーは自分の点を繋げて四角形を作ることを目指す
  • 線の引き方は「今打った点と元々置かれていた点を結ぶ」「線を引かない」の2通り
  • 線を引く際のルールとして、すでに引かれている線をまたぐような線を引くことはできない
  • また四角形が完成する場合のみ、2本目の線を引くことができる
  • その際、「今打った点と、元々置かれていた点を結ぶ」ルールは適用される
  • 点を打つ際に、元々点が置かれている場所には点を打てない
  • 四角形が完成した場合、対応するプレイヤーのスコアが1増える
  • ゲームは無制限に続けられるが、ゲームボード上にこれ以上点を打てなくなった場合や、プレイヤーが終了したいと判断した場合に終了する
  • 最終的に四角形を完成させた回数が多いプレイヤーが勝者

ルールが分かりにくい?

文字の説明だとルールが分かりにくいですか?僕もそう思います。
ボードゲームあるあるですが、「とりあえず1回やってみよう?」となりますよね?

今でこそ自分はルールを理解しましたが、ChatGPTのルール説明は正直わかりづらく、なのでルール詳細を詰める前に実装してもらっていました。

Prototype

出来上がったバージョンはこちらです。
https://gami0901.github.io/chatgpt_game_nocord/1st_proto/index.html

初期コード
const canvas = document.getElementById("gameBoard");
const ctx = canvas.getContext("2d");

const gridSize = 40;
const pointRadius = 4;
const players = ["A", "B"];
let currentPlayer = 0;
let points = { A: [], B: [] };
let score = { A: 0, B: 0 };

canvas.addEventListener("click", (event) => {
    const rect = canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;

    const gridX = Math.round(x / gridSize) * gridSize;
    const gridY = Math.round(y / gridSize) * gridSize;

    const player = players[currentPlayer];
    points[player].push({ x: gridX, y: gridY });

    drawPoint(gridX, gridY, player);
    checkForSquare(player);

    currentPlayer = (currentPlayer + 1) % 2;
});

function drawPoint(x, y, player) {
    ctx.beginPath();
    ctx.arc(x, y, pointRadius, 0, 2 * Math.PI);
    ctx.fillStyle = player === "A" ? "red" : "blue";
    ctx.fill();
    ctx.closePath();
}

function checkForSquare(player) {
    const playerPoints = points[player];

    for (let i = 0; i < playerPoints.length; i++) {
        for (let j = 0; j < playerPoints.length; j++) {
            if (playerPoints[i].x === playerPoints[j].x || playerPoints[i].y === playerPoints[j].y) continue;

            const dx = Math.abs(playerPoints[i].x - playerPoints[j].x);
            const dy = Math.abs(playerPoints[i].y - playerPoints[j].y);
            if (dx !== dy) continue;

            const corner1 = { x: playerPoints[i].x, y: playerPoints[j].y };
            const corner2 = { x: playerPoints[j].x, y: playerPoints[i].y };

            if (pointExists(playerPoints, corner1) && pointExists(playerPoints, corner2)) {
                drawSquare(playerPoints[i], playerPoints[j], corner1, corner2, player);
                score[player]++;
                updateScore(player);
            }
        }
    }
}

function pointExists(pointsArray, point) {
    return pointsArray.some((p) => p.x === point.x && p.y === point.y);
}

function drawSquare(p1, p2, corner1, corner2, player) {
    ctx.beginPath();
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(corner1.x, corner1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.lineTo(corner2.x, corner2.y);
    ctx.closePath();
    ctx.strokeStyle = player === "A" ? "red" : "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
}

function updateScore(player) {
    document.getElementById(`score${player}`).textContent = score[player];
}

コードを細かく見ていただくか、出来上がったバージョンを触っていただくと分かるのですが、バグが多いですし、自分でセットしたルールがいくつか抜けています。

  • 正方形のみが四角形として認定
  • すでに点を打った箇所に追加で点を打てる
  • そもそも線を引く機能がなく、自動で線が引かれる
    などなど

さて、ここでコードを書ける人であれば、ChatGPTが書いたコードを読み解き、自分で修正を始めるのかもしれません。
しかし今回私は何もコードが分からない馬鹿野郎ですので、更新もChatGPTにお願いすることになります。

そして結論から言うと、第一歩が全く上手くいかず、かなり細かく指示を飛ばす方針へと変えました。

ノーコードでの実装

直接的なバグの修正依頼

まずは線を引く機能の追加を依頼しました。依頼文はこんな感じです。

Line Draw

出来上がった実物とコードを掲載しておきます。
https://gami0901.github.io/chatgpt_game_nocord/1st_proto_draw/index.html

直接的にバグ修正依頼をした際のコード
const canvas = document.getElementById("gameBoard");
const ctx = canvas.getContext("2d");

const gridSize = 40;
const pointRadius = 4;
const players = ["A", "B"];
let currentPlayer = 0;
let points = { A: [], B: [] };
let score = { A: 0, B: 0 };
let lastClickedPoint = null;

canvas.addEventListener("click", (event) => {
    const rect = canvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;

    const gridX = Math.round(x / gridSize) * gridSize;
    const gridY = Math.round(y / gridSize) * gridSize;

    const player = players[currentPlayer];

    if (lastClickedPoint && lastClickedPoint.player === player) {
        const otherPointIndex = points[player].findIndex(
            (p) => p.x === gridX && p.y === gridY
        );
        if (otherPointIndex > -1) {
            const otherPoint = points[player][otherPointIndex];
            drawLine(lastClickedPoint, otherPoint, player);
            lastClickedPoint = null;
            checkForSquare(player);
            currentPlayer = (currentPlayer + 1) % 2;
        } else {
            lastClickedPoint = null;
        }
    } else {
        points[player].push({ x: gridX, y: gridY });
        drawPoint(gridX, gridY, player);
        lastClickedPoint = { x: gridX, y: gridY, player: player };
    }
});

function drawPoint(x, y, player) {
    ctx.beginPath();
    ctx.arc(x, y, pointRadius, 0, 2 * Math.PI);
    ctx.fillStyle = player === "A" ? "red" : "blue";
    ctx.fill();
    ctx.closePath();
}

function drawLine(p1, p2, player) {
    ctx.beginPath();
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.strokeStyle = player === "A" ? "red" : "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.closePath();
}

function checkForSquare(player) {
    const playerPoints = points[player];

    for (let i = 0; i < playerPoints.length; i++) {
        for (let j = 0; j < playerPoints.length; j++) {
            if (playerPoints[i].x === playerPoints[j].x || playerPoints[i].y === playerPoints[j].y) continue;

            const dx = Math.abs(playerPoints[i].x - playerPoints[j].x);
            const dy = Math.abs(playerPoints[i].y - playerPoints[j].y);
            if (dx !== dy) continue;

            const corner1 = { x: playerPoints[i].x, y: playerPoints[j].y };
            const corner2 = { x: playerPoints[j].x, y: playerPoints[i].y };

            if (pointExists(playerPoints, corner1) && pointExists(playerPoints, corner2)) {
                drawSquare(playerPoints[i], playerPoints[j], corner1, corner2, player);
                score[player]++;
                updateScore(player);
            }
        }
    }
}

function pointExists(pointsArray, point) {
    return pointsArray.some((p) => p.x === point.x && p.y === point.y);
}

function drawSquare(p1, p2, corner1, corner2, player) {
    ctx.beginPath();
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(corner1.x, corner1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.lineTo(corner2.x, corner2.y);
    ctx.closePath();
    ctx.strokeStyle = player === "A" ? "red" : "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
}

function updateScore(player) {
    document.getElementById(`score${player}`).textContent = score[player];
}

結論、このコードを試してみると、悪化しました。恐らくですが、線を引く機能を追加した際に、ターンの移行は線を引いた後という条件を自動で追加してしまい、最初の点を打った後の線を引けない状態の時にターン移行が機能しない、という風に見えるバグが発生しました。

できる方であれば、ここまで来れば後は自分で楽々実装できるんですかね?少なくとも私はどこのパートで何をしていてなぜ機能してしていないのかが分からない状態でした。

こいつ、ちゃんとルール理解してるのか?と訝しんだ自分は、改めてルールのセットをし直し、コードを最初から書き直してもらいました。

Second Prototype
Second Prototype2

これでいけるかな?と思って試してみましたが、、、、、、、

全然だめ!!!
https://gami0901.github.io/chatgpt_game_nocord/2nd_proto/index.html

書き直してもらったコード
const canvas = document.getElementById("gameBoard");
const ctx = canvas.getContext("2d");
const passButton = document.getElementById("pass");
const turnText = document.getElementById("turn");

let player = "A";
let points = { A: [], B: [] };
let selectedPoint = null;

canvas.addEventListener("click", (e) => {
    const x = e.clientX - canvas.offsetLeft;
    const y = e.clientY - canvas.offsetTop;

    if (pointExists(x, y)) {
        if (selectedPoint && selectedPoint.player === player) {
            drawLine(selectedPoint.x, selectedPoint.y, x, y, player);
            points[player].push({ x: selectedPoint.x, y: selectedPoint.y, connectedTo: { x, y } });
            selectedPoint = null;
        }
    } else {
        drawPoint(x, y, player);
        points[player].push({ x, y });
        selectedPoint = { x, y, player };
    }

    nextTurn();
});

passButton.addEventListener("click", () => {
    selectedPoint = null;
    nextTurn();
});

function pointExists(x, y) {
    for (const point of [...points.A, ...points.B]) {
        if (Math.abs(point.x - x) < 10 && Math.abs(point.y - y) < 10) {
            return true;
        }
    }
    return false;
}

function drawPoint(x, y, player) {
    ctx.beginPath();
    ctx.arc(x, y, 5, 0, 2 * Math.PI);
    ctx.fillStyle = player === "A" ? "red" : "blue";
    ctx.fill();
    ctx.closePath();
}

function drawLine(x1, y1, x2, y2, player) {
    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.strokeStyle = player === "A" ? "red" : "blue";
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.closePath();
}

function nextTurn() {
    player = player === "A" ? "B" : "A";
    turnText.textContent = `Player ${player}`;
}

スコア機能が消えた・線を引かない・今どっちのターンか分からないからデバッグもできない、、、
とりあえず、一つ一つ丁寧にバグを直すように指示していきましたが、四角形の判定どころの騒ぎではなく、結局完成の目処が立ちませんでした。
Add Order 1
Add Order 2

出来上がったコードとhtmlを今まで通り載せたかったのですが、いろいろ試した結果、全く動かなくなったという状態です。
text-to-gameって本当か?結局プログラマ/エンジニアが必要じゃね?がここまでの所感で、別の方向性で試すことにしました。

MVPをセットしての段階的な実装

さて、ChatGPTのアウトプット精度を上げる手段として、StepByStepでやってみて、という魔法のプロンプトが知られています。
同時にこのプロンプトは、中で考えさせるだけでは効果が薄く、アウトプットさせる方が効果が高いことが知られています。

というわけで、StepByStepで実装することにいたしました。実装順は以下の通りです。

  1. プレイヤーAとBが交互に点を打つ機能
  2. パスボタンを初めターン管理機能
  3. 線を引く機能
  4. 四角形が描けるかを判定する機能
  5. 実際に描かれた四角形を判定する機能

交互に点を打つ機能〜線を引く機能

以下のように、点を交互に打つ機能の実装を依頼しました。
Point
同じスレッドで会話した結果、自動でターン表示もされていて、MVPとしての機能は実装されています。
https://gami0901.github.io/chatgpt_game_nocord/3_point/index.html

点を打つ機能
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dot Game</title>
</head>
<body>
    <canvas id="gameCanvas" width="600" height="600" style="border: 1px solid;"></canvas>
    <p id="turnText"></p>

    <script>
        const canvas = document.getElementById("gameCanvas");
        const ctx = canvas.getContext("2d");
        const turnText = document.getElementById("turnText");

        const gridSize = 40;
        const pointRadius = 5;
        let player = "A";
        let points = [];

        canvas.addEventListener("click", onClickCanvas);
        updateTurnText();

        function onClickCanvas(event) {
            const rect = canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;

            const gridX = Math.round(x / gridSize) * gridSize;
            const gridY = Math.round(y / gridSize) * gridSize;

            if (!pointExists(gridX, gridY)) {
                drawPoint(gridX, gridY, player);
                points.push({ x: gridX, y: gridY, player: player });
                nextTurn();
            }
        }

        function pointExists(x, y) {
            return points.some((point) => point.x === x && point.y === y);
        }

        function drawPoint(x, y, player) {
            ctx.beginPath();
            ctx.arc(x, y, pointRadius, 0, Math.PI * 2);
            ctx.fillStyle = player === "A" ? "red" : "blue";
            ctx.fill();
            ctx.closePath();
        }

        function nextTurn() {
            player = player === "A" ? "B" : "A";
            updateTurnText();
        }

        function updateTurnText() {
            turnText.textContent = `Player ${player} (placing point)`;
        }
    </script>
</body>
</html>

続いてパスボタンの実装を依頼しました。
Pass

https://gami0901.github.io/chatgpt_game_nocord/4_pass/index.html

パスボタンの機能
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dot Game</title>
</head>
<body>
    <canvas id="gameCanvas" width="600" height="600" style="border: 1px solid;"></canvas>
    <p id="turnText"></p>
    <button id="passButton" disabled>Pass</button>

    <script>
        const canvas = document.getElementById("gameCanvas");
        const ctx = canvas.getContext("2d");
        const turnText = document.getElementById("turnText");
        const passButton = document.getElementById("passButton");

        const gridSize = 40;
        const pointRadius = 5;
        let player = "A";
        let points = [];

        canvas.addEventListener("click", onClickCanvas);
        passButton.addEventListener("click", onClickPassButton);
        updateTurnText();

        function onClickCanvas(event) {
            const rect = canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;

            const gridX = Math.round(x / gridSize) * gridSize;
            const gridY = Math.round(y / gridSize) * gridSize;

            if (!pointExists(gridX, gridY)) {
                drawPoint(gridX, gridY, player);
                points.push({ x: gridX, y: gridY, player: player });
                passButton.disabled = false;
                updateTurnText(true);
            }
        }

        function onClickPassButton() {
            passButton.disabled = true;
            nextTurn();
        }

        function pointExists(x, y) {
            return points.some((point) => point.x === x && point.y === y);
        }

        function drawPoint(x, y, player) {
            ctx.beginPath();
            ctx.arc(x, y, pointRadius, 0, Math.PI * 2);
            ctx.fillStyle = player === "A" ? "red" : "blue";
            ctx.fill();
            ctx.closePath();
        }

        function nextTurn() {
            player = player === "A" ? "B" : "A";
            updateTurnText();
        }

        function updateTurnText(drawingLine = false) {
            if (drawingLine) {
                turnText.textContent = `Player ${player} (drawing line)`;
            } else {
                turnText.textContent = `Player ${player} (placing point)`;
            }
        }
    </script>
</body>
</html>

こちらも無事、完了しましたが、実装したことにより自分の設定している要件に不足が見つかる運びになりました。
具体的には、パスボタンを押すまで点を無限に打つことができるというものです。

Pass

こちらも要件を指示することで、無事実装が完了しました。ご丁寧に説明も頂戴しました。
変数を1つ追加するだけでいけるらしいです。賢いですね。

Pass

続いて、線を引く機能の指示をしました。
Draw Line

こちらの指示の際も、ちゃんと動きました。
が、自分の事前要件の定義が不足していて、一部変な機能が出来上がりました。

https://gami0901.github.io/chatgpt_game_nocord/5_draw_line/index.html

線を引く機能を足した時点のコード
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dot Game</title>
</head>
<body>
    <canvas id="gameCanvas" width="600" height="600" style="border: 1px solid;"></canvas>
    <p id="turnText"></p>
    <button id="passButton" disabled>Pass</button>

    <script>
        const canvas = document.getElementById("gameCanvas");
        const ctx = canvas.getContext("2d");
        const turnText = document.getElementById("turnText");
        const passButton = document.getElementById("passButton");

        const gridSize = 40;
        const pointRadius = 5;
        let player = "A";
        let points = [];
        let lines = { A: [], B: [] };
        let lastPlacedPoint = null;
        let hasPlacedPoint = false;

        canvas.addEventListener("click", onClickCanvas);
        passButton.addEventListener("click", onClickPassButton);
        updateTurnText();

        function onClickCanvas(event) {
            if (hasPlacedPoint) {
                const rect = canvas.getBoundingClientRect();
                const x = event.clientX - rect.left;
                const y = event.clientY - rect.top;

                const gridX = Math.round(x / gridSize) * gridSize;
                const gridY = Math.round(y / gridSize) * gridSize;

                const targetPointIndex = points.findIndex(
                    (point) => point.x === gridX && point.y === gridY && point.player === player
                );

                if (targetPointIndex !== -1) {
                    const targetPoint = points[targetPointIndex];
                    if (!lineExists(player, lastPlacedPoint, targetPoint) && !lineExists(opponent(), lastPlacedPoint, targetPoint)) {
                        drawLine(lastPlacedPoint.x, lastPlacedPoint.y, targetPoint.x, targetPoint.y, player);
                        lines[player].push({ x1: lastPlacedPoint.x, y1: lastPlacedPoint.y, x2: targetPoint.x, y2: targetPoint.y });
                        hasPlacedPoint = false;
                        passButton.disabled = true;
                        nextTurn();
                    }
                }
            } else {
                const rect = canvas.getBoundingClientRect();
                const x = event.clientX - rect.left;
                const y = event.clientY - rect.top;

                const gridX = Math.round(x / gridSize) * gridSize;
                const gridY = Math.round(y / gridSize) * gridSize;

                if (!pointExists(gridX, gridY)) {
                    drawPoint(gridX, gridY, player);
                    points.push({ x: gridX, y: gridY, player: player });
                    lastPlacedPoint = { x: gridX, y: gridY };
                    hasPlacedPoint = true;
                    passButton.disabled = false;
                    updateTurnText(true);
                }
            }
        }

        function onClickPassButton() {
            passButton.disabled = true;
            hasPlacedPoint = false;
            nextTurn();
        }

        function pointExists(x, y) {
            return points.some((point) => point.x === x && point.y === y);
        }

        function drawPoint(x, y, player) {
            ctx.beginPath();
            ctx.arc(x, y, pointRadius, 0, Math.PI * 2);
            ctx.fillStyle = player === "A" ? "red" : "blue";
            ctx.fill();
            ctx.closePath();
        }

        function lineExists(player, point1, point2) {
            return lines[player].some(
                (line) =>
                    (line.x1 === point1.x && line.y1 === point1.y && line.x2 === point2.x && line.y2 === point2.y) ||
                    (line.x1 === point2.x && line.y1 === point2.y && line.x2 === point1.x && line.y2 === point1.y)
            );
        }

        function drawLine(x1, y1, x2, y2, player) {
            ctx.beginPath();
            ctx.moveTo(x1, y1);
            ctx.lineTo(x2, y2);
            ctx.lineWidth = 2;
            ctx.strokeStyle = player === "A" ? "red" : "blue";
            ctx.stroke();
            ctx.closePath();
        }

        function nextTurn() {
            player = player === "A" ? "B" : "A";
            updateTurnText();
        }

        function updateTurnText(hasPlaced = false) {
            if (hasPlaced) {
                turnText.textContent = `Player ${player} (selecting line)`;
            } else {
                turnText.textContent = `Player ${player} (placing point)`;
            }
        }

        function opponent() {
            return player === "A" ? "B" : "A";
        }
    </script>
</body>
</html>

Draw Line

ここら辺の時点で感じたこととしては、私自身の要件設計が上手でないときちんと意図を汲み取ってくれないということと、一方で不足した要件がスムーズに分かるということです。

四角形を判定する機能

さて、今回のペンタワーバトルには、線を引く際の例外条件がさらに必要です。
具体的には、四角形が完成する瞬間だけ線を2本引く必要があるというものです。

Check Square
Check Square

四角形判定機能の呼び出し部分
if (selectedPoint) {
        const index = points.findIndex((point) => point.x === gridX && point.y === gridY && point.player === player);
        if (index !== -1) {
            const targetPoint = points[index];
            if (!lineExists(player, selectedPoint, targetPoint) && validLine(player, selectedPoint, targetPoint)) {
                drawLine(selectedPoint.x, selectedPoint.y, targetPoint.x, targetPoint.y, player);
                lines[player].push({ x1: selectedPoint.x, y1: selectedPoint.y, x2: targetPoint.x, y2: targetPoint.y });
                linesDrawnThisTurn += 1;
                if (linesDrawnThisTurn < 2 && canFormRectangle(player, selectedPoint, targetPoint)) {
                    passButton.disabled = false;
                    updateTurnText();
                } else {
                    selectedPoint = null;
                    passButton.disabled = true;
                    nextTurn();
                }
            }
        }
    }
四角形判定機能の該当部分
function canFormRectangle(player, point1, point2) {
    const linesWithSharedPoints1 = lines[player].filter(
        (line) => (line.x1 === point1.x && line.y1 === point1.y) || (line.x2 === point1.x && line.y2 === point1.y)
    );

    for (const line of linesWithSharedPoints1) {
        const thirdPoint = line.x1 === point1.x && line.y1 === point1.y ? { x: line.x2, y: line.y2 } : { x: line.x1, y: line.y1 };

        if (pointExists(thirdPoint.x, thirdPoint.y)) {
            const linesWithSharedPoints2 = lines[player].filter(
                (line) => (line.x1 === point2.x && line.y1 === point2.y) || (line.x2 === point2.x && line.y2 === point2.y)
            );
            for (const line of linesWithSharedPoints2) {
                const fourthPoint = line.x1 === point2.x && line.y1 === point2.y ? { x: line.x2, y: line.y2 } : { x: line.x1, y: line.y1 };
                if (fourthPoint.x === thirdPoint.x && fourthPoint.y === thirdPoint.y) {
                    return true;
                }
            }
        }
    }
    return false;
}

こちらのコードを実装したところ、試しても動かない状態でした。
canFormRectangle関数が何をしているかが分からなかったので、ChatGPTに解説してもらいました。

Check Square

この解説は以下の内容だと理解したのですが、三角形の判断に見える・片方の線はまだ引かれていない線なので探索方法がおかしい、という結論に自分の中で達しました。

Check Square

ここでまたサボって直接的な修正を試したのですが、上手くいかなかったので、四角形判定機能を自分で考えることにいたしました。

四角形を判定する機能を分解する

自分なりに考えて以下のロジックで実装することにしました。
色々と後悔はあるのですが、そちらは感想に回します。

Make Square

以上のロジックの前提で、ChatGPTへ実装方法を考えてもらいます。

  1. まず、片方のプレイヤーの全ての線の座標情報を出力する方法は何でしょうか?
  2. それでは続いて、直前においた点A、直前に引かれた線における反対側の点Bを出力した上で、さらに反対側の点Bと線で繋がっている点のうちAでないものを全て出力する方法は何でしょうか?
  3. では続いて、直前においた点A、直前に引かれた線における反対側の点B、さらに点Bと線で繋がっている点のうちAでない点群を点C群としたとき、点C群のいずれかから線で繋がっているが、A, B,C群のいずれにも該当しない点Dが存在するかを判定するコードを示せますでしょうか?
  4. 上記の流れがcanFormRectangleで実施すべき判定方法です。一つにまとめて、canFormRectangle関数を示せますでしょうか?
  5. 以下のコード(元のコードを貼り付けました)の中に、canFormRectangleを組み込めますでしょうか?

上記の方法で想定していた実装が無事できました。細かいところですが、自分の指示した要件だと、線分ADが既存の線分と交わるパターンを除外できていなかったので、ADが既存の線を跨るかどうかの判断を追加してもらいました。

###四角形をカウントする機能
さて、このパートは省略いたします。
「四角形をカウントする機能を実装してください」や「四角形を判定する機能から類推してカウントする機能を作ってください」と指示したのですが、やはり上手くいかず、
自分でロジックとMVPを考えて実装する運びになりました。

  1. パスボタンを押すたびに加点する機能
  2. その機能を削除し、線が引かれるたびに引かれた線の数だけ加点する機能
  3. その機能を削除し、四角形ができた瞬間にその数だけ加点する機能(ただし、詳細ロジックの説明付き)

Count Square

これで機能の実装は完了し、最後にcssの調整やルール説明の追加をChatGPTに依頼してゲームが完成しました。
試行錯誤も含め、プランニングから4時間はかかってないくらいでしょうか。
https://gami0901.github.io/chatgpt_game_nocord/final/index.html

完走した感想

完走した直後につぶやいた自分の感想なのですが、この記事を書いている今でも、これが一番しっくり来ると思ったので載せておきます。
https://twitter.com/renew_s1/status/1646756003596337152

どのレベルのプログラマ/エンジニアを目指すのかにはよると思いますが、将来的には自然言語によるプログラミングの時代が来るのかなと感じる体験でした。

低級言語から高級言語に移り変わったように、高級言語から自然言語に移り変わる
「昔はみんなアセンブリ言語を書いていたんだよ」と言うように「昔はみんなPythonで計算式を書いてたんだよ」と言う時代が来る

そんなイメージです。

一方で、数学力や論理的思考力は、まだまだ必要そうとも思いました。
今回の例でも、四角形の判定ロジックは自分で考える必要はありました。あんまり大きな声じゃ言えないですが、現在のロジックでは既に対応できないパターンが見つかっており()、そういった思考力は必要そうです。

うまくChatGPTを使えば解法を教えてくれたかもしれないですが、ChatGPTに質問できる時点で論理的思考力が必要だし、その解法の正しさを判断するのにも数学力が必要だと思うと、プログラマ/エンジニアに求められる力は、思考力の方に移り変わるのかもしれません。

ここら辺の議論は、前提となるAIの能力・将来性をどう置くかによって結論が変わるし、そもそも前提が違いすぎて議論が食い違うことが多数発生しているやつなので、各々が考えるAI像をぶつけ合うしかないと思います。

何はともあれ、自然言語で考えているだけで動くものが出来上がるという感覚は、なかなか得難い体験でした。ぜひぜひ皆さんも試してみてください!

エンジニア絶賛募集中!

MESONではUnityエンジニアを絶賛募集中です! XRのプロジェクトに関わってみたい! 開発したい! という方はぜひご応募ください!

MESONのメンバーページからご応募いただくか、TwitterのDMなどでご連絡ください。

書いた人

がみ

田上 翔一(あだな:がみ)

廣瀬谷川鳴海研究室(通称:あの人研)にてVRxスポーツ心理学をテーマに修士課程を修了。その後、ボストン・コンサルティング・グループにて種々のクライアントの経営課題へ取り組んだあと、やっぱりXR領域で現場に近いことがやりたいという一心でMESONに入社。現在ディレクターとして邁進中。
自分でもコードを書けるようになりたいと思っているが、遠い道だなぁと半ば諦めており、プロマネ職には不適切なぐらいエンジニアをリスペクトしている。

Twitter

MESON Works

MESONの制作実績一覧もあります。ご興味ある方はぜひ見てみてください。

MESON Works

Discussion

intet1234intet1234

自然言語が複雑だからルールしかない簡単なプログラミング言語ができたのに…
逆走している