ふくふくHukuhuku Inc.
EP.11LLM Gamedev 11分公開: 2026-05-26

ゲームバランス調整 ── LLM プレイヤーで自動プレイテスト

LLM にプレイヤー役をやらせてステージ難度・敵 AI のバランスを統計的に検証する手法。MCTS + LLM ハイブリッド、ペルソナ別シミュレーション、ヒートマップによる難所可視化。

#バランス調整#プレイテスト#シミュレーション#MCTS#LLM プレイヤー
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

EP.11 は プレイヤーで自動プレイテスト。人間プレイテストの代替ではなく、初期スクリーニング・回帰テスト・難所検出を 24 時間動かす自動化の話。完全情報ゲームは + ハイブリッドが強力。「インディー個人で 100 ステージ作ったけど全部手でテストできない」 を解決します。

1. なぜ LLM プレイヤーが要るか

  • 人間テスター調達コスト: フルプレイテストは 1 人 8 時間 × 3-5 人 × 週単位
  • コンテンツ量が膨大: 100 ステージのバランス、各ペルソナで 20 回試行 = 2000 セッション
  • アップデート時の回帰: 数値を 1 つ変えたら全ステージ再テスト
  • 早期検出: 開発の早い段階で 「絶対詰むポイント」 を弾きたい
  • 統計的議論: 「主観的に難しい」 ではなく「Aパターンのクリア率 35%、Bパターン 78%」 と数字で語る

2. ゲームを LLM が遊べる形に抽象化

ゲーム状態を JSON 化、行動を選択肢化
Python
from dataclasses import dataclass, asdictfrom typing import Literal
@dataclassclass GameState:    player_hp: int    player_max_hp: int    player_mp: int    enemies: list[dict]  # [{"name": str, "hp": int}]    items: list[str]    location: str    turn: int
@dataclassclass Action:    type: Literal["attack", "magic", "item", "flee"]    target: str | None = None    item_name: str | None = None    magic_name: str | None = None
def legal_actions(state: GameState) -> list[Action]:    actions = []    for enemy in state.enemies:        actions.append(Action("attack", target=enemy["name"]))    if state.player_mp >= 5:        actions.append(Action("magic", magic_name="fire"))    for item in state.items:        actions.append(Action("item", item_name=item))    actions.append(Action("flee"))    return actions

3. LLM に行動を選ばせる

ペルソナ付きで行動選択
Python
from anthropic import Anthropicimport jsonimport os
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
PERSONAS = {    "speedrunner": "最短でクリア、回復は最小限、危険でも前進。",    "collector": "全アイテム回収、隠し要素探索、戦闘は丁寧に。",    "cautious": "HP 半分以下なら必ず回復、危険な敵は逃げる。",    "berserker": "常に攻撃魔法、回復軽視、最大火力。",    "newbie": "あまり考えずランダム寄り、たまに操作ミスで弱い行動。",}
def llm_choose_action(state: GameState, persona: str) -> Action:    actions = legal_actions(state)    actions_text = "\n".join(        f"{i}: {asdict(a)}" for i, a in enumerate(actions)    )
    prompt = f"""あなたは {persona} 型のプレイヤー: {PERSONAS[persona]}
現在の状態:{json.dumps(asdict(state), ensure_ascii=False, indent=2)}
選択可能な行動:{actions_text}
理由を一言と、選んだ番号を JSON で返してください:{{"reason": "...", "choice": 0}}"""
    response = client.messages.create(        model="claude-haiku-4-5-20251001",        max_tokens=100,        messages=[{"role": "user", "content": prompt}],    )    text = response.content[0].text
    # JSON 抽出 (失敗時はランダム)    try:        decision = json.loads(text[text.index("{"):text.rindex("}")+1])        return actions[decision["choice"]]    except Exception:        import random        return random.choice(actions)

4. プレイテストを統計化

ペルソナ別 × ステージ別の試行
Python
import statisticsfrom collections import defaultdict
def simulate_run(stage_id: str, persona: str) -> dict:    """1 試行のシミュレーション (結果を辞書で返す)"""    state = load_stage(stage_id)  # ステージ初期状態    turn = 0    death_locations = []    while not is_clear(state) and not is_dead(state) and turn < 200:        action = llm_choose_action(state, persona)        state = apply_action(state, action)        if is_dead(state):            death_locations.append(state.location)        turn += 1    return {        "stage_id": stage_id,        "persona": persona,        "cleared": is_clear(state),        "turns": turn,        "remaining_hp": state.player_hp,        "death_locations": death_locations,    }
# 全ステージ × 全ペルソナで 20 試行results = []for stage_id in ["stage_01", "stage_02", "stage_03"]:    for persona in PERSONAS:        for trial in range(20):            results.append(simulate_run(stage_id, persona))
# 集計summary = defaultdict(lambda: {"clear": 0, "total": 0, "turns": []})for r in results:    key = (r["stage_id"], r["persona"])    summary[key]["total"] += 1    if r["cleared"]:        summary[key]["clear"] += 1        summary[key]["turns"].append(r["turns"])
print(f"{'Stage':12} {'Persona':12} {'ClearRate':10} {'AvgTurns':10}")for (sid, p), s in summary.items():    rate = s["clear"] / s["total"] * 100    avg = statistics.mean(s["turns"]) if s["turns"] else 0    print(f"{sid:12} {p:12} {rate:>8.1f}% {avg:>8.1f}")

5. 結果から読み取る (実例パターン)

症状想定原因改善案
全ペルソナで低クリア率難度過大敵 HP 下げる / 回復配置追加
speedrunner のみ低回復不可避な設計ショートカット用回復追加
cautious だけ高戦闘より回避が強い戦闘報酬を強化
平均ターン数が想定の 2 倍冗長 / 詰みポイントMAP を整理
特定 location で死亡集中トラップ強すぎ警告サイン追加
newbie が全滅難度上昇カーブ急チュートリアル / ゆとり追加

6. ヒートマップで難所可視化

matplotlib で死亡地点ヒートマップ
Python
import matplotlib.pyplot as pltimport numpy as npfrom collections import Counter
# 全試行から死亡地点を集計all_deaths = Counter()for r in results:    for loc in r["death_locations"]:        all_deaths[loc] += 1
# マップを 2D グリッドに見立ててヒートマップ# (実際はマップ座標 → グリッドインデックスの変換が要る)grid = np.zeros((20, 30))LOC_TO_XY = {    "entry": (0, 0), "corridor_a": (5, 10), "trap_room": (10, 15),    # ...}for loc, count in all_deaths.items():    if loc in LOC_TO_XY:        y, x = LOC_TO_XY[loc]        grid[y, x] = count
plt.figure(figsize=(10, 6))plt.imshow(grid, cmap='hot', interpolation='nearest')plt.colorbar(label='Death count')plt.title('Death locations heatmap')plt.show()

7. MCTS + LLM ハイブリッド

完全情報ゲーム (盤面ゲーム / カードゲーム) は が圧倒的に強い は評価関数やペルソナ説明に使い、探索本体は という分担。現代アルゴリズム図鑑 EP.14 (MCTS) との組合せで AlphaGo 的アプローチが個人開発でも手の届く距離に。

MCTS の評価関数を LLM に書かせる例 (擬似コード)
Python
# MCTS の rollout で 「完全ランダム」 でなく# 「LLM が選んだ妥当な手」 でロールアウトすると精度向上# (AlphaGo の policy network の代わりに LLM を使う)
def llm_rollout(state):    """MCTS の simulation 段階で LLM に方策を提供"""    while not is_terminal(state):        # 簡易 LLM (Haiku) で手を選ばせる        action = llm_choose_action(state, persona="optimal")        state = apply_action(state, action)    return evaluate(state)  # 勝敗 / スコア

8. CI/CD への組込み

  • GitHub Actions で PR ごとに自動プレイテスト: 全ステージ × 全ペルソナ × 10 試行 (ローカル LLM 推奨)
  • 閾値で fail: クリア率が <30% に下がる PR は merge ブロック
  • 実行時間目標: 30 分以内 (並列化 + ローカル LLM で実現)
  • 結果ダッシュボード: Grafana / Datadog にメトリクス送信

9. 注意点

LLM プレイヤーの限界

LLM は 「楽しさ」 を判定できない。クリア率が高くても「単調でつまらない」 のは検出できない。人間プレイテストの代わりではなく、初期スクリーニング / 回帰防止に使う ものと割り切る。最終判断は必ず人間で。

10. 次の話

EP.12 は プレイログ解析 ── BigQuery + LLM でユーザー行動を理解する ── リリース後のプレイデータを LLM で分析し、難所・離脱ポイント・予期せぬプレイスタイルを発見する話。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp