OpenAI o1 によってプロトタイピングはどう変わるか

# はじめに

新年明けましておめでとうございます。

今年は年賀状がわりに2025新年スネークゲームというミニゲームを作成しました。

https://new-year-snake-game.vercel.app

![スクリーンショット 2025-01-04 7.46.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/07213878-5d28-ce6a-0e2e-e475ec07a038.png)

シンプルなゲームではあるのですが、作ろうと思い立ったのが12/30の夜だったのでともかく時間がない。
そこでプロトタイピングの手法を用いつつ、ChatGPTなどを活用していかに高速に実装できるかをチャレンジしてみました。

# アプリ作成の流れ

## 12/30夜: ChatGPT o1とネタだし

息子を寝かしつけた布団の中でゲームアイデア出しの相談をo1としました。

**筆者**: 新年の年賀状がわりにミニゲームを作って公開したいのですが、どのようなアイデアが良いでしょうか？1ゲーム2-3分で遊べるシンプルなものが良いです。

**o1**: 以下に、1ゲームあたり2-3分程度で遊べるシンプルな正月向けミニゲームのアイデアを3つご提案いたします。もしイメージと異なる部分や気になる点があれば、ぜひ遠慮なくお知らせください。
1. 干支(えと)合わせゲーム
   - 十二支(子・丑・寅・卯…)のアイコンやキャラクターをシャッフルして並べ、正しい順番にドラッグ＆ドロップで並べ替えるだけのシンプルパズルです
2. おみくじ風ビジュアルノベル
   - プレイヤーが“今年の抱負を決める”という短いストーリーに沿って、数回の選択肢を選びます
   - 最後にランダムで“大吉・中吉・小吉”などのおみくじ結果が表示され、新年のメッセージで締めくくるスタイルです
3. 餅つきタイムアタック
   - シンプルなタイムアタックゲームで、画面上を動く餅(キャラクター)を一定回数“ぺったん”とタップするなど、テンポよく餅をつくミニゲーム
   - 目標回数やスコアを達成するとメッセージが解放される

**筆者**: 新年は2025年で巳年です。2025は(1+2+…+9)(1+2+…+9)と表せます。この二つをモチーフにしたアイデアを考えて

**o1**: 以下に、「2025＝(1+2+…+9)×(1+2+…+9)＝45×45」と「巳年（へび年）」の2つの要素をモチーフにした、2～3分で遊べるミニゲームのアイデアを3つご紹介します。どれもゲーム時間は短めですが、「へび」のビジュアルや数字の“45”を上手く取り入れることで、ちょっとした新年の話題にもなるかと思います。ぜひ気になるものがあればご要望をお聞かせください！
1. スネーク de 45コレクション
   - フィールドには1～9の数字がランダムに配置されており、プレイヤーは“へび”（スネーク）を操作して画面上の数字を集めていきます。
   - へびの頭が数字に触れるとその数字を獲得し、合計が45に達すればクリア。
   - 時間制限（または長さ制限）を設けることで、2～3分程度の緊張感あるプレイに。
2. マジックスクエア de 巳年パズル
   - 3×3のマス（マジックスクエア）に1～9までの数字を配置して、全ての横列・縦列・対角線が合計15になるように並べるパズルです。
   - クリアすると、合計15の3列分×3方向×2対角線…などの仕組みを通じて、最終的に1～9の合計が45になることがわかる。
   - クリア画面で「45×45=2025」と巳年のイラストやメッセージが表示される。
3. 45×45秒チャレンジ！スネーク・クイズラッシュ
   - 画面にコミカルな“へび”キャラクターが登場し、45秒間の制限時間中に「1～9の合計は？」や「2025を分解すると？」などの計算クイズ（もしくはお正月にちなんだ豆知識クイズ）をテンポよく出題。
   - 各問題に素早く回答するとスコアが加算され、制限時間終了後に総合点を表示。
   - 全部で2ラウンド（合計90秒＝45秒×2）行い、終了時に「45×45=2025」で締めくくり

---


と、こんな感じでディスカッションしながらゲームの原案を考えました。

結果、３０分ほどで以下のようなゲームアイデアが完成しました。

### 完成したミニゲーム原案（o1作）

#### 基本ルール

1. **操作方法**
    - プレイヤーはヘビを上下左右に動かして操作します。
    - ヘビの頭が壁や自分の体に衝突すると即ゲームオーバー。

2. **アイテム（0, 2, 5）の出現**
    - フィールド上には常時最大3個までアイテムが存在します。
    - アイテムが食べられる・または一定時間（例：10秒程度）経過すると、そのアイテムは消滅。
    - 空きが出たタイミングで、新たなアイテムがランダムに生成されます（0:2:5＝等確率）。

3. **体長の増減**
    - アイテムを1つ食べるごとに、ヘビの体長が**+1マス**伸びます。
    - ただし「2 → 0 → 2 → 5」の順番で連続して食べると、即座に体長が-4マス（“2025コンボ”）になります。
    - 連続というのは、違う数字を間に挟まずに「2→0→2→5」を揃えて食べるイメージです。
    - 体長が減るので、自己衝突リスクを抑えられるメリットがあります。

4. **ゲーム終了条件**
    - 時間制限（2～3分などお好みの設定）を過ぎるとゲーム終了。
    - またはヘビが壁・自分の体に衝突した時点で即ゲームオーバー。
    - 終了後は「獲得スコア」や「2025コンボ達成回数」などの結果を表示します。

5. **スコア例（任意）**
    - アイテムを1つ食べるごとに**+10点**。
    - “2025コンボ”を達成するたびに**+50点**のボーナス。
    - 2～3分内に何度コンボを決められるか、どれだけ多くアイテムを食べられるかがカギとなります。

#### 遊び方イメージ

1. **開始直後**
    - ヘビは短い状態でスタート。
    - すでに数個のアイテム（0,2,5）が散らばっている。

2. **アイテムを取りに行く戦略**
    - 基本的には、とにかくアイテムを食べるとスコアUP＆体長+1。
    - ただし、どんどん伸びると自己衝突のリスクが高まる。
    - 「2→0→2→5」を揃えるチャンスを狙いつつ、マップを周回してうまくルートを考えるのがポイント。

3. **アイテムの入れ替わり**
    - 古いアイテムは一定時間で消滅、新しいアイテムが出現。
    - うまく欲しい数字が出るとは限らないため、多少の運要素もありつつ、出たものをどう活用するかがプレイヤーの腕次第。

4. **制限時間終了 / 自己衝突**
    - 2～3分の短いゲームなので、最後まで生き残る緊張感や、どれだけ効率よくスコアを稼げるかに盛り上がり要素があります。
    - 衝突での突然死もあり得るため、油断大敵。

#### バランス調整のヒント

1. **アイテムの消滅時間**
    - 10秒だと入れ替わりが速く、テンポよくチャンスが巡る。
    - 15秒以上だと、食べたい数字を温存しやすくなり、コンボが狙いやすくなる代わりにリスク管理も難しくなる。

2. **体長上限の設定（任意）**
    - ヘビの最大長を設定しておくと、極端に長くなってゲームにならない事態を防げます。
    - 体長が上限に達すると、それ以上伸びずにスコアだけ加算される、などの仕組みも検討できます。

3. **フィールドの広さ・障害物配置**
    - 広めのフィールド＋少ない障害物：コンボを狙いやすいがゲームがやや簡単に。
    - 狭いフィールド＋障害物多め：スリリングで短時間で終わりやすく、リプレイ性UP。

4. **スピードアップ要素**
    - 一定時間ごとにヘビの移動速度が段階的に速くなる仕様にすると、終盤の盛り上がりが増します。
    - ただし初心者にはハードルが高くなるため、難易度調整用のオプションとして実装するのもアリです。

#### まとめ

- 最大3個までアイテムが同時存在→古いアイテムは消える→新しいアイテム出現、というシステムで、常に画面内に程よい数の0,2,5が散らばる状況を作ります。
- “2025コンボ”で体長をマイナス4する仕組みがあるため、序盤～中盤で体長を伸ばしすぎても、コンボを成功させることで一気にリスクをリセットできます。
- 2～3分の時間制限の中で、コンボを何回できるか・どれだけスコアを稼げるかを競う短時間アクションゲームとして仕上げるのがおすすめです。

## 12/31 朝: モック作成

大晦日の朝、昨日のChatGPTとの対話に続けて以下のプロンプトを打ちました。

---

**筆者**: jsfiddleで動かせる形でゲームを作ってください

**o1**: 以下は、HTML/CSS/JavaScriptだけで動作するサンプルコードです。
JSFiddleなどでそのまま貼り付けて動かすことを想定しています。
「最大3つのアイテムが同時存在し、10秒経過で古いアイテムは消える」「2 → 0 → 2 → 5 の順に連続で食べたら体長が4マス縮む」などのルールを簡易的に実装しています。 ...

---

できていたHTMLを表示してみると、こちら

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/4c952109-6afe-9333-8d96-15e24ae9411b.gif)

**高速で壁に激突する🐍...**

それでもエラーにならずゲームが動作しているのは驚異的です。

流石に操作できないので原因を調べてもうちょっとゆっくりにしてもらいました。

**筆者**: requestAnimationFrameをつかっていますが、蛇の進行速度が速すぎます。もっとゆっくりにしてください。

**o1**: 以下は、蛇の移動（ゲームロジックの更新）を一定間隔に抑えることで、蛇の動きをゆっくりにした例です。
元コードでは requestAnimationFrame で毎フレーム moveSnake() を呼んでいましたが、蛇の移動は一定間隔（例：200msごと）だけ行い、それ以外のフレームは描画だけ行うように変更しています。
JSFiddleに貼り付けて「Run」すれば動作を確認できます。...

![snakegame_ver2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/a8d7bfbd-d564-6257-2271-8a1bc5804bd1.gif)

スネークゲームとして最低限遊べるものが出てきました。スネークゲーム自体は有名なゲームということを差し引いてもアイテムの出現ルールなどが仕様通りになっているのはすごいですね。

この後、何回か対話して改善を進めました。

１時間くらいで完成したモックはこちら

![snakegame_mock.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/81aaedc2-e45b-341f-4302-18da0ea993f3.gif)

**なお、ここまでコードは１行も書いていないです。100% o1が書いたコードです。**

<details><summary>モックのソースコード</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>2025スネークゲーム (スタックを2番目から表示)</title>
  <style>
    body {
      text-align: center;
      background: #f5f5f5;
      margin: 20px;
      font-family: sans-serif;
    }
    #gameCanvas {
      background: #fff;
      border: 2px solid #ccc;
      display: block;
      margin: 0 auto;
    }
    #info {
      margin-top: 10px;
      font-size: 14px;
    }
  </style>
</head>
<body>
  <h1>2025スネークゲーム (スタックを2番目以降に表示)</h1>
  <canvas id="gameCanvas" width="400" height="400"></canvas>
  <div id="info"></div>

  <script>
    // === 設定 ============================================
    const COLS = 20;             // 横マス数
    const ROWS = 20;             // 縦マス数
    const CELL_SIZE = 20;        // 1マスの大きさ(px)
    const MAX_ITEMS = 3;         // 同時に存在できるアイテムの最大数
    const ITEM_LIFETIME = 10000; // アイテムが消えるまでの時間(ms)
    const GAME_TIME_LIMIT = 120; // ゲーム制限時間(秒)

    // 蛇の移動速度 (ms): 値が大きいほどゆっくり
    const snakeSpeed = 200; 
    let lastUpdateTime = 0;      // 前回 蛇を動かしたフレームのtimestamp

    // === キャンバス =====================================
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');

    // === スネーク管理 ====================================
    // 各セグメントは { x, y } のみ (どのアイテムで伸びたかは表示しない)
    let snake = [
      { x: 10, y: 10 }, // 頭 (index 0)
      { x: 9,  y: 10 }, // 2番目 (index 1)
    ];
    let vx = 1, vy = 0;  // 蛇の移動方向 (初期は右)
    let snakeLength = snake.length;

    // === スタック管理 (先頭が一番古いアイテム) ============
    // 例: ateStack = ['2','0','5','2', ...]
    // push で末尾に追加 (新しいアイテム)
    // 2番目のセグメント (index=1) に ateStack[0] を表示
    let ateStack = [];

    // === アイテム ========================================
    // { x, y, type, born }
    let items = [];

    // === スコア・時間 =====================================
    let score = 0;
    let startTime = Date.now();

    // === 初期化 ===========================================
    function initGame() {
      document.addEventListener('keydown', handleKey);
      // 初期アイテムをいくつか出現させる
      for (let i = 0; i < MAX_ITEMS; i++) {
        spawnItem();
      }
      updateInfo();
    }

    // === メインループ (requestAnimationFrame) ============
    function gameLoop(timestamp) {
      const now = Date.now();
      const elapsedSec = (now - startTime) / 1000;

      // 時間切れ
      if (elapsedSec >= GAME_TIME_LIMIT) {
        endGame("TIME UP");
        return;
      }

      // 前回の更新からsnakeSpeed(ms) 経過していれば移動
      if (timestamp - lastUpdateTime >= snakeSpeed) {
        lastUpdateTime = timestamp;
        moveSnake();
        checkCollisions();
        updateItems();
      }

      draw();
      requestAnimationFrame(gameLoop);
    }

    // === 蛇の移動 ========================================
    function moveSnake() {
      const head = snake[0];
      const newHead = {
        x: head.x + vx,
        y: head.y + vy
      };
      // 頭を先頭に追加
      snake.unshift(newHead);

      // アイテムを食べたかチェック
      const eatenIndex = items.findIndex(it => it.x === newHead.x && it.y === newHead.y);
      if (eatenIndex >= 0) {
        const eatenItem = items[eatenIndex];
        score += 10;
        snakeLength++;
        // スタックに追加
        ateStack.push(eatenItem.type);

        // 2025判定
        if (check2025Combo()) {
          // 末尾から4つ pop
          ateStack.pop(); // '5'
          ateStack.pop(); // '2'
          ateStack.pop(); // '0'
          ateStack.pop(); // '2'

          // 尻尾を4つ削る (最低1セグメントは残す)
          let cutCount = 4;
          while (cutCount > 0 && snake.length > 1) {
            snake.pop();
            cutCount--;
          }
          snakeLength = snake.length; // 実際の配列長と合わせる

          // ボーナス
          score += 50;
        }

        // 食べたアイテムを削除 & 新規生成
        items.splice(eatenIndex, 1);
        spawnItem();
      }

      // 蛇の長さを揃える
      while (snake.length > snakeLength) {
        snake.pop();
      }
    }

    // === 2025コンボの判定 (スタックの末尾4つ) ============
    function check2025Combo() {
      if (ateStack.length < 4) return false;
      let last4 = ateStack.slice(-4).join(''); // 末尾4つの文字列
      return (last4 === "2025");
    }

    // === 衝突チェック (壁・自己) =========================
    function checkCollisions() {
      const head = snake[0];
      // 壁
      if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) {
        endGame("壁に衝突");
        return;
      }
      // 自己衝突
      for (let i = 1; i < snake.length; i++) {
        if (snake[i].x === head.x && snake[i].y === head.y) {
          endGame("自分の体に衝突");
          return;
        }
      }
    }

    // === アイテムの更新 (古いアイテム削除 & 補充) =======
    function updateItems() {
      const now = Date.now();
      // 古いアイテムを消去
      items = items.filter(it => (now - it.born) < ITEM_LIFETIME);
      // 足りなければ補充
      while (items.length < MAX_ITEMS) {
        spawnItem();
      }
    }

    // === アイテム生成 =====================================
    function spawnItem() {
      const now = Date.now();
      const types = ['0','2','5'];
      const type = types[Math.floor(Math.random() * types.length)];

      let x = Math.floor(Math.random() * COLS);
      let y = Math.floor(Math.random() * ROWS);

      // かんたん衝突回避
      for (let i = 0; i < 10; i++) {
        if (!snake.some(s => s.x === x && s.y === y) &&
            !items.some(it => it.x === x && it.y === y)) {
          break;
        }
        x = Math.floor(Math.random() * COLS);
        y = Math.floor(Math.random() * ROWS);
      }

      items.push({ x, y, type, born: now });
    }

    // === キー入力 =========================================
    function handleKey(e) {
      switch(e.key) {
        case 'ArrowLeft':
          if (vx !== 1) { vx = -1; vy = 0; }
          break;
        case 'ArrowRight':
          if (vx !== -1) { vx = 1; vy = 0; }
          break;
        case 'ArrowUp':
          if (vy !== 1) { vy = -1; vx = 0; }
          break;
        case 'ArrowDown':
          if (vy !== -1) { vy = 1; vx = 0; }
          break;
      }
    }

    // === 描画 =============================================
    function draw() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // スネーク描画
      snake.forEach((seg, i) => {
        // 頭は色を変える
        ctx.fillStyle = (i === 0) ? 'green' : 'limegreen';
        let px = seg.x * CELL_SIZE;
        let py = seg.y * CELL_SIZE;
        ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);

        // 2番目のセグメント (i=1) から ateStack[0] を表示
        // 3番目のセグメント (i=2) に ateStack[1], ... という対応
        let stackIndex = i - 1; 
        if (stackIndex >= 0 && stackIndex < ateStack.length) {
          ctx.fillStyle = 'white';
          ctx.font = "12px sans-serif";
          ctx.fillText(ateStack[stackIndex], px + 6, py + 14);
        }
      });

      // アイテム描画
      items.forEach(item => {
        let color;
        switch(item.type) {
          case '0': color = 'red';    break;
          case '2': color = 'blue';   break;
          case '5': color = 'orange'; break;
          default:  color = 'gray';   break;
        }
        ctx.fillStyle = color;
        let px = item.x * CELL_SIZE;
        let py = item.y * CELL_SIZE;
        ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);

        // アイテム文字
        ctx.fillStyle = 'white';
        ctx.font = "14px sans-serif";
        ctx.fillText(item.type, px + 6, py + 14);
      });

      updateInfo();
    }

    // === ゲーム情報表示 ====================================
    function updateInfo() {
      const now = Date.now();
      const elapsedSec = ((now - startTime) / 1000).toFixed(1);
      document.getElementById('info').textContent =
        `スコア: ${score} | 体長: ${snakeLength} | 経過: ${elapsedSec}s | Stack: [${ateStack.join(', ')}]`;
    }

    // === ゲーム終了 ========================================
    function endGame(reason) {
      draw(); // 最終描画
      alert(`ゲームオーバー: ${reason}\nスコア: ${score}`);
      location.reload();
    }

    // === スタート ==========================================
    initGame();
    requestAnimationFrame(gameLoop);
  </script>
</body>
</html>
```
</details>

## 12/31 午前中: Vite+Vue3でプロトタイプ作成

ここからHTML一枚のモックを公開できるように仕上げていきます。

技術スタックとしては以下のものを採択しました。

- 実装言語: TypeScript
- ビルドツール: Vite
- フロントエンドフレームワーク: Vue3
- CSSライブラリ: Bulma
- デプロイツール: Vercel

ここはChatGPTのおすすめ、というわけではなく、普段から使っていて自分の手に馴染んでいるものを採択しています。

リポジトリの雛形を作って、o1にHTMLから変換してもらいました。

---

**筆者**: 以下のプロトタイプをVue + Vite（TypeScript)のフレームワークで実装したいです。CSSフレームワークはBulmaを使用します。（ここにHTMLコピペ）

**o1**: 以下に、「Vue + Vite(TypeScript) + Bulma」の環境で、提示いただいたスネークゲームを実装するためのサンプル案を3つご紹介します。
どの案もBulmaを導入し、Gameコンポーネント上にキャンバスを配置し、onMountedフックなどでゲームロジックを動かす形を例示します。
ご要望やご不明点などありましたら、遠慮なくお知らせください。
- 案1: コンポーネント単体ですべてのゲームロジックを管理するシンプル実装
- 案2: ロジックをComposableに分割する例
- 案3: 描画部分をCanvasコンポーネントに切り分け、親子通信する例

---

ただコンポーネント分割するだけではなく、**composableを使ってロジックを分離する案まで示してくれました。** これはありがたいです。

提案されたソースをVSCodeにコピペして動かしていきます。

が当てはめても動かないのでソースコードを確認します。原因は以下の箇所でした。

```typescript
    draw()
    animationFrameId = requestAnimationFrame(gameLoop)
  }

  const moveSnake = () => {
    // ... (略: 案1と同じ)
    // アイテム食べ判定 → 2025判定 → 末尾ポップ → スコア加算
  }

  const check2025Combo = () => {
    // ... (略: 案1と同じ)
  }

  const checkCollisions = () => {
    // ... (略: 案1と同じ)
  }

  const updateItems = () => {
    // ... (略: 案1と同じ)
  }

  const spawnItem = () => {
    // ... (略: 案1と同じ)
  }

  const handleKey = (e: KeyboardEvent) => {
    // ... (略: 案1と同じ)
  }
```

めっちゃ省略されてる！！！

AIは油断するとサボるみたいですね。省略された部分を元のソースからコピペしたら無事動きました。

ここからVSCodeのGitHub Copilotを使いつつ手動でリファクタリングしていきました。


https://github.com/autotaker/new-year-snake-game/tree/cbd4f32d581671e63193cb5b04b98ef4bd459a53


## 12/31 午後: ヘビの画像化

ゲームの要素としては概ね問題なかったのですが、今のままだとヘビがヘビっぽく見えないので、ただの四角形ではなく、画像で表示することにしました。

正式版の画像は妻に依頼して書いてもらいましたが、書いてもらう間にモックの画像でゲームに組み込む作業を進めました。

以下のようなプロンプトを書いてモックの画像生成するPythonプログラムをGitHub Copilot Editを使って生成します。

```python
"""
このスクリプトはスネークゲーム用の画像を生成するためのスクリプトです。

出力先: src/assets/snake_images

画像の一覧:
- snake_head_{dst_position}.png
- snake_body_{src_position}_{dst_position}.png
- snake_tail_{src_position}.png

src_position: 進行方向(頭がある方向)
  - up
  - down
  - left
  - right
dst_position: 進行後の方向(尾がある方向)
  - up
  - down
  - left
  - right

例: snake_body_up_right.png: 蛇の頭が上に進行していて、尾が右にある、胴体の画像

画像のサイズは48x48で、背景は透過です。黒い線で蛇の形を書くこと。
蛇の顔は目があるとわかりやすいです。
"""
```

GitHub Copilot Editについてはこちらの記事もご参考にしてください。

https://qiita.com/autotaker1984/items/a59828a9f378cfb79ca6

生成されたスクリプトを若干リファクタリングして使用しました。

[使用したスクリプト](
https://github.com/autotaker/new-year-snake-game/blob/955d17b0f99c7877d560b77bd2bb4dbb91ca74ee/scripts/gen_snake_images.py)

目の位置とか特に指定してないのにいい感じに書いてくれました。

描画ロジックを画像に変更する部分もGitHub Copilotを使いつつ書き換えて組み込みました。

![snakegame_image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/0e30f859-5210-bcef-5154-d0ab77533fad.gif)

やはり画像があるとヘビっぽくなりますね。

あとは画像を入れて差し替えるだけです。

また、画像が出来上がるまでの間に、スマホでも操作できるようにスワイプで操作できるように修正しました。

## 12/31 夜: 公開に向けた調整

息子の寝かしつけが終わったあとに紅白を流しつつ公開に向けた最終調整をしました。

### ヘビ画像のパーツ化

まずは妻が描いてくれたヘビの画像への差し替えを行いました。描いてくれた画像は以下のような画像だったのでこれをimagemagickを使って3x2のパーツに切り分けて、各パーツを回転、反転させて画像を作りました。

![snake_original.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/3572b416-dfcd-c60b-4ae7-fb4bf6116822.png)

パーツ一枚ごとに、コメントでどう回転、反転させるべきかを指示して
GitHub Copilotを使ってimagemagickのコマンドを生成させてスクリプトを作成しました。

これが地味に頭を使う作業で、Copilot単体では全然できなかったので大変でした。何回かバグらせつつ、完了しました。

```bash
#!/bin/bash

# ./tmp/snake_images/以下の画像を回転、反転して画像を増やす
# すでにあるもの
# - snake_body_left_down.png
# - snake_body_left_right.png
# - snake_head_right.png
# - snake_tail_up.png

# 作成するもの
# - snake_body_{src}_{dst}.png
# - snake_head_{dst}.png
# - snake_tail_{src}.png
# src, dstはup, down, left, rightのいずれか
# srcは蛇の頭の方向、dstは蛇の尾の方向

# 画像の回転、反転にはImageMagickのconvertコマンドを使用

# snake_head_{dst}.pngを作成
# snake_head_upはsnake_head_rightを反時計回りに90度回転
convert ./tmp/snake_images/snake_head_right.png -rotate -90 ./tmp/snake_images/snake_head_up.png

# snake_head_downはsnake_head_rightを時計回りに90度回転
convert ./tmp/snake_images/snake_head_right.png -rotate 90 ./tmp/snake_images/snake_head_down.png

# snake_head_leftはsnake_head_rightを水平反転
convert ./tmp/snake_images/snake_head_right.png -flop ./tmp/snake_images/snake_head_left.png

# snake_tail_{src}.pngを作成

# snake_tail_downはsnake_tail_upを垂直反転
convert ./tmp/snake_images/snake_tail_up.png -flip ./tmp/snake_images/snake_tail_down.png

# snake_tail_leftはsnake_tail_upを反時計回りに90度回転
convert ./tmp/snake_images/snake_tail_up.png -rotate -90 ./tmp/snake_images/snake_tail_left.png

# snake_tail_rightはsnake_tail_upを時計回りに90度回転
convert ./tmp/snake_images/snake_tail_up.png -rotate 90 ./tmp/snake_images/snake_tail_right.png

# snake_body_{src}_{dst}.pngを作成

# snake_body_up_downはsnake_body_left_rightを時計回りに90度回転
convert ./tmp/snake_images/snake_body_left_right.png -rotate 90 ./tmp/snake_images/snake_body_up_down.png

# snake_body_up_leftはsnake_body_left_downを反時計回りに90度回転
convert ./tmp/snake_images/snake_body_left_down.png -rotate 90 ./tmp/snake_images/snake_body_up_left.png

# snake_body_up_rightはsnake_body_up_leftを水平反転
convert ./tmp/snake_images/snake_body_up_left.png -flop ./tmp/snake_images/snake_body_up_right.png

# snake_body_down_upはsnake_body_left_rightを反時計回りに90度回転
convert ./tmp/snake_images/snake_body_left_right.png -rotate -90 ./tmp/snake_images/snake_body_down_up.png

# snake_body_down_rightはsnake_body_left_downを反時計回りに90度回転
convert ./tmp/snake_images/snake_body_left_down.png -rotate -90 ./tmp/snake_images/snake_body_down_right.png

# snake_body_down_leftはsnake_body_down_rightを水平反転
convert ./tmp/snake_images/snake_body_down_right.png -flop ./tmp/snake_images/snake_body_down_left.png

# snake_body_left_upはsnake_body_left_downを垂直反転
convert ./tmp/snake_images/snake_body_left_down.png -flip ./tmp/snake_images/snake_body_left_up.png

# snake_body_right_upはsnake_body_left_upを水平反転
convert ./tmp/snake_images/snake_body_left_up.png -flop ./tmp/snake_images/snake_body_right_up.png

# snake_body_right_downはsnake_body_left_downを水平反転
convert ./tmp/snake_images/snake_body_left_down.png -flop ./tmp/snake_images/snake_body_right_down.png

# snake_body_right_leftはsnake_body_left_rightを水平反転
convert ./tmp/snake_images/snake_body_left_right.png -flop ./tmp/snake_images/snake_body_right_left.png
```

### ゲームバランス調整

何回かプレイしてゲームを微調整しました。

- コンボの種類やボーナス得点
- ゲーム盤のサイズ、ヘビの大きさ
- 背景やアイテムの色

この辺はAIは特に使わず、人力で調整しました

### OGP対応

SNSでシェアした時にただのリンクではなく、カードが生成されるように設定をしました。

↓こういうやつです

https://new-year-snake-game.vercel.app/

GitHub Copilot Editで指示したところ雛形を作ってくれたのでそれを埋めて完了しました。

```html
    <meta property="og:title" content="2025新年スネークゲーム" />
    <meta
      property="og:description"
      content="楽しい新年のスネークゲームをプレイしよう！"
    />
    <meta
      property="og:image"
      content="https://new-year-snake-game.vercel.app/new-year-snake.png"
    />
    <meta property="og:url" content="https://new-year-snake-game.vercel.app" />
    <meta property="og:type" content="website" />
```

## 1/1 1時: 公開

そうこうしているうちに年が明け、少し遅れてゲームをリリースしました。

![snakegame_release.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/665b7743-3fa9-d970-4780-49cf3e9a1c48.gif)

リリース後は、発覚した不具合を直したり、Xにシェアするボタンを追加する対応していました。

# AIを使ったプロトタイピングの流れ

今回はAIを使ったおかげで、約1日でミニゲームをリリースすることができました。
o1モデルの登場によって、アイデア出しからHTML一枚で動くモック作成までをAIに任せられるようになりました。ここまでであれば**ほとんどコードの知識がなくても動くものが作れます。**

![プロトタイピング.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67228/93bf7907-fb93-a356-54fb-26d22e870f19.png)


一方で、o1が作ったモックをプロジェクトに組み込んで保守できるコードにしてリリースする際にはやはりコードの知識は必要です。その理由は２点あります。

1. o1の中には既存コードの構成やルールに関する知識がなく、プロンプトで大量に指示する必要がある
1. 型エラーやアプリの不具合のたびにo1のリクエストを投げるのはコストと時間がかかりすぎる

来年にはまたAIモデルが進化して変わっているでしょうが、今の時点でもソフトウェアの作り方を変えていかないとなと感じる正月でした。