| """Figures (named real cast) as game-theoretic players (GAME_RULES.md §0.2; |
| COUNTRY_SCENARIOS.md §2). |
| |
| Each figure has a *base utility vector*: the indicators it rewards/punishes you for moving. |
| At game start the SLM (Cast Designer) gives each figure traits + a hidden agenda and may |
| nudge its utility within bounds (±SHIFT_BOUND) — stored as the *effective* utility in |
| state.cast. Stance is then derived deterministically from the effective utility; the SLM |
| narrates the stance, it does not decide it. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
|
|
| from engine.state import WorldState |
|
|
| SHIFT_BOUND = 0.3 |
|
|
|
|
| @dataclass |
| class Figure: |
| key: str |
| name: str |
| role: str |
| faction: str |
| utility: dict[str, float] = field(default_factory=dict) |
| themes: list[str] = field(default_factory=list) |
| start_favor: float = 0.0 |
| adjustable: bool = True |
| influence: float = 0.0 |
| |
|
|
|
|
| |
|
|
| UTILITY_PRESETS: dict[str, dict[str, float]] = { |
| "treasury": {"fiscal_health": 1.0, "economy": 0.7, "public_services": -0.2}, |
| "loyalist": {"approval": 0.6, "social_cohesion": 0.4, "international_power": 0.3}, |
| "diplomat": {"international_power": 0.8, "security": 0.3}, |
| "hawk": {"international_power": 0.6, "security": 0.7, "fiscal_health": -0.2}, |
| "military": {"security": 0.8, "international_power": 0.5, "fiscal_health": -0.2}, |
| "pla": {"pla_loyalty": 1.0, "international_power": 0.5, "fiscal_health": -0.2}, |
| "health": {"public_services": 0.7, "approval": 0.2, "fiscal_health": -0.1}, |
| "justice": {"security": 0.6, "institutional_stability": 0.5}, |
| "security": {"security": 0.8, "social_cohesion": -0.1}, |
| "opposition": {"social_cohesion": 0.9, "approval": -0.6, "institutional_stability": 0.4}, |
| "media": {"approval": 0.5, "social_cohesion": 0.6, "security": -0.2}, |
| "elite": {"fiscal_health": 0.7, "economy": 0.6, "international_power": 0.3, "institutional_stability": 0.3}, |
| "society": {"approval": 0.6, "public_services": 0.7, "economy": 0.6, "fiscal_health": -0.1}, |
| "institution": {"fiscal_health": 0.8, "economy": 0.4, "institutional_stability": 0.5}, |
| "foreign": {"international_power": 0.8, "security": 0.2}, |
| } |
|
|
| |
| _START_FAVOR = {"opposition": -45.0, "military": -10.0, "foreign": -15.0} |
| _NONADJUSTABLE = {"institution"} |
|
|
|
|
| def fig(key: str, name: str, role: str, faction: str, preset: str, |
| themes: list[str], influence: float = 0.0, **over: float) -> Figure: |
| """Build a Figure from a utility preset, with optional per-indicator overrides.""" |
| utility = dict(UTILITY_PRESETS[preset]) |
| utility.update(over) |
| return Figure( |
| key=key, name=name, role=role, faction=faction, utility=utility, themes=themes, |
| start_favor=_START_FAVOR.get(faction, 0.0), |
| adjustable=faction not in _NONADJUSTABLE, |
| influence=influence, |
| ) |
|
|
|
|
| |
|
|
| def clamp_shift(value: float) -> float: |
| return max(-SHIFT_BOUND, min(SHIFT_BOUND, value)) |
|
|
|
|
| def effective_utility(figure: Figure, shift: dict[str, float] | None) -> dict[str, float]: |
| """base utility + clamped shift (only if the figure is adjustable).""" |
| if not shift or not figure.adjustable: |
| return dict(figure.utility) |
| eff = dict(figure.utility) |
| for ind, delta in shift.items(): |
| eff[ind] = eff.get(ind, 0.0) + clamp_shift(delta) |
| return eff |
|
|
|
|
| |
|
|
| def favor_to_stance(favor: float) -> str: |
| if favor <= -30: |
| return "hostile" |
| if favor >= 30: |
| return "allied" |
| return "neutral" |
|
|
|
|
| def expected_payoff(utility: dict[str, float], applied: dict[str, float]) -> float: |
| return sum(weight * applied.get(ind, 0.0) for ind, weight in utility.items()) |
|
|
|
|
| def update_agent_stances( |
| state: WorldState, roster: dict[str, Figure], applied: dict[str, float] |
| ) -> dict[str, bool]: |
| """Mutate favor + stance for every figure using its *effective* utility. Returns changes.""" |
| changed: dict[str, bool] = {} |
| for key, figure in roster.items(): |
| util = effective_utility(figure, state.cast.get(key, {}).get("utility_shift")) |
| payoff = expected_payoff(util, applied) |
| old = state.agent_stances.get(key, favor_to_stance(state.agent_favor.get(key, figure.start_favor))) |
| favor = state.agent_favor.get(key, figure.start_favor) |
| favor = max(-100.0, min(100.0, favor * 0.92 + payoff * 1.5)) |
| state.agent_favor[key] = favor |
| state.agent_stances[key] = favor_to_stance(favor) |
| changed[key] = state.agent_stances[key] != old |
| return changed |
|
|