約1万行・6地域49ステージのゲームを、毎回手で全クリアして「壊れていない」と確認していられない。だから「動いている」を機械に証明させる。タイトルから戦闘・リザルトまでをロボットに通しプレイさせ、「コンソールエラー0・画像/音声の404ゼロ」を合格条件にする。そして配布は、ビルドが無いから静的ファイルを置くだけ。連載最終回は、自動検証とデプロイを実コードで開く。
▶ 第5回(AIアセット量産)までで部品は揃った。全体像は連載ハブ。完成品はここで遊べる(この公開も、本記事の「置くだけデプロイ」の成果だ)。
目次
最終回のゴール
(1) puppeteer でゲームをヘッドレス(画面なし)通しプレイし、(2) コンソールエラー0・404ゼロを完了条件に自動検証し、(3) 静的ファイルを置くだけで本番公開する。この3点を実装する。第1回の「無ビルド」と第3回の「DOM非依存」が、ここで一気に回収される。
Step 1. なぜ自動検証なのか
個人開発で一番怖いのは「どこかを直したら、別のどこかが静かに壊れる」回帰だ。手動テストは抜けるし、毎回は続かない。そこでロボットに毎回、同じ通しプレイをさせる。人間は「新しく作った所」に集中し、「前から動いていた所」は機械に見張らせる。これが小規模でも完成まで走り切るコツだ。
Step 2. puppeteer で通しプレイする
第1回の「file:// で動く」が、ここで効く。サーバーを立てずに index.html を直接開いて操作できる。鍵は「今どの画面か」をゲーム自身に聞けること。第1回で G.game.current に現在シーン名を持たせておいた。
// scripts/shot_game.js(抜粋)
const url = "file:///" + path.resolve("game/index.html").replace(/\\/g, "/");
const b = await puppeteer.launch({ headless: "new" });
const pg = await b.newPage();
const errs = [];
pg.on("console", m => { if (m.type() === "error") errs.push(m.text()); }); // ① コンソールエラーを集める
pg.on("pageerror", e => errs.push("PAGEERR " + e.message)); // 例外も集める
await pg.goto(url, { waitUntil: "networkidle0" });
// 「今どの画面か」をゲームに聞く(第1回の G.game.current)
const scene = () => pg.evaluate(() => (window.G && G.game && G.game.current) || null);
const waitScene = async (name, ms = 8000) => {
const t0 = Date.now();
while (Date.now() - t0 < ms) { if ((await scene()) === name) return true; await sleep(120); }
return false;
};
await pg.click("#btnNew"); // 「はじめる」
await waitScene("party"); // 編成画面になるまで待つ
const cards = await pg.$$(".hero-card");
await cards[0].click(); await cards[2].click(); await cards[4].click(); // 3人選ぶ
await pg.click("#go"); // 出発
waitScene("party") のように「目的の画面になるまで待つ」のが安定動作の肝。固定の sleep だけに頼ると、遅い環境で取りこぼす。ゲームが状態を公開しているから、ロボットは「画面が変わった」を正確に検知できる。
Step 3. 戦闘オートパイロット:DOM非依存設計が効く
戦闘は、コマンドメニューが出たら「こうげき → 倒せる敵をクリック」を繰り返すだけ。第3回で戦闘ロジックを画面から切り離したおかげで、挙動が安定し、こうした機械操作が決定的に楽になる。
// 戦闘が終わるまで自動で殴り続ける
let guard = 0;
while ((await scene()) === "battle" && guard++ < 120) {
const ready = await pg.evaluate(() => { // 自分の手番(コマンド表示)か?
const c = document.querySelector("#cmd");
return c && !c.classList.contains("hidden") && !!c.querySelector('.cbtn[data-c="attack"]');
});
if (ready) {
await pg.evaluate(() => document.querySelector('.cbtn[data-c="attack"]').click()); // こうげき
await pg.evaluate(() => { const t = document.querySelector(".unit.enemy.targetable"); if (t) t.click(); }); // 標的
}
await sleep(300);
}
const after = await scene(); // "result"(勝利)に遷移していれば成功
guard++ < 120 は無限ループの保険。万一どこかで詰まっても、120手番で打ち切ってレポートする(CIがハングしない)。「ちゃんと result に着いたか」で勝敗まで自動判定できる。

puppeteer の通しプレイが到達するリザルト画面。DOM非依存の戦闘コアだから、タイトル→戦闘→リザルトを自動で走らせ、「コンソールエラー0・画像/音声の404ゼロ」を機械的に確認できる。Step 4. 完了条件:「エラー0・404ゼロ」
通しプレイが終わったら、集めたエラーを数える。1件でもあれば不合格。これを CI(あるいは手元のワンコマンド)の合格条件にする。
// 画像・音声の読み込み失敗(404)も拾う
const failed = [];
pg.on("requestfailed", r => failed.push(r.url()));
pg.on("response", r => { if (r.status() === 404) failed.push("404 " + r.url()); });
// …通しプレイ後…
console.log("console errors:", errs.length, errs);
console.log("404 / failed :", failed.length, failed);
if (errs.length || failed.length) process.exit(1); // ← 1件でもあれば失敗で終了
console.log("✅ 通しプレイOK:エラー0・404ゼロ");
なぜ404を重視するか。第5回で作った立ち絵や背景のファイル名が、データの art パスと1文字でも食い違うと404になる。それをロボットが通しプレイの中で必ず踏んで検出してくれる。「エラー0・404ゼロで最後まで遊べた」を緑信号にすれば、リリース前の不安がほぼ消える。
🔎 設計が検証を可能にしている:これが効くのは偶然ではない。(1) 無ビルド(第1回)だから file:// で即起動できる。(2) シーン名を公開(第1回)しているからロボットが画面を追える。(3) 戦闘がDOM非依存(第3回)だから挙動が安定する。テストしやすい設計を最初に選んだ報酬が、ここで返ってくる。
Step 5. デプロイ:ビルドが無いから「置くだけ」
そして公開。第1回で webpack も npm run build も使わないと決めた。その恩恵が最後に効く。配布物は「静的ファイル一式」そのもの。ビルド成果物の生成も、依存解決の事故も無い。サーバーに js/ css/ assets/ index.html を置けば、それで公開完了だ。
実運用では、転送量と表示速度(LCP)のために本番だけ画像を webp 化している。大きな .png 参照を .webp に書き換えた最小サブセットを作り、zip で固めてアップ、サーバー側で展開する、という1コマンドにまとめてある。
# デプロイの流れ(要点)
# 1) 本番用サブセットを作る(js/css の .png 参照を .webp に書換え、画像をwebp化)
# 2) zip に固める → 3) SFTP で /tmp へ送る → 4) サーバーで unzip して上書き配置
$ python scripts/deploy_archipelago.py # 上記を一括実行(dry-run も可)
# 配置先はただの静的ディレクトリ:
# /game/archipelago/{index.html, js/, css/, assets_ai/, audio/}
ポイントは、ローカル開発(生の .png・file://)と本番(.webp・サーバー配信)を切り替えても、ゲームのコードは一切変わらないこと。最適化は配信時の置き換えに閉じている。デスクトップ配布版(Tauri)も、この同じ静的ファイルを実行ファイルに同梱するだけだ。
運用のコツ:検証を「回し続ける」ために
自動検証は、回し続けた月日が価値になる。運用で効いた工夫を3つ残しておく。
- 不安定な待ちを作らない … 固定
sleepは環境の速さで結果が揺れる(フレークの温床)。waitScene()のように「状態が変わるまで待つ+上限時間」の形に統一すると、遅いPCでも安定して同じ結果になる。 - 落ちたときの証拠を残す … 各シーンでスクショを保存しているのは、鑑賞用ではない。失敗した実行の「最後の画面」が残っていれば、エラー文だけでは分からない「どこまで進んだか」が一目で分かる。
- 実行の入口を1コマンドにする …
node scripts/shot_game.jsの一発で最後まで走るから、修正のたびに気軽に叩ける。手順が2つ以上あるテストは、忙しい日に必ずスキップされる。検証は、実行の摩擦が小さいほど文化として根づく。
通しプレイの守備範囲も正直に書いておく。この検証がカバーするのは「本編の一本道」(タイトル→編成→マップ→戦闘→リザルト)で、ショップ・図鑑・ステータスのようなサブ画面は素通りする。サブ画面の壊れ方は「開いた瞬間にエラーが出る」型が大半なので、対策も軽くていい。全シーンを G.game.show() で順に開いてエラー0を確かめるだけの「全画面スモーク」を1本足せば、通しプレイの死角はほぼ埋まる。深いテストを1本と、浅いテストを全面に。この組み合わせが、労力あたりの検出力で一番効率がいい。全部を深くテストしようとすると書く方が先に力尽きるし、全部を浅くすると肝心の戦闘の壊れを見逃す。守るべき中心がどこかを決めてから、テストの深さを配る。
この構成は GitHub Actions などのCIにもそのまま載る。push のたびに通しプレイが走り、緑のチェックが付く形だ。個人開発でそこまでやるかは規模次第だが、「載せられる形になっている」こと自体が、検証設計の健全さの証明になる。手元で1コマンドで走る物は、どこでも走る。逆に、手元で儀式が要る物は、自動化しようとした瞬間に儀式が全部露呈する。
ツール選びの補足も1つ。ここでは puppeteer を使ったが、同種の Playwright でも同じ構成が組める。効いているのは、「シーン名を公開する・状態変化を待つ・エラーと404を数える」というゲーム側の作りの方だ。そこさえ守れば、自動化ツールは好みで選んでいい。
連載のまとめ:「小さく完成させ切る」設計
全6回で、ブラウザで通しクリアできる JRPG を最初から組み上げた。貫いているのは一つの思想、「派手な技術より、完成させ切れる構成」だ。
- ① 無ビルド:
file://で動く。読込順が設計図。window.Gに集約。 - ② データ駆動:中身を純データに。コンテンツ追加は「データ追記だけ」。
- ③ DOM非依存の戦闘コア:ロジックは画面を知らず、イベント配列を返すだけ。
- ④ 状態を1つに:マップ・進行・セーブを
S1オブジェクトに集約。 - ⑤ AI量産フロー:生成→透過→正規化→配置を型に。絵が描けなくても揃う。
- ⑥ 自動検証+静的デプロイ:ロボットが通しプレイで証明。公開は置くだけ。
これらの工夫は互いに支え合っている。無ビルドだから検証が即起動でき、DOM非依存だから検証が安定し、データ駆動だから量産が回る。最初に「テストしやすく・増やしやすい」骨格を選べば、絵が描けなくても、1人でも、ゲームは最後まで完成する。
あとは、あなたの番だ。第1回の「最小で動く土台」を写経するところから、始めてみてほしい。4ファイルの土台が動いた瞬間が、あなたのゲームの起工式になる。
※ AI生成コンテンツの開示:本作の BGM・効果音・キャラクター画像・ボイスは、すべて生成AIの出力物です(BGM=ACE-Step 1.5〈MIT・出力の商用利用可を配布元モデルカードで確認済み〉 / ボイス=Irodori-TTS 系の合成設計音声・実在人物の複製ではありません / 効果音=数値合成 / 画像=画像生成AI〈各サービスの利用条件で出力の商用利用可を確認のうえ使用。透過処理は BiRefNet〈MIT〉を rembg 経由で使用〉)。第三者の著作物は同梱していません。© 2026 Cooliris(本作のコード・構成・データの権利は作者に帰属します)。
👉 ← 第5回:AIアセット量産 ・ 第1回から読み直す ・ 連載ハブ ・ ▶ 完成版を遊ぶ
