🌍

構造主観力学で作る「生きている」NPC:原始村落シミュレーション

に公開

はじめに

「ゲームのNPCをもっと生き生きとさせたい」
「単純なルールから複雑な社会が生まれる様子を見てみたい」

そんな想いから、**構造主観力学(Structural Subjectivity Dynamics: SSD)**という理論フレームワークを使って、原始的な村落のシミュレーションを作りました。

本記事では、このシミュレーションがどのように動作し、なぜ「生きている」と感じられるNPCが生まれるのかを解説します。

この記事で分かること

  • 構造主観力学(SSD)の基本概念
  • SSDをNPC AIに実装する方法
  • シミュレーション結果と創発的な社会行動
  • コードの実装詳細とカスタマイズ方法

対象読者

  • ゲームAIに興味がある方
  • シミュレーション開発に取り組んでいる方
  • 理論を実装に落とし込む過程を知りたい方

原始村落シミュレーション
https://github.com/HermannDegner/primitive-NPC-AI

構造主観力学(SSD)とは?

核心概念:安定と変化の力学

SSDは、世界のあらゆる事象を「安定(整合)」と「変化(跳躍)」の動的バランスとして捉える理論です。

3つの基本要素

1. 意味圧(Meaning Pressure)

構造に作用するあらゆるエネルギーや影響。

  • NPCにとっての「空腹」「疲労」「脅威」など

2. 整合(Alignment)

構造が意味圧に対して安定を保とうとするプロセス。

  • 食べ物を探す、休息する、仲間と協力する

3. 跳躍(Leap)

整合の限界を超えた時に発生する非連続的な変化。

  • このシミュレーションでは「死」として表現

SSDをNPCに実装する

コアシステムの数理モデル

NPCは以下の内部状態を持ちます:

class NPCWithTerritory:
    def __init__(self, name, preset, env, roster_ref, start_pos):
        # 生理的状態
        self.hunger = 50.0      # 空腹度
        self.fatigue = 30.0     # 疲労度
        self.injury = 0.0       # 負傷度
        
        # SSD核心パラメータ
        self.kappa = defaultdict(lambda: 0.1)  # 整合慣性
        self.E = 0.0                           # 未処理圧(熱)
        self.T = 0.3                           # 探索温度
        
        # 社会的パラメータ
        self.rel = defaultdict(float)          # 他者との関係性
        self.empathy = preset["empathy"]       # 共感性
        self.risk_tolerance = preset["risk_tolerance"]

整合慣性(κ: kappa)

「慣れ」や「学習」を表現する重要な概念

def update_kappa(self, action_type, success, reward):
    """行動の成功体験を蓄積する"""
    kappa = self.kappa[action_type]
    
    if success:
        # 成功すればκが増加 → 次回その行動が取りやすくなる
        work = self.eta * reward
    else:
        # 失敗すればκが減少
        work = -self.rho * (kappa ** 2)
    
    # 忘却も考慮
    self.kappa[action_type] = max(
        self.kappa_min, 
        kappa + work - (self.lambda_forget * (kappa - self.kappa_min))
    )

κの効果

  • κが高い行動 → その行動を取りやすくなる(習慣化)
  • κが低い行動 → その行動を避ける傾向

未処理圧(E: Heat)

処理しきれないストレスの蓄積

def update_heat(self, meaning_pressure, processed_amount):
    """意味圧が処理しきれないと熱として蓄積"""
    unprocessed = max(0, meaning_pressure - processed_amount)
    self.E = max(0, 
        self.E + self.alpha * unprocessed - self.beta_E * self.E
    )

Eの効果

  • Eが高い → 探索温度Tが上昇 → ランダムな行動が増える
  • Eが閾値超過 → 「跳躍」(このシミュレーションでは死の確率上昇)

シミュレーションの進化

レベル1:基本的な生存

# 基本的な行動選択
if self.hunger > 70:
    self.forage()  # 採餌
elif self.fatigue > 80:
    self.rest()    # 休息
else:
    self.patrol()  # パトロール

レベル2:昼夜サイクルの導入

class DayNightCycle:
    def get_sleep_pressure(self):
        """夜になると睡眠圧が上昇"""
        time = self.get_time_of_day()
        if self.night_start <= time < self.night_end:
            return 1.0
        return 0.1

効果

  • NPCが夜に寝るようになる
  • 睡眠中に記憶が定着(κが強化される)

レベル3:縄張り意識

class Territory:
    def __init__(self, owner, center, radius):
        self.owner = owner
        self.center = center  # 縄張りの中心
        self.radius = radius  # 縄張りの範囲
        self.memory = {
            "food_found": 0,
            "helped_here": 0,
            "rested": 0
        }

縄張りの効果

  • ホームボーナス:縄張り内での睡眠は回復量1.5倍
  • 侵入者検知:他のNPCが侵入すると警戒・追跡
  • 愛着形成:成功体験が積まれた場所に戻りたくなる

レベル4:集団狩猟と社会行動

戦闘力の動的計算

def combat_power(self, group_bonus=0.0):
    """NPCの戦闘力を計算"""
    k = self.kappa.get("hunt", 0.1)
    xp_term = 1.0 + 0.15 * np.tanh(self.combat_xp / 30.0)
    fatigue_pen = 1.0 - 0.35 * (self.fatigue / 120.0)
    injury_pen = 1.0 - 0.50 * (self.injury / 120.0)
    inertia_boost = 1.0 + 0.25 * self.align_inertia
    
    return (
        self.combat_power_base * 
        (1.0 + 0.25 * k) * 
        xp_term * 
        fatigue_pen * 
        injury_pen * 
        inertia_boost
    ) * (1.0 + group_bonus)

戦闘力に影響する要因

  • 狩りの整合慣性(κ)
  • 戦闘経験値
  • 疲労度・負傷度
  • 整合慣性(align_inertia)
  • グループボーナス

集団狩猟のメカニズム

def emit_rally_for_hunt(self, t, hunt_node=None, min_k=2, ttl=10):
    """狩りの呼びかけ"""
    self.rally_state = {
        "leader": self.name,
        "ttl": ttl,           # 有効時間
        "node": hunt_node,    # 目標地点
        "min_k": min_k,       # 最小人数
        "kind": "hunt"
    }

集団狩猟の流れ

  1. リーダーが呼びかけ(emit_rally_for_hunt
  2. 仲間が呼びかけに応答(consider_join_rally
  3. 十分な人数が集まったら実行(attempt_rally_group_hunt
  4. 成功すれば全員で報酬を分配

捕食者への対応

def emit_rally_predator(self, t, node, min_k=3, ttl=12):
    """捕食者討伐の呼びかけ"""
    self.rally_state = {
        "leader": self.name,
        "ttl": ttl,
        "node": node,
        "min_k": min_k,
        "kind": "predator"  # 種類を区別
    }

捕食者との戦闘

  • 捕食者が特定エリアに留まると(camping)検知
  • NPCが自発的に討伐隊を結成
  • グループサイズと戦闘力で勝率が変動
  • 成功すれば大量の食料と経験値

優先度ベースの意思決定

階層的な行動選択

def step(self, t, predator=None):
    # L1: 生命維持のガードレール
    if self.hunger >= 95:
        self.emergency_forage()
        return
    if self.injury >= 85 or self.should_sleep():
        self.move_to_home_and_sleep()
        return
    
    # L2: 協調行動
    if self.state == "rallying":
        self.follow_rally_leader()
        return
    if predator and predator.is_camping():
        self.emit_rally_predator()
    if self.rally_state["leader"] == self.name:
        self.attempt_group_action()
        return
    
    # L3: 個人の欲求
    if self.hunger > self.TH_H:
        self.forage()
        return
    
    # L4: 社会行動
    self.social_gravity_move()
    self.copresence_tick()
    self.apply_triadic_closure()

優先度の意味

  1. L1:生命維持 - 何よりも優先
  2. L2:協力行動 - 個人より集団の利益
  3. L3:個人欲求 - 通常の生活行動
  4. L4:社会行動 - 余裕があれば

創発的な社会行動

関係性の自然な発展

三者関係の形成

def apply_triadic_closure(self):
    """友達の友達は友達"""
    for a in self.nearby_allies():
        for b in self.nearby_allies():
            if self.rel[a.name] > 0.2 and self.rel[b.name] > 0.2:
                # AとBも仲良くなる
                inc = 0.03 * min(self.rel[a.name], self.rel[b.name])
                a.rel[b.name] += inc
                b.rel[a.name] += inc

社会的引力

def social_gravity_move(self):
    """仲の良い相手に自然と近づく"""
    cands = self.nearby_allies(radius=6)
    if cands:
        best = max(cands, key=lambda o: self.rel[o.name])
        if random.random() < 0.25:
            self.move_towards(best.pos())

NPCのパーソナリティプリセット

# 採餌者:安全志向、共感性高い
FORAGER = {
    "risk_tolerance": 0.2,
    "curiosity": 0.3,
    "avoidance": 0.8,
    "stamina": 0.6,
    "empathy": 0.8
}

# 追跡者:リスクを取る、体力が高い
TRACKER = {
    "risk_tolerance": 0.6,
    "curiosity": 0.5,
    "avoidance": 0.2,
    "stamina": 0.8,
    "empathy": 0.6
}

# 放浪者:超高リスク志向、共感性低い
NOMAD = {
    "risk_tolerance": 0.7,
    "curiosity": 0.8,
    "avoidance": 0.1,
    "stamina": 0.9,
    "empathy": 0.3
}

パーソナリティの効果

  • empathyが高い → 他者を助けやすい
  • risk_toleranceが高い → 危険な狩りに挑戦
  • avoidanceが高い → 縄張り意識が強い

実装のポイント

1. 整合慣性(κ)の調整

# 学習が速すぎる場合
self.eta = 0.2  # 学習率を下げる

# 忘れやすすぎる場合
self.lambda_forget = 0.01  # 忘却率を下げる

# 最小値の調整
self.kappa_min = 0.05  # 完全に忘れないようにする

2. 熱の蓄積速度

# ストレスが溜まりすぎる場合
self.alpha = 0.4      # 蓄積係数を下げる
self.beta_E = 0.20    # 減衰係数を上げる

# ストレスが溜まらない場合
self.alpha = 0.8
self.beta_E = 0.10

3. 集団行動の調整

# 呼びかけに応じやすくする
def consider_join_rally(self, t):
    # ...
    u = base + 1.0  # 基準値を上げる
    # ...

# 必要人数を調整
self.emit_rally_for_hunt(t, min_k=2)  # 2人から開始

シミュレーション結果の分析

基本的な統計

if __name__ == "__main__":
    final_npcs, df_logs = run_sim(TICKS=400)
    
    # 生存者数
    survivors = sum(1 for n in final_npcs if n.alive)
    print(f"Survivors: {survivors} / {len(final_npcs)}")
    
    # 捕食者討伐の成功率
    pred_hunts = df_logs[df_logs['action'].str.contains("predator_slay")]
    success_rate = len(pred_hunts[pred_hunts['action'] == 'predator_slay_success']) / len(pred_hunts)
    print(f"Predator Hunt Success Rate: {success_rate:.2%}")

詳細な分析例

# 行動パターンの分析
action_counts = df_logs['action'].value_counts()
print("\n行動の分布:")
print(action_counts.head(10))

# 関係性の強さ
for npc in final_npcs:
    if npc.alive:
        strong_bonds = {name: rel for name, rel in npc.rel.items() if rel > 0.5}
        print(f"{npc.name}: {len(strong_bonds)} strong bonds")

# 縄張りの分析
for npc in final_npcs:
    if npc.alive:
        attachment = npc.territory.attachment_strength
        print(f"{npc.name} territory attachment: {attachment:.2f}")

カスタマイズのアイデア

1. 新しい行動の追加

def build_shelter(self, t):
    """避難所を建設する"""
    if self.fatigue < 50 and self.hunger < 70:
        # 建設コスト
        self.fatigue += 20
        self.hunger += 10
        
        # 縄張りを強化
        self.territory.radius += 1
        self.territory.attachment_strength += 0.1
        
        self.update_kappa("build", True, 30)
        self.log.append({"t": t, "name": self.name, "action": "build_shelter"})

2. 資源の追加

class Environment:
    def __init__(self):
        # ...
        self.water_sources = {}  # 水源
        self.tool_materials = {}  # 道具の材料
        self.medicinal_herbs = {}  # 薬草

3. スキルシステム

class NPC:
    def __init__(self):
        # ...
        self.skills = {
            "hunting": 0.0,
            "foraging": 0.0,
            "medicine": 0.0,
            "crafting": 0.0
        }
    
    def gain_skill_xp(self, skill_name, amount):
        self.skills[skill_name] += amount
        # スキルレベルに応じて成功率が上がる

4. 季節の変化

class SeasonalCycle:
    def __init__(self):
        self.seasons = ["spring", "summer", "autumn", "winter"]
        self.current_season = 0
    
    def get_season_modifier(self):
        season = self.seasons[self.current_season]
        if season == "winter":
            return {"forage_success": 0.3, "energy_cost": 1.5}
        elif season == "summer":
            return {"forage_success": 1.2, "energy_cost": 0.9}
        # ...

まとめ

SSDを使うメリット

  1. シンプルな原理から複雑な行動が創発

    • κ、E、Tという3つのパラメータで多様な行動
  2. 学習と適応が自然に組み込まれる

    • 成功体験が自動的に蓄積される
  3. 個性の表現が容易

    • パーソナリティプリセットで多様なキャラクター
  4. 社会行動が自然に発生

    • 明示的にプログラムしなくても協力行動が生まれる

今後の展開

  • より高度な認知モデルの実装
  • リアルタイムビジュアライゼーション
  • ゲームエンジンへの統合(Unity/Unreal)
  • マルチエージェント強化学習との統合

参考リンク

最後に

このシミュレーションは、「生きている」と感じられるNPCを作るための一つのアプローチです。

SSDという理論フレームワークを使うことで:

  • NPCが自律的に判断する
  • 環境に適応して学習する
  • 他者と協力する
  • 個性的な行動を取る

これらが自然に実現できることを示しました。

皆さんもぜひ、このコードをベースに独自のシミュレーションを作ってみてください!

Discussion