マガジン

同じ仕様書を3回渡して、出てくるコードはどれだけ違うか

遊びをつくる

執筆にClaudeを使用しています。

コード・絵・音を表す三つの作業領域を、中央の手と黄色い駒がつなぐ版画
記事のテーマをもとに制作したAI生成イラスト。

同じ仕様書をAIに3回渡して、3つのコードが返ってくる場面を考えてみます。ファイルを開くと、見た目はほとんど同じなのに中身の書き方が違うかもしれません。逆に、書き方は似ているのに、入力を間違えたときの挙動だけが違うかもしれません。この「違い」は一括りにすると、どれも同じ重さに見えてしまいます。 この記事は結果を報告するものではなく、読者が自分で行うための実験の設計書です。確かめるのは、同じ仕様書での再現性です。コードの差を「挙動」「構造」「表面」の三つの層に分けて数え、どの層が安定し、どの層が揺れるのかを切り分けます。ゲームを作る人にとって、これが分かると仕様書のどこを厳密に書き、どこを任せてよいかが見えてきます。 実験の中心は、仕様書に書いていない部分がどう埋められるかを見ることです。そこで、あえて空白を一つ残した小さな仕様書を使います。

この記事の目次
  1. 準備:仕様書を固定し、条件をそろえる
  2. 手順:3回の生成と保存
  3. 記録:三つの層に分けて残す
  4. 結果の読み方:揺れの場所で判断を変える
  5. 起こりやすい誤り
  6. 次の行動

準備:仕様書を固定し、条件をそろえる

題材は、1〜100の数を当てる「数当て」にします。小さな題材にするのは、3つのコードを読み比べて動作まで確かめる作業を、1回の実験で終えられるようにするためです。次の仕様書を、一字も変えずに3回使います。

  1. 1〜100の整数をランダムに一つ決めます。
  2. 入力欄と「答える」ボタンを置きます。
  3. 入力が整数でない、または1〜100の範囲外なら、エラー文を表示します。この入力は回数に数えません。
  4. 答えが正解より大きければ「もっと小さい」、小さければ「もっと大きい」と表示します。
  5. 挑戦できるのは最大7回です。残り回数を常に表示します。
  6. 正解したら、何回目で当たったかを表示して終了します。7回で当たらなければ正解を表示して終了します。
  7. 終了後に「もう一度」ボタンで最初からやり直せます。
  8. 成果物は、直下に置く単一の index.html です。外部ライブラリ、外部通信、サーバー用コード、サービスワーカーは使いません。データは保存しません。

8番は、アソバースへHTMLを直接アップロードするときの条件(直下の index.html が入口、サーバー用コードとサービスワーカーは対象外)に合わせています。出力形式を仕様書に入れておくと、「ファイル構成」の差が最初から消え、比べたい部分に集中できます。

空白は意図的に残しています。たとえば「前に入力した数と同じ数をもう一度入れたらどうするか」は書いていません。回数に数えるのか、警告するのか、何も言わないのか。仕様に書かれていない部分は、生成のたびに別の判断で埋められる可能性があります。ここに限らず、3つのコードの間で判断が食い違う箇所を探すのがこの実験の主眼です。

次の条件を、3回とも同じにします。

  • 使うAIツール、モデル、設定(選べる場合)。設定の詳細は、使うツールの案内で確認してください。
  • 仕様書の文面。コピーして貼り付け、毎回同じ前置きだけを添えます。
  • 会話の状態。毎回、新しい会話を開始します。

手順:3回の生成と保存

手順は次のとおりです。

  1. 新しい会話を開き、仕様書を貼って「この仕様で作ってください」と依頼します。
  2. 返ってきたコードを、手を加えずに run1.html として保存します。
  3. 会話を閉じて新しい会話を開き、同じ文面で依頼して run2.html を保存します。
  4. もう一度同じことをして run3.html を保存します。

「手を加えずに」が大切です。エラーが出ても直さず、そのまま保存してください。動かないコードが出たなら、それも「揺れ」の一つとして記録します。

保存したら、ブラウザで3つとも開き、仕様書の項目1〜8を一つずつ実際に操作して確かめます。コードを読むだけで判断せず、必ず遊んでください。見落としやすいのは、範囲外入力が回数に数えられていないか、7回目の扱い、「もう一度」で残り回数が戻るかの3点です。

記録:三つの層に分けて残す

差を数える前に、層を決めておきます。

  • 挙動、見る対象:仕様書の各項目を満たすか、空白をどう埋めたか、記録の単位:項目ごとに○×、空白は内容を一文で
  • 構造、見る対象:状態の持ち方、関数の分け方、DOMの作り方、記録の単位:状態変数の数、関数名の一覧、更新方法の種別
  • 表面、見る対象:変数名、画面の文言、色、要素のid、記録の単位:一致・不一致の一覧

挙動の層は、各項目を run ごとに○×で付けます。空白の項目(同じ数の再入力など)は、実際に同じ数を2回入れて、回数が増えるか、警告が出るか、無反応かを書き留めます。

構造と表面の層は、機械的に数えられます。次の最小スクリプトは、選んだHTMLファイルから id、変数名、関数名を取り出し、ファイル同士の重なり具合を0〜1で出します。ローカルで開くだけで動き、外部通信はしません。

html
<input type="file" id="f" multiple accept=".html">
<pre id="out"></pre>
<script>
const jac = (a, b) => {
  const u = new Set([...a, ...b]);
  if (u.size === 0) return 1;
  let n = 0;
  for (const x of a) if (b.has(x)) n++;
  return n / u.size;
};
document.getElementById('f').addEventListener('change', async (e) => {
  const rows = [];
  for (const file of e.target.files) {
    const t = await file.text();
    const pick = (re) => new Set([...t.matchAll(re)].map(m => m[1]));
    rows.push({
      name: file.name,
      lines: t.split('\n').length,
      ids: pick(/id="([^"]+)"/g),
      vars: pick(/\b(?:const|let|var)\s+(\w+)/g),
      funcs: pick(/function\s+(\w+)/g)
    });
  }
  let s = '';
  for (const r of rows) {
    s += `${r.name}: ${r.lines}行 / id ${r.ids.size} / 変数 ${r.vars.size} / 関数 ${r.funcs.size}\n`;
  }
  for (let i = 0; i < rows.length; i++) {
    for (let j = i + 1; j < rows.length; j++) {
      const a = rows[i], b = rows[j];
      s += `${a.name} × ${b.name}: id ${jac(a.ids, b.ids).toFixed(2)}`
        + ` / 変数 ${jac(a.vars, b.vars).toFixed(2)}`
        + ` / 関数 ${jac(a.funcs, b.funcs).toFixed(2)}\n`;
    }
  }
  document.getElementById('out').textContent = s;
});
</script>

1.00ならまったく同じ、0に近いほど別物です。これは名前の一致を見るだけの粗い道具なので、数値は目安にとどめ、構造の判断は実際にコードを読んで補ってください。読むときは、状態を持つ変数が何個あるか、画面の更新が「毎回全部描き直す」のか「必要な箇所だけ書き換える」のか、入力検証が一つの関数にまとまっているかを、run ごとに一行ずつメモします。

結果の読み方:揺れの場所で判断を変える

3つの記録がそろったら、各項目を「3つとも同じ」「2つが同じ」「3つとも違う」に分けます。そのうえで、揺れた場所によって次のように読みます。

  • 挙動(仕様項目が×)、読み方:仕様書の書き方が曖昧か、AIの取りこぼしの可能性があります。、次の手:該当項目を数値や条件で書き直し、再度3回試します。
  • 挙動(空白の埋め方が割れた)、読み方:仕様書が答えを決めていない部分です。、次の手:作りたい挙動を一文足して固定します。
  • 構造(挙動は同じ)、読み方:遊ぶ側には影響しません。、次の手:後で自分が改造しやすい形かどうかだけ見ます。
  • 表面(名前・文言・色)、読み方:ほぼ必ず揺れる前提の部分です。、次の手:固定したいなら、文言や色を仕様に書きます。

特に注目したいのは、挙動の空白です。3回とも同じ埋め方になったなら、その部分は仕様に書かなくても安定している可能性があります。割れたなら、そこは作り手が決めるべき設計判断で、書かなければ運任せになります。

仕様項目が3回とも満たされ、空白の埋め方も一致していたら、その仕様書は再現性が高いと言えます。ただし、3回だけでは傾向しか分かりません。「この仕様書なら安定していそうだ」と読むにとどめ、断定しないでください。

構造の層が揺れても慌てる必要はありません。ゲームの遊び心地は挙動で決まります。構造の揺れが問題になるのは、後から機能を足す場面です。3つのうちどれが自分に読みやすいかを、改造のしやすさの観点で選んでください。

起こりやすい誤り

実験を台無しにしやすい点を挙げておきます。

  • 同じ会話の中で3回続けて頼むこと。前の出力が文脈に残り、2回目以降は1回目に引きずられる可能性があります。必ず新しい会話で行います。
  • 仕様書にサンプルコードを入れること。コードが固定の手がかりになり、再現性を測るはずが、写経の精度を測ることになります。
  • 行数だけで差を判断すること。コメントや改行の入れ方で行数は変わります。行数は参考値にとどめます。
  • 動かさずに読むだけで済ませること。見た目が似たコードでも、入力検証の一行で挙動が変わります。
  • 関数検出の限界を忘れること。上のスクリプトは function 名前 の形だけを拾います。const check = () => {} のような書き方は、関数としては数えられず、変数として数えられます。関数の数は、コードを読んで補正してください。
  • 動かないコードを直してから保存すること。直した時点で、それは生成結果ではなく自分の編集になります。

次の行動

まず上の仕様書を使って、新しい会話で3回生成し、run1〜3.html を保存してください。挙動の表を埋めたあと、揺れた項目だけを仕様書に書き足して、もう一度3回試すと、書き足しが効いたかを確かめられます。安定した部分と、毎回書き添えるべき部分の一覧ができれば、次に自分のゲームを作るときの仕様書の雛形として使えます。