こんにちは。開発のSSです。

Claude Code に開発を丸投げして、ブラウザで動くアプリを1本、完走させました。JavaScript約4,400行、テスト39本、コミット245回。**私が自分の手で書いたコードは1行もありません。** それどころか、私がまともに出した指示は、一番最初の1つだけでした。

先に正直に白状しておきます。この記事に出てくる工夫――`vm`で偽のwindowを作ってテストする、ボットに何年もプレイさせてバランスを数字で見る――は、どれも**私の工夫ではありません。全部AIが自分で判断してやったこと**です。私は技術的な判断を一つもしていないし、「テストを書いて」とお願いした覚えすらありません。作っていて一番面白かったのは、**AIが自分で自分にダメ出しをして、バグを直し、テストを足し、バランスの目標まで立てて調整していた**ことでした。この記事は、その様子を後から眺めた記録です。

読みどころは2つ。
 
  • **どれだけ“言わずに”済んだか**――私が出した“最初の1つ”と、その後AIが勝手にやっていたこと(任せ方)
  • **AIが自分の数値調整を検算するために用意した仕組み**――シード固定のヘッドレスシミュ(暴走を止めるガードレール)
念のため断っておくと、ここから先はノーコードの魔法話ではなく、`vm`やシード固定乱数といった**かなり泥臭いNode.js/フロントの裏側**の話です。丸投げを“それらしく動く”で終わらせないための地味な足場、と思って読んでください。
 
題材は自社の仕事――SES・受託開発の会社経営――をモチーフにした経営シミュレーションゲームです。**きっかけは、公開されたばかりの Claude Fable 5 というモデルを触ってみたかったこと。** ちょうどブログの当番も回ってきて、書けるネタも無かったので、開き直って“ネタそのもの”を作ることにしました。手を動かしたのは Claude Code で、そのモデルにこの Fable 5 を指定しています(以前このブログで営業事務のS.Tさんが「Claudeに業務アプリを作ってもらった話」を書いていましたが([ある日のこと])、今回はその開発者版です)。できたものはこちらで遊べます(無料・ブラウザ)→[ゲームURL]。以下、ゲームはあくまで“例題”です。
 

## 私が出した指示は、最初の1つだけだった

丸投げといっても最初は身構えていました。ところが、最初に渡した制約が効いたのか、以降はほとんど口を出す場面がありませんでした。**私が実際にお願いしたのは、これだけです。**
 
自社みたいな会社の経営ゲームを作りたい。どこに置くことになるか決まっていないし、置き先のサーバ環境も細かく分からないので、環境を選ばず“置けば動く”スタックにしておいてほしい。要件定義もそちらでやって、自分はレビューだけにしたい。
 
ポイントは**制約(置き先が未定・環境も不明)を先に渡した**こと。これがないと React+Vite のような「ビルド前提の定番」が返ってきがちです。この最初の依頼から、AIはビルドも外部ライブラリも無い**素のHTML/JS+Canvas**(`<script>`分割+`window.SES.*`名前空間)を選び、「置けば動く」構成に決めました。スタックを選んだのも、私ではありません。その代わり、モダンなSPAのようなリッチな手触りではありません。“どこでも置けば動く”を最優先した割り切りです。この記事の主役も、見た目の作り込みではなく、その下でAIが自分を律していた仕組みのほうだと思ってください。
 
そして「要件定義もそちらで/自分はレビューだけ」を、AIは文字通り実行しました。以降で“AIが勝手にやっていた”ことを2つ挙げます。
 

**その1:AIは、自分のバグを自分で見つけて、テストまで足していた。**

開発の途中、社員を1人も雇っていないのに「他社があなたの社員に引き抜きの誘いを…」というイベントが出る不具合がありました。これを私が指摘した記憶はありません。AIが「社員0人なのに引き抜きイベントが出る」と自分で気づいて直し、しかも**同じバグを検出するテストを自分で追加**していました。

AIのコードは**「動く」と「正しい」が別物**で、こういう発生条件の漏れはコードレビューでは見抜きにくい(ロジック単体はきれいに書けているからです)。それを言われる前に潰していたわけです。テストが39本あるのは、私がテストを書いたからでも「書いて」と頼んだからでもなく、AIがこれを習慣にしていた結果でした。

(「ランダムに出るイベントのテストなんて書けるのか」と思うかもしれません。イベントは各定義に「出してよい条件(`cond`)」を持っていて、このバグの正体は引き抜きイベントの`cond`に「社員が一定数いること」のチェックが無かっただけ。そして**乱数はシード固定で決定的**なので、「このシードでイベントを100回引いても、条件を満たさない場面では当該イベントは出ない」と素直に書けます。乱数さえ固定すればイベントは“必ず同じ順で出る”ので、ランダム要素はむしろ落とし穴を突きやすいのです。)
 
 

**その2:AIは、自分でバランスの目標を立てて、自分で調整していた。**


数値バランスの調整も、私が「3年目に黒字転換するように」などと細かく注文したわけではありません。AIが自分で「シード5本×10年の資金カーブを出して、3年目に黒字転換するように調整する」といった目標を立て、次に説明する仕組みで検算しながら詰めていました。私はその数字を見て頷いていただけです。

「面白い難易度にして」のような曖昧な言い方だと、AIは平気で大味にします。ところが今回、その曖昧さを埋める“数字の共通言語”を、AIは自分で用意していました。それがもう一つの読みどころです。
 
 

## AIが用意した“暴走を止める”仕組み(数字で見張るシミュ)

 
 
AIに数値バランスを任せると、悪気なく大味にします。「動く」ので、目視や単発プレイでは気づけません。面白いのは、**AIがその弱点を自分で埋める仕組みまで用意していた**ことです。人がプレイしなくても“面白い数字になっているか”を機械的に測る、シード固定のヘッドレスシミュ。合理的に経営するボットに何年もプレイさせて集計します。

ボットの中身は、**経営方針をそのまま優先順位つきのコードにしたもの**です。
 
```js
// sim/headless.js のボット step()(毎日呼ばれる。実物を要約)
function step() {
  if (engCount() === 0) tryHire('eng');                         // まず働き手を1人
  if (salesCount() === 0 && engCount() >= 1) tryHire('sales');  // 営業0だと新規案件が来ない
  if (adminCount() < Math.ceil(staffCount() / 6)) tryHire('admin'); // 6人に1人は事務
  if (seatsAlmostFull() && cashRich()) E.expandOffice();        // 資金に余裕があれば増築
  acceptWhatWeCan();                                            // 空きエンジニアで貼れる案件を受注
  for (const s of staff()) {                                    // 給料が相場を12%超下回ったら昇給
    const m = E.marketSalary(s);
    if (m > 0 && (m - s.salary) / m > 0.12) E.raiseSalary(s.id);
  }
}
```

この“まっとうな経営”がボットの基準線です。**この方針で回してもつまらない数字(黒字転換が遅すぎる/退職が出すぎる等)になるなら、ゲーム側のバランスが悪い**、と機械的に判断できます。


このボット、あえて**“賢く”作られていない**のが効いています。最適プレイを目指して作り込むと、今度はボット側のバグや癖がそのまま数字に化けて、「ゲームのバランスを見ているのか、ボットのバグを見ているのか」が分からなくなる。10行程度の素直な作りで全部人が読めること自体が、この計測を信じてよい根拠です(“もっと賢いボットなら楽勝”は「上手い人には簡単すぎる」の検出で、別途測ればいい話)。

計測側にも仕掛けがあります。退職や納品は**ログ文字列ではなく状態の差分**で数えています。
 
 
```js

// 退職の検出:1日進める前後で社員IDの集合を比べる(消えたID=退職)

const before = new Set(G().staff.map(s => s.id));

const r = E.advanceDay();

const after = new Set(G().staff.map(s => s.id));

for (const id of before) if (!after.has(id)) quits++;

```
ログの文言は開発中ずっと変わり続けるので、文字列を当てにすると計測がすぐ壊れます。状態の差分なら、**AIが本体を何度作り直しても、ものさしは折れません**

実行するとこうなります。
 
```text
$ node sim/headless.js 10        # 10年 × 5シード
=== バランス計測 (10年 × 5シード) ===
[シード 1 の月次推移]
  期       資金     社員(E/S/A)  技  Lv  ストレス HP 評判 案件  称号
  Y1M6     120万    3 (2/1/0)   ...
[全シードのサマリ]
  seed  最終資金  ピーク資金  最低資金  社員 退職 赤字月  称号
  ...
[平均] 最終資金 …  退職 …人/10年
```
乱数がシード固定なので、**パラメータをいじる前後で同じ乱数列=同じ展開**が再現され、変更の差分だけが数字に出ます。「初任給を1万円上げたら10年で退職が何人減るか」が一発で読める。AIが自分で立てた「3年目に黒字転換」という目標も、この土台があるから感覚でなく検算できる注文になっていたわけです。**任せる範囲を広げるほど、こういう“数字で見張る足場”が効いてくる**――これが、後から眺めて一番なるほどと思った点でした。

## 技術補足:ブラウザ専用JSを本体無改造でNodeテストする`vm`ハーネス(コピペ可)


上の計測も39本のテストも、対象は`window`/`localStorage`/`Math.random`前提のブラウザコードです。テストのために書き換え始めると本体が汚れる。そこで**AIが採ったのは、本体には一切手を入れず**、Node標準の`vm`で「偽のwindow」を作ってゲームコードを流し込む方法でした。題材を問わず使える手なので、コード付きで置いておきます。

ハーネス本体は以下がすべてです。実ファイルにはこの下に自前assert(`eq`/`ok`/`done`)が十数行続くだけで、テストフレームワークは使っていません。
 
```js
// test/harness.js
'use strict';
const fs = require('fs'), path = require('path'), vm = require('vm');

function mulberry32(a){return function(){a|=0;a=(a+0x6D2B79F5)|0;
  let t=Math.imul(a^(a>>>15),1|a);t=(t+Math.imul(t^(t>>>7),61|t))^t;
  return((t^(t>>>14))>>>0)/4294967296;};}

function loadGame(files, seed) {
  const rand = mulberry32(seed == null ? 1 : seed);
  const store = {};
  const g = {
    Math: Object.assign(Object.create(Math), { random: rand }), // ←ポイント1
    Date, JSON, Object, Array, Set, Map, Number, String, Boolean,
    parseInt, parseFloat, isNaN, console,
    performance: { now: () => 0 },                              // ←ポイント2
    localStorage: {                                             // ←ポイント3
      getItem: k => (k in store ? store[k] : null),
      setItem: (k, v) => { store[k] = String(v); },
      removeItem: k => { delete store[k]; },
    },
  };
  g.window = g; g.global = g; g.globalThis = g;                 // ←ポイント4
  const ctx = vm.createContext(g);
  const base = path.join(__dirname, '..', 'js');
  for (const f of files)
    vm.runInContext(fs.readFileSync(path.join(base, f), 'utf8'), ctx, { filename: f });
  return { SES: g.SES, store };
}
```
ポイントは4つ。

1. **`Object.create(Math)`で複製してから`random`だけ差し替える**――本物の`Math`を汚さずに乱数をシード固定にできる(前節の再現性はここ)
2. **`performance.now`を`() => 0`に固定**――経過時間に依存するロジック(アニメ・クールダウン等)が決定的になる
3. **`localStorage`は連想配列のシムで十分**――セーブ/ロードのテストが普通に書ける
4. **`g.window = g`(自分自身を参照させる)**――`(function(global){…})(window)`型のコードがそのまま動く

テストはこう書けます。
```js
// test/smoke.test.js
const { loadGame, ok, done } = require('./harness');
const { SES } = loadGame(['config.js', 'state.js', 'economy.js'], 1);
ok(!!SES.economy, 'economy loaded');
done();   // 1ファイル=1プロセスで実行
```
この手はビルド工程のない既存のWebアプリ(jQuery時代の画面ロジックなど)にも同じように使えます。注意点として、Node公式が明記している通り**`vm`はセキュリティ目的のサンドボックスではありません**。信頼できないコードの隔離には使えませんが、「自分のコードを別コンテキストで実行する」用途には十分です。

## まとめ


  • **任せ方**:効いたのは“最初に方向性と制約を渡した”ことだけ。「要件定義もそちらで/自分はレビューだけ」と預けたら、AIは自分でバグを見つけ、テストを足し、バランス目標まで立てて調整していた。人間に残ったのは“何を作るか”と“できたものを見ること”
  • **ガードレール**:AIは数値を大味にする。が、シード固定+合理ボットの決定的シミュで“面白い数字か”を機械的に測る仕組みを、**AI自身が用意して自分を検算していた**
  • **土台**:計測もテストも`vm`+偽windowで本体無改造。状態の差分で数え、ものさしを折れなくする(この足場はそのままコピペで流用可)

丸投げで一番驚いたのは、派手なプロンプト術ではなく、**言われなくてもAIが自分の出力を数字で検算する足場を自分で組んでいた**ことでした。人間の仕事は、“何を作りたいか”を決めて、出てきたものに反応することへ移っていく。そう実感した一件です。

題材にしたゲームはこちら → [ゲームURL]。なぜ自社の仕事を題材にしたのか、という“作った側の話”はスタッフブログに書きました → [スタッフブログ記事URL]。弊社では、こういう「まず作って試す」を面白がれる方を歓迎しています → [採用ページURL]