※注記
本記事のテキスト構成および文章のドラフト自体も、筆者の指示に基づきAIエージェント(Antigravity)が執筆・生成したものです。次世代のAI開発ワークフローの実践例(および個人的なポエム)として公開しています。
また本記事は、スマートフォン向けRPG『losthistoria』の実開発において、実際に構築・運用されている「C++製ヘッドレス高速シミュレータ」および「自動レベルデザイン検証パイプライン」の設計思想・アーキテクチャ・数理モデルを解説した技術記事です。
企画論(プランニング)にとどまらず、「なぜゲームクライアントにクリーンアーキテクチャとDIが必要なのか」「どうやってGUIからロジックを剥がして秒間数千戦回すのか」というソフトウェアエンジニアリングの実現手法に踏み込んで解説します。
シリーズ構成と本記事の位置づけ
- 第1弾(思想編): ゲームは「作品」ではない、「商品」である——開発の空中戦を終わらせ、チームとAIの視点を顧客に向けるための思考法
- 第2弾(空間・認知編): 「なんか面白くない」を撲滅する——ゲームの「楽しさ」と「面白さ」を数理モデル化し、AIとレベルデザインを爆速化する技術
- 第3弾(時間・アーキテクチャ編 / 本記事): ゲームの「プレイ時間」を数理モデル化してCIで回す技術——クリーンアーキテクチャとDIで実現する秒間1,000戦のヘッドレス検証基盤
はじめに:なぜ多くのゲームは「自動テスト・シミュレーション」で挫折するのか?
ゲーム開発の現場で「敵のパラメータ調整や戦闘バランスの検証を自動化したい」と考えたとき、ほぼ100%のチームが以下の巨大な技術的障壁に直面して挫折します。
「1回戦闘をテストするのに、UnityやCocosの画面を起動し、ロード画面を待ち、アニメーションを見守りながら30秒〜1分かかる。これでは100パターンの調整を試すだけで数時間〜数日溶けてしまう」
多くのゲームプロジェクトにおいて、ゲームロジック(HP計算、スキル発動、状態異常、AI思考ルーチン)は、ゲームエンジンの描画ノード(GameObject/Node)、画面更新ループ(Update/Render)、UI、そしてシングルトン(AudioManager, SaveData, OS時計)と固く癒着しています。
この状態のまま「AIでレベルデザインを自動化しよう」「モンテカルロシミュレーションで勝率を測ろう」としても不可能です。
結論から言えば、レベルデザインの論理化・自動化とは、ゲームクライアントの「クリーンアーキテクチャ化(脱GUI・脱シングルトン)」と「DI(依存性注入)」を徹底するソフトウェアエンジニアリングそのものです。
【本音の白状】筆者はもともとクリーンアーキテクチャやDIが大嫌いでした
本題に入る前に、まず白状しておかなければならないことがあります。
筆者は個人的に、クリーンアーキテクチャや過剰なDI(依存性注入)が大嫌いでした。
エンタープライズWeb開発などで持て囃されるこれらの手法を、スピードと直感的な試行錯誤が命であるゲーム開発に持ち込もうとすると、人間のプログラマーには以下の耐え難いペナルティがのしかかります。
-
コードの全体把握が極端に困難になる:
インターフェースや抽象化レイヤーが何重にも挟まり、IDEで定義元にジャンプしても中身のないヘッダやインターフェースに飛ばされ、「結局いま、実行時に具体的にどのコードが動いているのか」を追うだけで認知リソースが削られる。 -
変更範囲の見落とし・設計漏れが多発する:
たった1つのパラメータ追加や仕様変更のために、ドメインモデル、DTO、インターフェース、本番用アダプタ、テスト用アダプタ、DIコンテナ配線と、何層ものファイルを横断して修正しなければならない。人間が手作業でやると必ずどこかで見落としが発生する。 -
テスト工程まで進んで初めて気づく手戻り地獄:
コンパイルは通ったのに、いざテスト工程や実行時になって初めて「DIの配線が1箇所抜けていた」「テスト用アダプタの更新を忘れていた」と発覚し、泥沼のデバッグに時間を吸い取られる。
**「理屈はわかるが、人間が手作業でやるにはあまりに苦痛で、個人開発やインディーで手を出したら自滅する」**というのが、これまでの筆者の偽らざる本音でした。
なぜ今、クリーンアーキテクチャを採用したのか?——「AI君がやってくれるから」
しかし、AIエージェント(Coding Agent)とペアプログラミングをする時代になり、この力学は180度激変しました。
人間が最も苦痛を感じ、かつミスを起こしやすい
- 「インターフェースの切り出し」
- 「仕様変更に伴う全アダプタコードの追従修正」
- 「退屈なボイラープレートやモックの生成」
- 「DI配線の網羅性チェックとユニットテストの追加」
これらはすべて、AIが最も得意とする領域です。
人間が「ここの描画ロジックをインターフェースで分離して、ヘッドレスでも動くようにDI化して」「この新仕様に合わせてモックと本番アダプタの両方を直しておいて」と指示を出せば、AI君が一瞬で、しかも一切の愚痴を言わず、完璧な網羅性で追従コードを書き上げてくれます。
人間が負うべき認知負荷(何層ものコードを頭の中で保持し、漏れがないか怯えるストレス)をAIが肩代わりしてくれると分かったからこそ、「人間だけだったら絶対にやりたくなかったクリーンアーキテクチャとDI」を、いまや心置きなく、遠慮なく全面採用できるようになったのです。
そしてその結果として手に入ったのが、本記事の主役である**「秒間1,000戦のヘッドレス高速シミュレーション環境」**でした。
本記事では、
- 描画エンジンからゲームロジックを100%剥がし、秒間1,000戦回すためのクリーンアーキテクチャ設計
- 戦闘秒数から周回サイクル、ゲーム全体のクリア時間までを定義する「3つの時間階層の数理モデル」
- 実例コード(C++ CLI & Python並列実行)とCI(GitHub Actions)による自動品質ゲートの実装
を一切端折らずに解説します。
1. ヘッドレス爆速検証を実現するクリーンアーキテクチャ
画面を描画せず、OSの描画フレーム待ち(VSyncや60FPS制限)を完全バイパスし、CPUの演算限界速度で戦闘を回すためには、**Ports and Adapters(ヘキサゴナル・アーキテクチャ)**を採用します。
① コアドメインからグラフィック・サウンドAPIを完全追放する
-
PlayerUnitやEnemyUnitの内部でSprite::create()やPlaySound()を直接呼ぶことを固く禁じます。 - ダメージを受けたときは、描画オブジェクトを揺らすのではなく、
DamageEvent { targetId, damage, isCritical }という純粋なPOD(Plain Old Data)構造体をイベントキューへ発行するだけに留めます。
② 固定デルタタイム(Fixed Δt)による「時間の脱構築」
実ゲームプレイでは、1フレームあたり dt = 0.01666秒 (1/60s) の時間経過を実時間に合わせて待ちます。
しかしヘッドレスシミュレータでは、スリープ(Sleep)や描画同期待ちを一切挟みません。
// ヘッドレスシミュレータのメインループ
const float fixedDt = 0.01667f; // 60FPS相当の固定刻み
float battleTimer = 0.0f;
const float timeLimit = 120.0f; // 2分制限
while (battleTimer < timeLimit && !battleField.isTerminated()) {
// 描画・サウンド・入力待ちをスキップし、純粋なステート更新のみを実行
battleField.update(fixedDt);
battleTimer += fixedDt;
}
このループにはOSのウェイト処理が一切存在しないため、現代のマルチコアCPUであれば**120秒のフルバトル(7,200フレーム分の物理・当たり判定・AI思考計算)が、わずか 0.002秒〜0.005秒(秒間200〜500戦、並列化すれば秒間数千戦)**で完了します。
2. 依存性注入(DI)とデータ駆動設計
シミュレータを柔軟に回すための第2の鍵は、**「コード内にステータスやパーティ構成を1文字もハードコードしない」**ことです。
① 抽象インターフェースとモック注入
例えば、戦闘ユニット間の当たり判定やダメージ解決は、具体的なクラスではなく共通のインターフェース(例: HitUnit)を介して行います。
// 抽象インターフェース
class HitUnit {
public:
virtual ~HitUnit() = default;
virtual int getHp() const = 0;
virtual int getDefense() const = 0;
virtual void applyDamage(int rawDamage, DamageType type) = 0;
};
描画用の座標計算やアニメーション状態は、このインターフェースの外側(プレゼンテーション層)にのみ存在するため、シミュレータ側では最小限のメモリフットプリントでインスタンス化できます。
② パーティ編成・敵データの完全JSON駆動化
検証したいパーティ編成や敵のステータスは、すべて外部JSONから読み込み、実行時に動的に注入(DI)します。
// party_presets.json(検証用パーティ定義)
{
"presets": [
{
"id": "starter_lv1",
"description": "初期加入直後のLv1パーティ(無強化)",
"members": [
{ "job": "warrior", "level": 1, "main_weapon": { "id": "iron_sword", "refine": 0 } },
{ "job": "cleric", "level": 1, "main_weapon": { "id": "oak_staff", "refine": 0 } }
]
},
{
"id": "forest_boss_lv5_crafted",
"description": "森ボス突破推奨:Lv5+鍛冶屋クラフト装備+3",
"members": [
{ "job": "warrior", "level": 5, "main_weapon": { "id": "broad_sword", "refine": 3 }, "armor": { "id": "iron_mail", "refine": 3 } },
{ "job": "cleric", "level": 5, "main_weapon": { "id": "silver_mace", "refine": 3 }, "armor": { "id": "silk_robe", "refine": 3 } }
]
}
]
}
このように設計しておくことで、C++のコードを再コンパイルすることなく、コマンドライン引数ひとつで**「無強化パーティで突撃した場合」と「推奨装備を揃えて突撃した場合」の挙動差分**を瞬時に計測できるようになります。
# コマンド一発で任意のパーティとステージをシミュレーション
./build/bin/losthistoria_stage_sim --stage 101 --party-preset forest_boss_lv5_crafted
3. 「プレイ時間の適正さ」を定義する3つの階層数理モデル
クリーンアーキテクチャとDIによって「超高速に戦闘を回せる実行環境」が整いました。
次に必要なのは、「得られたシミュレーション結果(秒数や勝率)が、製品として適正かどうか」を判定する数理モデルです。
プレイ時間とは単一のストップウォッチではなく、以下の3つの異なる時間階層の連立方程式として解く必要があります。
第1層:ミクロ時間(1戦闘のインゲーム秒数 T_battle)
1戦闘の時間は、**ステージの「設計意図(Intent)」に応じた適正バンド(Target Band)**に収まっているかで評価します。
| ステージ分類 | プレイヤーの目的 | 適正時間バンド | 不良品判定の基準 |
|---|---|---|---|
| 資材・金策周回 | 早く回して素材を集めたい | 10秒 〜 20秒 |
> 30秒: テンポが最悪、周回作業が苦痛 < 5秒: ロード時間の方が長く感じる虚無 |
| 道中一般ステージ | パーティのシナジーを試す | 25秒 〜 45秒 |
> 60秒: 雑魚が硬すぎる(スポンジエネミー) < 15秒: スキルを撃つ前に終わる |
| ボス戦(Spike/Gate) | 死力を尽くして壁を越える |
60秒 〜 90秒 (制限120秒) |
> 90秒: 泥沼化 / 120秒タイムアウトの壁 < 30秒: ボスの威厳がなく達成感が薄い |
数理評価関数(戦闘時間ペナルティ)
目標とする最小時間 T_min と最大時間 T_max に対し、ペナルティ関数を定義します。
\mathcal{L}_{\mathrm{battle\_time}} = \max(0, T_{\min} - T_{\mathrm{battle}}) + \max(0, T_{\mathrm{battle}} - T_{\max})
「時間」と「薄氷勝利度(Clutch Ratio)」の結合
単に戦闘時間が60秒であれば良いわけではありません。
- パターンA(退屈): 敵が異常に硬く、味方はノーダメージのまま平坦に60秒殴り続けて勝った。
- パターンB(激闘): 30秒地点で敵の全体攻撃を受け前衛が倒れ、残りHP10%のギリギリの死闘を制して60秒で勝った。
第2弾で定義した**薄氷勝利度(Clutch Ratio)と掛け合わせることで、「時間はたっぷりかかるが単に敵が硬いだけのクソゲー(スポンジエネミー)」**をAIが自動検知してリジェクトします。
\mathcal{L}_{\mathrm{clutch}} = |\mathrm{Median}(HP_{\mathrm{final}}) - 0.15|
第2層:メソ時間(育成・突破サイクル時間 T_TTB)
メソ時間の適正さとは、**「ボスの壁にぶつかって全滅してから、必要な素材を集めて装備を作り、リベンジを果たすまでに現実時間で何分/何時間かかるか」**の指標です。これを Time-to-Breakthrough(T_TTB) と定義します。
数理モデル(突破必要時間の計算式)
ボス突破に必要な素材数を M_req、遠征1回あたりの期待素材獲得量を E[drop]、1遠征の所要時間を T_exp とすると:
T_{\mathrm{TTB}} = \left\lceil \frac{M_{\mathrm{req}}}{E[\mathrm{drop}]} \right\rceil \times T_{\mathrm{exp}} + T_{\mathrm{craft\_UI}}
適正さの判定基準
| 進行フェーズ | 目標 T_TTB(現実時間) | 設計意図と判定 |
|---|---|---|
| 第1エリア(序盤:森) |
1時間 〜 2時間 (実プレイ30分+遠征2〜4回) |
初日離脱(Day 1 Churn)の防止。 「インストールした初日に最初のボスを倒す快感」を保証する。 |
| 中盤エリア(砂海・氷原) |
半日 〜 1日 (放置1〜2回) |
生活習慣化(Habit Loop)の形成。 「朝に遠征を出し、夜帰宅後に強化して突破する」日常リズム。 |
| 終盤エリア(城・魔王) | 2日 〜 3日 |
特化ビルドの試行錯誤。 属性耐性やスキル特化装備の厳選を楽しむ期間。 |
⚠️ 自動テストで検知すべき2大欠陥:
- 理不尽な課金壁 / 放置地獄(T_TTB > 上限閾値): 序盤の森なのに素材集めに3日かかる → プレイヤーが退屈して離脱する。
- アウトゲームの形骸化(T_TTB ≒ 0): 道中ドロップだけでボスが一発突破できる → 遠征・牧場・鍛冶屋を使う必然性が消滅する。
第3層:マクロ時間(ゴールデンロード総時間 T_total)
ゲーム開始から魔王討伐までの総プレイ時間(ゴールデンロード)は、製品スペックとして以下のように定義されます。
- 広告ブーストあり: 実プレイ時間 約10時間
- 広告ブーストなし: 実プレイ時間 約20時間
全11地域のボス突破にかかる時間を積み上げた累積曲線に対し、以下の2つのメトリクスで品質を監視します。
① 停滞係数(Stall Index: I_stall)
特定の1地域が、ゲーム全体のプレイ時間に占める割合を測ります。
I_{\mathrm{stall}}(k) = \frac{T_{\mathrm{area}}^{(k)}}{T_{\mathrm{total}}}
- 適正値: 各エリア 5% 〜 15%(終盤に向かって緩やかに増加する美しい曲線)
-
異常値: 特定の地域(例: 第4エリア)だけで
I_stall > 25%に達している場合、そこが**「レベルデザイン上の局所的な詰まり(設計ミス)」**であるとAIが自動アラートを出します。
② 新鮮さの半減期(Novelty Half-life: ΔT_novelty)
プレイヤーが**「新しい刺激(新スキル、新武器種、新施設、新敵グラフィック)」**に出会うまでの時間間隔です。
\Delta T_{\mathrm{novelty}} \le \begin{cases} 15\text{分} & \text{(序盤:0〜2時間)} \\ 45\text{分} & \text{(中盤:2〜6時間)} \\ 90\text{分} & \text{(終盤:6〜20時間)} \end{cases}
新要素の投入間隔がこの時間を超えて空いてしまうと、同じ画面・同じパターンの繰り返しとなり、人間の脳は「飽き(作業感)」を感じ始めます。
4. 実装詳細:C++ CLI & Python並列実行の実例
実際に『losthistoria』で稼働している実装コードをもとに、シミュレータの実装構造を解説します。
① C++製スタンドアロンCLI(stage_sim_main.cpp)
描画エンジン(Cocos2d-x)の AppDelegate や Scene を一切介さず、標準の main() 関数から直接バトルフィールドを駆動するCLIバイナリです。
// Source/App/stage_sim_main.cpp(抜粋)
int main(int argc, char* argv[]) {
int stageId = parseStageId(argc, argv);
std::string partyPreset = parsePartyPreset(argc, argv); // 例: "forest_boss_lv5_crafted"
// 1. JSONからステージデータ・敵配置を読み込み
StageData stageData = StageDataLoader::loadStage(stageId);
// 2. JSONからパーティ編成プリセットを読み込み
PartyData party = PartyPresetLoader::loadPreset("Content/data/party_presets.json", partyPreset);
// 3. ヘッドレス・バトルフィールドの構築(GUI非依存)
HeadlessBattleField battleField(stageData, party);
const float dt = 0.01667f; // 60FPS固定
float elapsedTime = 0.0f;
// 4. 超高速シミュレーションループ
while (!battleField.isFinished() && elapsedTime < 120.0f) {
battleField.update(dt);
elapsedTime += dt;
}
// 5. 結果をJSON形式で標準出力へ出力(Python側でパース)
std::cout << battleField.dumpResultJson(elapsedTime) << std::endl;
return 0;
}
② Pythonラッパーと並列シミュレーション(tools/sim_stage.py)
C++ CLIバイナリをサブプロセスとして並列実行し、モンテカルロ試行や各種パーティプリセットの網羅検証を行うPythonスクリプトです。
# tools/sim_stage.py(抜粋)
import subprocess, json, sys
def run_simulation(stage_id, preset):
cmd = [
"./build/bin/losthistoria_stage_sim",
"--stage", str(stage_id),
"--party-preset", preset
]
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
return json.loads(res.stdout)
# 森ボス(Stage 101)に対するスパイク設計の検証
result_unprepared = run_simulation(101, "starter_lv1")
result_prepared = run_simulation(101, "forest_boss_lv5_crafted")
print(f"【無強化】勝敗: {result_unprepared['winner']}, 討伐/全滅時間: {result_unprepared['time']}s")
print(f"【Lv5装備】勝敗: {result_prepared['winner']}, 討伐/全滅時間: {result_prepared['time']}s")
実際の実行ログ(実測値)
本プロジェクトで実際に計測された出力ログがこちらです。
$ python3 tools/sim_stage.py 101 --party-preset starter_lv1
>>> [結果] 敗北 (DEFEAT) / 全滅時間: 31.2秒 / ボス残HP: 78.4%
--> 判定: 門番(壁)として正常に機能。無強化では絶対に突破できない。
$ python3 tools/sim_stage.py 101 --party-preset forest_boss_lv5_crafted
>>> [結果] 勝利 (VICTORY) / 撃破時間: 61.1秒 / 味方残HP: 24.1%
--> 判定: 目標適正バンド(60〜90秒)に完全合致。極めて良好なボス体験!
無強化の初心者パーティで挑むと31秒で返り討ちに遭い、ギルド遠征で素材を集めて鍛冶屋でLv5装備を揃えて挑むと61秒の激闘の末に競り勝つというレベルデザインが、わずか0.01秒のCLI実行で完全に数値として実証されています。
③ 【評価テスト】実アプリ(E2E)で泥臭く調整した実戦値と、シミュレータの整合性検証
ここで、エンジニアなら当然ひとつの大きな疑問が浮かぶはずです。
「シミュレータ上で61秒と言っても、それはシミュレータ用の簡略化されたおもちゃの計算なんじゃないの? 実際のゲーム画面(E2E)で動かしたら全然違う結果になるんじゃないか?」
まさにこの妥当性を検証するため、本プロジェクトでは**「実アプリでの泥臭いE2E調整結果」と「ヘッドレスシミュレータの計算結果」の突き合わせ(バリデーション)**を実施しました。
実はこのヘッドレスシミュレータを構築する前の段階で、筆者とAIはGUI実機(Cocos2d-x画面、4倍速再生、Bot操作、E2Eテストスイート)を何十戦・何百戦と回し、実際の死亡演出や味方のクリスタル化、手触りを確かめながら「森ボス(Stage 101)の黄金バランス」を泥臭く手動調整していました。
その時に記録された実アプリE2Eテストの実戦データがこちらです。

▲ 実機GUI(Cocos2d-x最新版)で手動・Bot調整を行っていた森ボス「ゴブリンロード」戦の激闘シーン(5.0秒間)。AUTOモードでパズルを連鎖させながら立ち向かい、スキル「魔力奔流」の発動とともにゴブリンロードと大激突する。この泥臭い実戦バランスが、ヘッドレスシミュレータ(0.01秒で走るCLI)の計算結果と寸分違わず一致した。
| 条件(実アプリE2E) | 実戦結果 | 討伐 / 全滅タイム | 生存人数 / 状況 |
|---|---|---|---|
| 無強化(+0) | 0勝 2敗(敗北) | 30秒前後で崩壊 | 全滅(突破不可の壁) |
| 防具のみ+3 | 2勝 0敗(勝利) | 82 〜 93秒 | 1〜2人生存(辛勝) |
| 武器+防具+3(推奨準備) | 2勝 0敗(勝利) | 56 〜 59秒 | 3人生存(前衛1人死亡) |
そして、この「泥臭い実機テストで数週間かけて詰めた数値」に対して、今回完成した**ヘッドレスシミュレータ(0.01秒で走るCLI)**を実行させた結果が以下です。
【ヘッドレスシミュレータの実行結果】
- 無強化 (starter_lv1): 31.2秒で全滅 (ボス残HP 78.4%) --> 突破不可
- 推奨Lv5装備 (forest_boss_lv5): 61.1秒で撃破 (味方残HP 24.1%) --> 3人生存・前衛死亡
驚くべき整合性(実機 56〜59秒 vs シミュレータ 61.1秒)
ご覧の通り、実アプリの画面上で泥臭く計測した「56〜59秒で突破、前衛が落ちて3人生存」という激闘の挙動と、ヘッドレスシミュレータが0.01秒で弾き出した「61.1秒で撃破、前衛死亡で後衛3人生存」という結果が、わずか数秒の誤差で完全に一致しました。
なぜここまで正確に一致するのか?
それは、「シミュレータ用の別計算ロジック(近似式)」を後から作ったのではなく、実アプリで動いている本番のC++戦闘コード(PlayerUnit, EnemyUnit, DamageCalculator)そのものを、クリーンアーキテクチャによってGUIから完全に剥がして実行しているからです。
実機で何時間もかけてテストした泥臭い結果が、シミュレータでも寸分違わず再現される。
この確証が得られたことで、開発チームは**「以降の全エリア・全ボスのレベルデザイン調整において、実機を手動で回す必要は一切なく、シミュレータとCIの数字だけを信用して爆速で進めて良い」**という絶対的な確信(テスト自動化の恩恵)を手に入れることができました。
5. CI/CDパイプライン統合:レベルデザインの回帰テスト
アーキテクチャが整い、シミュレータが秒速で動くようになれば、ソフトウェアテストにおける「ユニットテスト」と全く同じように、レベルデザインをCI(GitHub Actions)の自動テストへ組み込むことができます。
# .github/workflows/level_design_regression.yml
name: Level Design Regression Check
on: [push, pull_request]
jobs:
simulate-progression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Headless CLI
run: |
cmake -B build -DBUILD_HEADLESS_SIM=ON
cmake --build build --target losthistoria_stage_sim -j4
- name: Run Playtime & Balance Verification
run: |
python3 tools/verify_playtime_contracts.py
自動テスト(契約テスト)の検知ロジック
# tools/verify_playtime_contracts.py
def test_forest_boss_balance():
# 1. 無強化パーティで勝ててしまったらバグ(壁になっていない)
r_novice = run_simulation(101, "starter_lv1")
assert r_novice["winner"] == "enemy", "FAIL: 無強化でボスが突破できてしまう(難易度崩壊)"
# 2. 推奨装備パーティで倒せなかったらバグ(理不尽な詰み)
r_expert = run_simulation(101, "forest_boss_lv5_crafted")
assert r_expert["winner"] == "player", "FAIL: 推奨装備を揃えても勝てない(理不尽な壁)"
# 3. 討伐時間が適正バンド(60〜90秒)を外れていたらリジェクト
assert 60.0 <= r_expert["time"] <= 90.0, f"FAIL: ボス戦の時間が不適切 ({r_expert['time']}秒)"
エンジニアやプランナーが敵の攻撃力や装備の強化倍率のJSONを変更してPR(Pull Request)を出した際、もしその変更によって**「ボス戦が110秒に伸びてタイムアウト寸前になった」あるいは「序盤のボスが無強化で突破できるようになってしまった」**場合、CIが即座に赤色になり、マージがブロックされます。
❌ FAILED: test_forest_boss_balance
AssertionError: FAIL: ボス戦の時間が不適切 (104.2秒 > 最大許容90.0秒)
敵ID: 7 の防御力上方修正により、プレイヤーの与ダメージが低下しすぎて泥沼化しています。
6. まとめ:エンジニアがゲームデザイナーに提供できる最大の武器
ゲーム開発における「難易度調整」「テンポ感」「プレイ時間」の議論は、往々にして感覚論・精神論に陥りがちです。
- 「なんかちょっと敵が硬い気がする」
- 「もうちょっと周回を軽くした方がいいんじゃないか」
- 「ボスがあっさり倒せすぎて達成感がない」
しかし、これらの違和感を修正するために、毎回ビルドを作成し、スマホ端末に転送し、人間が手動で1時間プレイして確認していたら、どんなに潤沢な開発予算があっても時間が足りません。
【従来の開発】
変更 → 手動ビルド → 手動テストプレイ(30分) → 「なんか違う」 → 再調整 (1日2〜3回が限界)
【本アーキテクチャ】
変更 → git commit → 秒間1,000戦シミュレーション(10秒) → エビデンスカード出力 → 確信を持ってマージ
エンジニアがゲームデザイナー(プランナー)に提供できる最大の貢献は、面白いパラメータを直接決めることではありません。
**「ゲームデザイナーが1秒間に1,000回、自分の仮説をテストできる『高速な検証基盤』を作ること」**です。
そしてその検証基盤の土台となるのは、最新のAIモデルではなく、徹底して描画とロジックを分離するクリーンアーキテクチャと、依存性を自在に差し替えられるDIという、ソフトウェアエンジニアリングの基本原則なのです。
付録・関連記事
- 第1弾:ゲームは「作品」ではない、「商品」である——開発の空中戦を終わらせ、チームとAIの視点を顧客に向けるための思考法
- 第2弾:「なんか面白くない」を撲滅する——ゲームの「楽しさ」と「面白さ」を数理モデル化し、AIとレベルデザインを爆速化する技術
- 本記事の検証で用いたリポジトリ構成・技術スタック:C++20, RapidJSON, Python 3.11, doctest, GitHub Actions