マガジン

「一つのファイルで」と頼むと、AIは何をまとめるか

遊びをつくる

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

同じ形の三つの迷路試作品を、砂時計とノートを添えて比較する卓上写真風イメージ
記事のテーマをもとに制作したAI生成の写真風イメージ。

ゲームづくりをAIに頼むとき、「ファイルは index.html、style.css、game.js に分けて」と指定しなければ、AIは自分で分割を決めることがあります。ところが「一つのファイルで」と一行足すと、その判断は取り上げられます。そのとき、AIがもともと分けていた境界はどこへ行くのでしょうか。見出しコメントになって残るのか、オブジェクトにまとまるのか、それとも境界ごと消えるのかが問いです。 この記事は結果を報告するものではなく、答えを自分で出すための実験の設計書です。仕様文は固定し、変えるのは一行だけにします。出てきたコードの構造を、同じ物差しで数えて分類します。結果は、読者が使うモデルと設定によって変わります。 題材の選び方にも理由があります。アソバースへの投稿はHTMLの直接アップロードのほか、ZIPでも行えます。ZIPなら直下の index.html を入口に、素材を相対パスで読み込ませる構成を取れます。単一ファイルかZIPかは、投稿のたびに迫られる設計判断です。AIにその判断を任せたとき、あるいは任せなかったときに何が起きるかを知っておくと、次の依頼文を決めやすくなります。

この記事の目次
  1. 実験の問いと、変える一行
  2. 準備するもの:固定する仕様と、固定してはいけないもの
  3. 手順:生成から計測まで
  4. 記録の取り方:何を、どの単位で
  5. 結果の読み方:どうなればどう判断するか
  6. 誤りやすい点
  7. 次の行動

実験の問いと、変える一行

確かめたいのは次の二つです。

  1. ファイル構成を任せたとき、AIは分割するか。分割するなら、どんな単位で分けるか。
  2. 「一つのファイルで」と指定したとき、その単位はコードの中にどんな形で残るか。

比較する条件は三つです。仕様文はすべて共通にし、末尾の一行だけを差し替えます。

  • A(任せる)、末尾の一行:ファイル構成は任せます。
  • B(単一ファイル)、末尾の一行:成果物は index.html 一つだけにしてください。
  • C(単一+境界の指示)、末尾の一行:成果物は index.html 一つだけにしてください。中は役割ごとに読み分けられるようにしてください。

Cは余裕があれば加えてください。Bと比べると、「境界を残すかどうか」を指示した場合の違いが見えます。最小の実験はAとBの二条件です。

準備するもの:固定する仕様と、固定してはいけないもの

共通の仕様文は、分割の余地が生まれる程度に機能を持たせます。単純すぎると、どの条件でも同じ短いコードになってしまうためです。スライドパズルを例にした仕様を示します。

  • 3×3のスライドパズル。数字タイルを空きマスに向けて動かし、1から8の順にそろえる。
  • シャッフルは、必ず解ける配置になるようにする。
  • 手数を表示する。
  • 「一手戻す」ボタンで直前の状態に戻せる。
  • 矢印キーとタップ(クリック)の両方で操作できる。
  • そろったら完成の表示を出す。
  • 外部のライブラリやサーバーは使わない。

この仕様には、状態(盤面と手数)、描画、入力、シャッフル、履歴という複数の役割が含まれます。AIが分割を考える材料が十分にあります。

準備の段階で、次のことを決めておきます。

  • 使うモデルと設定。三条件で同じにします。
  • 会話は毎回新しく始めます。前の出力が文脈に残ると、構造が引きずられます。メモリ機能やプロジェクト指示がある環境では、構造に関する指示が入っていないことを確認します。
  • 試行回数。各条件5回が目安で、最低3回は必要です。同じ依頼でも出力は毎回変わるため、1回ずつでは条件の差かばらつきか判別できません。
  • 採用するのは、各試行の最初の出力だけです。「ここを直して」と重ねた後の出力は使いません。

動作確認のチェックリストも、生成の前に作ります。上の仕様の各項目(解ける配置、手数、一手戻す、矢印キー、タップ、完成表示)を「できた/できない」で判定できる文にします。構造の比較は、動くコードどうしでなければ意味が薄くなるためです。

手順:生成から計測まで

条件ごとに次の順で進めます。

  1. 新しい会話を開き、共通の仕様文に条件の一行を足して送ります。
  2. 返ってきたものが複数ファイルなら、すべて保存します。分割された場合は、その分け方(ファイル名と、各ファイルの役割)をメモします。
  3. チェックリストで動作を確認し、何項目できたかを記録します。
  4. 単一ファイルの出力は、下の計測ページで数えます。複数ファイルの出力は、比較用に手作業で一つにつなぎます。つなぎ方は次の節に書きます。

計測ページは、保存した .html を読み込んで、行数や関数宣言の数を出す小さなページです。ブラウザで開くだけで動き、通信はしません。DOMParserは <script> を実行しないので、読み込んだゲームが動き出すこともありません。

html
<!doctype html>
<meta charset="utf-8">
<input type="file" id="f" accept=".html">
<pre id="out"></pre>
<script>
const lines = s => s.split('\n').filter(l => l.trim()).length;
const count = (re, s) => (s.match(re) || []).length;
document.getElementById('f').addEventListener('change', async e => {
  const text = await e.target.files[0].text();
  const doc = new DOMParser().parseFromString(text, 'text/html');
  const js = [...doc.querySelectorAll('script:not([src])')]
    .map(s => s.textContent).join('\n');
  const css = [...doc.querySelectorAll('style')]
    .map(s => s.textContent).join('\n');
  const result = {
    '全体の非空行数': lines(text),
    'JSの非空行数': lines(js),
    'CSSの非空行数': lines(css),
    'function宣言の数': count(/^\s*(async\s+)?function\s/gm, js),
    'アロー関数の数': count(/=>/g, js),
    '行頭のlet/const/var': count(/^(let|const|var)\s/gm, js),
    'コメントの数': count(/\/\/|\/\*/g, js),
    'addEventListenerの数': count(/addEventListener/g, js)
  };
  document.getElementById('out').textContent =
    JSON.stringify(result, null, 2);
});
</script>

数え方は正規表現による近似です。「行頭の let/const/var」は、インデントなしで宣言された変数を数えるので、トップレベルの変数のおおよその数として扱います。文字列の中の // までコメントに数えるなど、誤差があります。絶対値より、条件間の相対的な違いを見るための道具と考えてください。

記録の取り方:何を、どの単位で

次の表を、1試行1行で作ります。

条件 / 試行 / 動作(項目数/全項目) / ファイル数 / JS非空行 / function宣言 / 行頭変数 / コメント / 境界の分類

最後の「境界の分類」が、この実験の中心です。ここだけは機械で数えられないので、コードを読んで判定します。分類の候補を先に決めておくと、判定が揺れません。

  • ファイルで分離:役割ごとにファイルが分かれている(条件Aで起こりうる)。
  • 見出しコメント:// ---- 入力 ---- のような区切りコメントで領域を分けている。
  • オブジェクトにまとめる:state、view、input のような名前の下に関数や値を集めている。
  • 即時関数やモジュール風:関数のスコープで包んで内部を隠している。
  • 境界なし:役割の区切りが見当たらず、順番に書き流している。

一つの出力に複数の分類が当てはまることもあります。その場合は当てはまるものをすべて記録します。

条件Aで複数ファイルが出たときは、つなぎ方に注意します。ファイルを上から順にそのまま連結し、CSSは <style>、JSは <script> に入れるだけにしてください。名前の整理や重複の除去までやると、測りたい構造が自分の手で変わってしまいます。つなぎ後の数値は、Bの数値と並べて比べられます。ただし、元の分割の仕方は別にメモしておきます。

結果の読み方:どうなればどう判断するか

数値を見る前に、各条件の中の幅を確認します。たとえば条件Bの5回で、関数宣言の数が大きく散らばっているなら、AとBの差が小さく見えてもばらつきの範囲に収まっている可能性があります。条件間の差が条件内の幅より明らかに大きいときだけ、差があると読みます。

次に、境界の分類で読み分けます。以下は、結果の形ごとの解釈の例です。

  • AでファイルがあったときBで見出しコメントが増える:分割の判断は、ファイルをなくしても領域の区切りとして保たれていると読めます。指示は物理的な分割だけを取り除いた、と解釈できます。
  • Bでオブジェクトや関数のまとまりが増える:区切りが、コードの論理的な単位に移ったと読めます。コメントと比べて、どちらが後から手を入れやすいかは、次の節の観点で判断します。
  • Bで境界なしが増え、関数が長くなる:分割の判断を取り上げた結果、構造まで一緒に失われた可能性があります。条件Cで改善するなら、一行の足し方で防げることになります。
  • AとBでほとんど差がない:この仕様の規模では、AIがもともと分割していなかった可能性があります。仕様を大きくして再実験する価値があります。

構造の良し悪しは、行数だけでは決まりません。判断に使える観点を挙げます。

  • 「手数の表示を変えたい」と思ったとき、読む場所を一か所に絞れるか。
  • 「一手戻す」の処理が、盤面の更新処理とどこで結びついているか。
  • 状態を書き換える場所が、散らばっていないか。

この三つは、各出力の該当箇所を探してみればだれでも確かめられます。探すのに何分かかったかを残しておくのも、一つの目安になります。ただし、そのとき探す人が出力ごとの内容をすでに知っているとずれが出ます。できれば条件名を伏せて、順序をばらした状態で見てください。

誤りやすい点

この種の比較は、条件以外のものが混ざりやすいです。

  • 一行以外の差が入る:「一つのファイルで」と「index.html一つだけで」は、似ていても別の文です。実験中に言い換えると、別の条件になります。
  • 会話の持ち越し:同じ会話で続けて頼むと、前の出力の構造が影響します。毎回新しい会話にしてください。
  • 途中の修正を混ぜる:エラーを直してもらった後のコードは、最初の出力とは別物です。動かなかった試行は「動作の項目数が少ない」と記録して、そのまま残します。
  • 動かないコードを構造で評価する:チェックリストの合格数が大きく違う試行は、別扱いにするか、動いた試行だけで比べます。
  • 行数を優劣と読む:行数が多い方が丁寧とは限りません。重複で増えていることもあります。同じ処理を複数回書いていないか、コードを見て確かめます。
  • 計測が近似であることを忘れる:数値は目安です。傾向が分類の読みと一致するかを確認する補助に使います。
  • 一般化しすぎる:この仕様、このモデルでの結果です。別のジャンルや規模では変わる可能性があります。

次の行動

まず、手元の仕様を一つ決めて、AとBの二条件を各3回ずつ試してください。計測ページと動作チェックリストを先に用意しておくと、判定がぶれません。得られた分類を見て、次に自分のゲームを投稿するとき、単一のHTMLにするかZIPで分けるかを決める材料にしてみてください。