ChatGPTを使ってほんの数時間でゲーム「Straight Line Challenge」をリリースしました

![straight-line-challenge-demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/9869b157-4065-ed3e-f6fc-b5d0990e4b60.gif)
ChatGPT(GPT-4)を使用して、2点間をきれいな直線で結んで高得点を目指すゲーム「Straight Line Challenge」をリリースしました🎉

以下のリンクから実際に遊べます！
https://keitomatsuri.github.io/straight-line-challenge/

今日の朝開発を始めて、途中で寝落ちして、作業再開して昼過ぎにはリリースできていたので、実質3~4時間ほどの作業でした。（GitHubアカウントの作成や設定等含む）

この記事では、アプリケーションの開発過程をご紹介します。

## キャンバスの準備
最初のプロンプトは、「fabric.jsを使用して、画面上にマウスドラッグやスマホのタッチで線を描画できるアプリケーションを作成してください。」でした。
![スクリーンショット 2023-03-18 164748.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/3a4e1385-c033-70fd-4013-9b836413efab.png)
これまでに何度かChatGPTに簡単なゲームを作ってもらう中で、まずはシンプルな基盤を用意してから徐々に機能追加していくやり方がうまくいっていたので、今回もそのようなプロセスにしました。

fabric.jsはHTMLのcanvas機能を拡張してくれるJavaScriptライブラリです。
何も指示をしないと素のjsで頑張りそうだったので、より効率的に開発を進められることを期待してライブラリを指定してみました。

得られたChatGPTからの回答は以下の通りです。
![スクリーンショット 2023-03-18 165540.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/340e2ed4-49a5-1c62-a6f7-452cbd83458e.png)
![スクリーンショット 2023-03-18 165559.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/66623dcb-000d-dad4-2e5c-155c4b6c4ebc.png)
![スクリーンショット 2023-03-18 165614.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/b0fb8af2-90e6-694a-a0ac-53b214f10c3b.png)

指示通りファイルを用意してindex.htmlをChromeで開いてみると、エラーが出ています。
![スクリーンショット 2023-03-18 165935.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/7146b64f-d2c0-49c5-19a2-e3f8220fd09e.png)

fabric.jsの読み込みに失敗しているので、cdnに存在するバージョンに差し替える必要があります。
手直しして再度読み込むと、キャンバスに線を書くことができました！
![スクリーンショット 2023-03-18 170151.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/77eebc13-c8cc-75c4-e431-dc241a81a1f1.png)

## スタート・ゴールの追加
線を引く際のスタートとゴールが必要なので、追加を依頼します。
細かい仕様も合わせて箇条書きで記述しています。
![スクリーンショット 2023-03-18 170614.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/bb1587c4-c1fe-b561-040f-b4a220d158d9.png)

先ほど手直ししたfabric.jsのバージョン修正も合わせて依頼しています。

また、出力されるコードが長い際は、何らかの制限に引っかかり「ネットワークエラー」となり出力が中断されてしまうことが多々ありました。
そのため、「・コードが長くなる場合は、40行程度出力したところで出力を停止してください。その後、「続き」と入力したら続きを出力してください。」という指示を加えています。

これにより出力されたjsは以下です。
```js
document.addEventListener('DOMContentLoaded', function() {
  const canvas = new fabric.Canvas('drawingCanvas', {
    isDrawingMode: true
  });

  canvas.on('path:created', function() {
    canvas.calcOffset();
  });

  canvas.freeDrawingBrush.width = 5;
  canvas.freeDrawingBrush.color = '#000000';

  const centerX = canvas.width / 2;
  const centerY = canvas.height / 2;
  const distance = 300;
  const angle = Math.atan2(canvas.height, canvas.width);

  const startX = centerX - (distance / 2) * Math.cos(angle);
  const startY = centerY - (distance / 2) * Math.sin(angle);
  const endX = centerX + (distance / 2) * Math.cos(angle);
  const endY = centerY + (distance / 2) * Math.sin(angle);

  const startCircle = new fabric.Circle({
    left: startX,
    top: startY,
    strokeWidth: 5,
    radius: 10,
    fill: 'green',
    stroke: 'green',
    originX: 'center',
    originY: 'center',
    hasControls: false,
    hasBorders: false,
    lockMovementX: true,
    lockMovementY: true,
  });

  const endCircle = new fabric.Circle({
    left: endX,
    top: endY,
    strokeWidth: 5,
    radius: 10,
    fill: 'red',
    stroke: 'red',
    originX: 'center',
    originY: 'center',
    hasControls: false,
    hasBorders: false,
    lockMovementX: true,
    lockMovementY: true,
  });

  canvas.add(startCircle);
  canvas.add(endCircle);
});

```
jsを差し替えると、2点が表示されました！
![スクリーンショット 2023-03-18 171223.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/e8fc859b-61d9-0112-60ab-1b2958ef35bb.png)

その後、2点をブラウザを更新する度にランダムに生成する、2点間の距離を変えるなどの修正を加えました。
次は一番の難関であろう、採点ロジックの実装です。

## 採点ロジックの実装
まずはざっくりと「2点間を結ぶ線が完璧な直線である場合を100点として、2点間を結ぶ線を採点し、画面上に表示してください。」と依頼してみました。

出力されたコードはこちら。
```js
// ... 既存のコード ...

function getDrawingPathLength(path) {
  let length = 0;
  const points = path.path;
  
  for (let i = 1; i < points.length; i++) {
    const prevPoint = points[i - 1];
    const currPoint = points[i];
    length += Math.sqrt(Math.pow(currPoint[1] - prevPoint[1], 2) + Math.pow(currPoint[2] - prevPoint[2], 2));
  }
  
  return length;
}

function getPerfectLineLength(start, end) {
  return Math.sqrt(Math.pow(end.left - start.left, 2) + Math.pow(end.top - start.top, 2));
}

function updateScoreDisplay(score) {
  const scoreDisplay = document.getElementById('scoreDisplay');
  scoreDisplay.textContent = `Score: ${score.toFixed(2)} / 100`;
}

document.addEventListener('DOMContentLoaded', function() {
  // ... 既存のコード ...

  const scoreDisplay = document.createElement('div');
  scoreDisplay.id = 'scoreDisplay';
  scoreDisplay.style.position = 'fixed';
  scoreDisplay.style.top = '10px';
  scoreDisplay.style.right = '10px';
  scoreDisplay.style.fontSize = '24px';
  scoreDisplay.style.fontWeight = 'bold';
  document.body.appendChild(scoreDisplay);

  canvas.on('path:created', function(e) {
    const pathLength = getDrawingPathLength(e.path);
    const perfectLineLength = getPerfectLineLength(startCircle, endCircle);
    const score = 100 * perfectLineLength / pathLength;

    updateScoreDisplay(score);
  });
});
```

```css
/* ... 既存のコード ... */

#scoreDisplay {
  background-color: rgba(255, 255, 255, 0.7);
  padding: 5px 10px;
  border-radius: 5px;
}
```
変更点が分かりやすく出力されているのは嬉しいポイントですね。
コードを差し替えて実行してみます。

![スクリーンショット 2023-03-18 172209.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/7685164e-76ba-6978-9949-166004cba194.png)
…想定と異なる結果になってしまいました。
右上のスコアを見てみると、2点を通ってすらいないのに150点が記録されています。

そこで、このロジックの問題点を列挙して修正を依頼してみました。
![スクリーンショット 2023-03-18 172359.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/d0edf640-32d4-c389-fe23-ef8fbfdf218a.png)

出力されたコードはこちら。
```js
function isPathThroughPoints(path, point1, point2, radius) {
  const points = path.path;

  let throughPoint1 = false;
  let throughPoint2 = false;

  for (const point of points) {
    if (
      Math.sqrt(Math.pow(point1.left - point[1], 2) + Math.pow(point1.top - point[2], 2)) <= radius
    ) {
      throughPoint1 = true;
    }
    if (
      Math.sqrt(Math.pow(point2.left - point[1], 2) + Math.pow(point2.top - point[2], 2)) <= radius
    ) {
      throughPoint2 = true;
    }
    if (throughPoint1 && throughPoint2) {
      return true;
    }
  }
  return false;
}

document.addEventListener('DOMContentLoaded', function() {
  // ... 既存のコード ...

  canvas.on('path:created', function(e) {
    const pathLength = getDrawingPathLength(e.path);
    const perfectLineLength = getPerfectLineLength(startCircle, endCircle);
    const throughStartOrEnd = isPathThroughPoints(
      e.path,
      startCircle,
      endCircle,
      startCircle.radius
    );

    if (throughStartOrEnd) {
      const score = Math.min(100 * perfectLineLength / pathLength, 100);
      updateScoreDisplay(score);
    } else {
      updateScoreDisplay(0);
    }
  });
});
```
これを差し替えたものが、実は今実装されている採点ロジックです。
再度デモをご覧いただくか、実際に触って確認してみてください。
1回の指摘でここまで違和感のないロジックを組んでくれました。ChatGPT…おそろしい子…！

採点ロジックを説明してもらいました。
![スクリーンショット 2023-03-18 172922.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/8bab39b4-4d0a-30b8-38be-9a8fe11bfd65.png)

細かいところはまだまだ改善の余地があるとは思いますが、一発でここまで持ってきてくれたので今回はこれでOKとしました。

## その他修正
その他、以下のような修正を依頼しました。
- 新しい線が描かれ始めたら、以前の線を画面上から消す。
- スコアをキャンバスの下に表示する。
- ゲームの簡単な説明をキャンバスの上に英語で追加する。
- スタイルを変更する。

このあたりは私の指示が悪かったのか、一部手直しが発生しました。
ロジックの部分は重複した処理を1つ消すくらいでしたが、UIに関する部分はなかなかうまくいかず、それっぽい状態で妥協しています。

このあたりのコツも掴めるようになりたいですね。

## ゲームに名前をつける
最後に、このゲームに名前をつけてもらいました。
![スクリーンショット 2023-03-18 173902.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290212/d4de51ca-4a97-2d6c-9719-96071ddf6083.png)

StraightLineChallenge（ストレートラインチャレンジ）がシンプルで分かりやすいと思ったのでこれにしました。

あとは、一部の色やテキストを手直しし、GitHubにpushしてPagesで公開するだけの簡単な作業でした。

## さいごに
今回は非常にシンプルなゲームを扱いましたが、ChatGPTをはじめとするLLM関連のサービスによって、アプリケーションの開発速度は飛躍的するだろうなと実感できました。

仕事・趣味問わず、活用の方法を模索していきたいですね。

さいごに、twitterアカウントも作ったので、フォローしていただけると嬉しいです！