|
|
""" |
|
|
Game state management for Unpredictable Lord. |
|
|
|
|
|
This module defines the game state data structure and provides |
|
|
functions for managing game sessions. |
|
|
""" |
|
|
|
|
|
import random |
|
|
from dataclasses import asdict, dataclass, field |
|
|
from typing import Literal |
|
|
|
|
|
LordPersonality = Literal["cautious", "idealist", "populist"] |
|
|
|
|
|
PERSONALITY_DESCRIPTIONS = { |
|
|
"cautious": "A risk-averse lord who prioritizes stability and financial security above all else.", |
|
|
"idealist": "An emotional idealist who pursues noble goals but reacts harshly to opposition.", |
|
|
"populist": "A popularity-focused lord who prioritizes public approval over long-term planning.", |
|
|
} |
|
|
|
|
|
|
|
|
AdviceType = Literal[ |
|
|
"increase_tax", |
|
|
"decrease_tax", |
|
|
"expand_territory", |
|
|
"improve_diplomacy", |
|
|
"public_festival", |
|
|
"build_infrastructure", |
|
|
"do_nothing", |
|
|
] |
|
|
|
|
|
ADVICE_DESCRIPTIONS: dict[str, dict[str, str]] = { |
|
|
"increase_tax": { |
|
|
"name": "Increase Tax", |
|
|
"description": "Raise taxes to increase treasury, but decrease public satisfaction.", |
|
|
"effects": "Treasury β, Satisfaction β", |
|
|
}, |
|
|
"decrease_tax": { |
|
|
"name": "Decrease Tax", |
|
|
"description": "Lower taxes to improve public satisfaction, but decrease treasury.", |
|
|
"effects": "Treasury β, Satisfaction β", |
|
|
}, |
|
|
"expand_territory": { |
|
|
"name": "Expand Territory", |
|
|
"description": "Launch a military campaign to expand territory. Risky but rewarding.", |
|
|
"effects": "Territory β, Treasury β, Royal Trust Β±, Risk involved", |
|
|
}, |
|
|
"improve_diplomacy": { |
|
|
"name": "Improve Diplomacy", |
|
|
"description": "Strengthen diplomatic relations with the kingdom.", |
|
|
"effects": "Royal Trust β, Treasury β", |
|
|
}, |
|
|
"public_festival": { |
|
|
"name": "Hold Public Festival", |
|
|
"description": "Organize a festival to boost public morale.", |
|
|
"effects": "Satisfaction β, Treasury β", |
|
|
}, |
|
|
"build_infrastructure": { |
|
|
"name": "Build Infrastructure", |
|
|
"description": "Invest in infrastructure to support population growth.", |
|
|
"effects": "Population β, Treasury β, Long-term Treasury β", |
|
|
}, |
|
|
"do_nothing": { |
|
|
"name": "Do Nothing", |
|
|
"description": "Maintain the current state without any action.", |
|
|
"effects": "No change (turn passes)", |
|
|
}, |
|
|
} |
|
|
|
|
|
|
|
|
def get_available_advice() -> dict[str, dict[str, str]]: |
|
|
""" |
|
|
Get all available advice options with their descriptions. |
|
|
|
|
|
Returns: |
|
|
dict: Dictionary of advice types and their descriptions. |
|
|
""" |
|
|
return ADVICE_DESCRIPTIONS |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class GameState: |
|
|
""" |
|
|
Represents the current state of a game session. |
|
|
|
|
|
All numeric parameters range from 0 to 100. |
|
|
Initial values are set to 50 (balanced state). |
|
|
""" |
|
|
|
|
|
|
|
|
territory: int = 50 |
|
|
population: int = 50 |
|
|
treasury: int = 50 |
|
|
satisfaction: int = 50 |
|
|
royal_trust: int = 50 |
|
|
advisor_trust: int = 50 |
|
|
|
|
|
|
|
|
turn: int = 1 |
|
|
lord_personality: LordPersonality = "cautious" |
|
|
|
|
|
|
|
|
game_over: bool = False |
|
|
result: str | None = None |
|
|
|
|
|
|
|
|
current_event: str | None = None |
|
|
event_history: list[str] = field(default_factory=list) |
|
|
|
|
|
def to_dict(self) -> dict: |
|
|
"""Convert game state to dictionary for JSON serialization.""" |
|
|
return asdict(self) |
|
|
|
|
|
def get_status_summary(self) -> str: |
|
|
"""Generate a human-readable status summary.""" |
|
|
status_lines = [ |
|
|
f"Turn: {self.turn}", |
|
|
f"Lord Personality: {self.lord_personality}", |
|
|
"", |
|
|
"=== Parameters ===", |
|
|
f"Territory: {self.territory}/100", |
|
|
f"Population: {self.population}/100", |
|
|
f"Treasury: {self.treasury}/100", |
|
|
f"Public Satisfaction: {self.satisfaction}/100", |
|
|
f"Royal Trust: {self.royal_trust}/100", |
|
|
f"Advisor Trust: {self.advisor_trust}/100", |
|
|
] |
|
|
|
|
|
if self.current_event: |
|
|
status_lines.extend(["", f"Current Event: {self.current_event}"]) |
|
|
|
|
|
if self.game_over: |
|
|
status_lines.extend(["", f"GAME OVER: {self.result}"]) |
|
|
|
|
|
return "\n".join(status_lines) |
|
|
|
|
|
def check_game_over(self) -> tuple[bool, str | None]: |
|
|
""" |
|
|
Check if the game has ended. |
|
|
|
|
|
Returns: |
|
|
tuple: (is_game_over, result_type) |
|
|
""" |
|
|
|
|
|
if self.satisfaction <= 0: |
|
|
return True, "defeat_rebellion" |
|
|
if self.royal_trust <= 0: |
|
|
return True, "defeat_exile" |
|
|
if self.treasury <= 0 and self.satisfaction <= 20: |
|
|
return True, "defeat_bankruptcy" |
|
|
|
|
|
|
|
|
if self.territory >= 100: |
|
|
return True, "victory_territory" |
|
|
if self.advisor_trust >= 100 and self.turn >= 10: |
|
|
return True, "victory_trust" |
|
|
if self.treasury >= 100 and self.royal_trust >= 80: |
|
|
return True, "victory_wealth" |
|
|
|
|
|
return False, None |
|
|
|
|
|
def clamp(self, value: int, min_val: int = 0, max_val: int = 100) -> int: |
|
|
"""Clamp a value to the valid range.""" |
|
|
return max(min_val, min(max_val, value)) |
|
|
|
|
|
def apply_changes(self, changes: dict[str, int]) -> dict[str, int]: |
|
|
""" |
|
|
Apply parameter changes and return the actual changes made. |
|
|
|
|
|
Args: |
|
|
changes: Dictionary of parameter changes (can be positive or negative). |
|
|
|
|
|
Returns: |
|
|
Dictionary of actual changes applied (after clamping). |
|
|
""" |
|
|
actual_changes = {} |
|
|
for param, delta in changes.items(): |
|
|
if hasattr(self, param): |
|
|
old_value = getattr(self, param) |
|
|
new_value = self.clamp(old_value + delta) |
|
|
setattr(self, param, new_value) |
|
|
actual_changes[param] = new_value - old_value |
|
|
return actual_changes |
|
|
|
|
|
|
|
|
|
|
|
PERSONALITY_TRAITS = { |
|
|
"cautious": { |
|
|
"base_acceptance": 0.4, |
|
|
"preferred": ["do_nothing", "improve_diplomacy"], |
|
|
"disliked": ["expand_territory", "decrease_tax"], |
|
|
"trust_weight": 0.6, |
|
|
}, |
|
|
"idealist": { |
|
|
"base_acceptance": 0.5, |
|
|
"preferred": ["build_infrastructure", "public_festival", "decrease_tax"], |
|
|
"disliked": ["increase_tax", "do_nothing"], |
|
|
"trust_weight": 0.4, |
|
|
}, |
|
|
"populist": { |
|
|
"base_acceptance": 0.6, |
|
|
"preferred": ["decrease_tax", "public_festival"], |
|
|
"disliked": ["increase_tax", "expand_territory"], |
|
|
"trust_weight": 0.5, |
|
|
}, |
|
|
} |
|
|
|
|
|
|
|
|
ACTION_EFFECTS: dict[str, dict[str, int]] = { |
|
|
"increase_tax": { |
|
|
"treasury": 15, |
|
|
"satisfaction": -10, |
|
|
}, |
|
|
"decrease_tax": { |
|
|
"treasury": -10, |
|
|
"satisfaction": 15, |
|
|
}, |
|
|
"expand_territory": { |
|
|
"territory": 10, |
|
|
"treasury": -15, |
|
|
"population": 5, |
|
|
|
|
|
}, |
|
|
"improve_diplomacy": { |
|
|
"royal_trust": 12, |
|
|
"treasury": -8, |
|
|
}, |
|
|
"public_festival": { |
|
|
"satisfaction": 15, |
|
|
"treasury": -12, |
|
|
}, |
|
|
"build_infrastructure": { |
|
|
"population": 10, |
|
|
"treasury": -10, |
|
|
"satisfaction": 5, |
|
|
}, |
|
|
"do_nothing": {}, |
|
|
} |
|
|
|
|
|
|
|
|
ALL_ACTIONS: list[str] = list(ACTION_EFFECTS.keys()) |
|
|
|
|
|
|
|
|
PREFERRED_ACTION_PROBABILITY = 0.7 |
|
|
|
|
|
|
|
|
def _calculate_acceptance(state: "GameState", advice: str) -> tuple[bool, str, str]: |
|
|
""" |
|
|
Determine whether the lord accepts the advice. |
|
|
|
|
|
Args: |
|
|
state: The current game state. |
|
|
advice: The advice type. |
|
|
|
|
|
Returns: |
|
|
tuple: (adopted, action_taken, adoption_message) |
|
|
""" |
|
|
personality = state.lord_personality |
|
|
traits = PERSONALITY_TRAITS[personality] |
|
|
|
|
|
|
|
|
base_prob = traits["base_acceptance"] |
|
|
trust_bonus = (state.advisor_trust / 100) * traits["trust_weight"] |
|
|
|
|
|
if advice in traits["preferred"]: |
|
|
preference_bonus = 0.2 |
|
|
elif advice in traits["disliked"]: |
|
|
preference_bonus = -0.25 |
|
|
else: |
|
|
preference_bonus = 0 |
|
|
|
|
|
acceptance_prob = min(0.95, max(0.1, base_prob + trust_bonus + preference_bonus)) |
|
|
|
|
|
|
|
|
adopted = random.random() < acceptance_prob |
|
|
|
|
|
if adopted: |
|
|
action_taken = advice |
|
|
adoption_message = ( |
|
|
f"The lord has decided to follow your advice: " |
|
|
f"{ADVICE_DESCRIPTIONS[advice]['name']}." |
|
|
) |
|
|
else: |
|
|
|
|
|
|
|
|
excluded_actions = {advice} | set(traits["disliked"]) |
|
|
available_actions = [a for a in ALL_ACTIONS if a not in excluded_actions] |
|
|
|
|
|
|
|
|
if not available_actions: |
|
|
action_taken = "do_nothing" |
|
|
else: |
|
|
|
|
|
|
|
|
preferred_available = [ |
|
|
a for a in traits["preferred"] if a not in excluded_actions |
|
|
] |
|
|
|
|
|
if preferred_available and random.random() < PREFERRED_ACTION_PROBABILITY: |
|
|
action_taken = random.choice(preferred_available) |
|
|
else: |
|
|
|
|
|
non_preferred = [ |
|
|
a for a in available_actions if a not in traits["preferred"] |
|
|
] |
|
|
if non_preferred: |
|
|
action_taken = random.choice(non_preferred) |
|
|
elif preferred_available: |
|
|
|
|
|
action_taken = random.choice(preferred_available) |
|
|
else: |
|
|
action_taken = "do_nothing" |
|
|
|
|
|
adoption_message = ( |
|
|
f"The lord has rejected your advice ({ADVICE_DESCRIPTIONS[advice]['name']}) " |
|
|
f"and instead chose: {ADVICE_DESCRIPTIONS[action_taken]['name']}." |
|
|
) |
|
|
|
|
|
return adopted, action_taken, adoption_message |
|
|
|
|
|
|
|
|
def _apply_action_effects( |
|
|
state: "GameState", action_taken: str |
|
|
) -> tuple[dict[str, int], str | None]: |
|
|
""" |
|
|
Apply the effects of the action taken by the lord. |
|
|
|
|
|
Args: |
|
|
state: The current game state. |
|
|
action_taken: The action the lord decided to take. |
|
|
|
|
|
Returns: |
|
|
tuple: (actual_changes, outcome_message) |
|
|
""" |
|
|
effects = ACTION_EFFECTS.get(action_taken, {}).copy() |
|
|
outcome_message = None |
|
|
|
|
|
|
|
|
if action_taken == "expand_territory": |
|
|
if random.random() < 0.6: |
|
|
effects["royal_trust"] = 8 |
|
|
outcome_message = "The military campaign was successful!" |
|
|
else: |
|
|
effects["royal_trust"] = -10 |
|
|
effects["territory"] = 0 |
|
|
effects["satisfaction"] = -5 |
|
|
outcome_message = "The military campaign failed, damaging our reputation." |
|
|
|
|
|
|
|
|
actual_changes = state.apply_changes(effects) |
|
|
|
|
|
return actual_changes, outcome_message |
|
|
|
|
|
|
|
|
def _update_advisor_trust(state: "GameState", adopted: bool) -> int: |
|
|
""" |
|
|
Update the advisor trust based on whether advice was followed. |
|
|
|
|
|
Args: |
|
|
state: The current game state. |
|
|
adopted: Whether the advice was adopted. |
|
|
|
|
|
Returns: |
|
|
int: The change in advisor trust. |
|
|
""" |
|
|
if adopted: |
|
|
trust_change = random.randint(2, 5) |
|
|
else: |
|
|
trust_change = random.randint(-5, -1) |
|
|
|
|
|
state.advisor_trust = state.clamp(state.advisor_trust + trust_change) |
|
|
return trust_change |
|
|
|
|
|
|
|
|
def execute_turn_logic(state: "GameState", advice: str) -> dict: |
|
|
""" |
|
|
Execute a turn based on the given advice. |
|
|
|
|
|
This function determines whether the lord accepts the advice based on |
|
|
personality and trust, applies the effects, and returns the result. |
|
|
|
|
|
Args: |
|
|
state: The current game state. |
|
|
advice: The advice type chosen by the Lord AI. |
|
|
|
|
|
Returns: |
|
|
dict: Result containing adoption status, action taken, effects, and messages. |
|
|
""" |
|
|
|
|
|
if advice not in ADVICE_DESCRIPTIONS: |
|
|
return { |
|
|
"error": "Invalid advice", |
|
|
"message": f"Unknown advice type: {advice}. Use list_available_advice to see options.", |
|
|
} |
|
|
|
|
|
if state.game_over: |
|
|
return { |
|
|
"error": "Game over", |
|
|
"message": f"This game has ended with result: {state.result}", |
|
|
"game_state": state.to_dict(), |
|
|
} |
|
|
|
|
|
|
|
|
adopted, action_taken, adoption_message = _calculate_acceptance(state, advice) |
|
|
|
|
|
|
|
|
actual_changes, outcome_message = _apply_action_effects(state, action_taken) |
|
|
|
|
|
|
|
|
trust_change = _update_advisor_trust(state, adopted) |
|
|
actual_changes["advisor_trust"] = trust_change |
|
|
|
|
|
|
|
|
state.turn += 1 |
|
|
|
|
|
|
|
|
game_over, result = state.check_game_over() |
|
|
if game_over: |
|
|
state.game_over = True |
|
|
state.result = result |
|
|
|
|
|
|
|
|
changes_str = ", ".join( |
|
|
f"{k}: {'+' if v > 0 else ''}{v}" for k, v in actual_changes.items() if v != 0 |
|
|
) |
|
|
|
|
|
return { |
|
|
"adopted": adopted, |
|
|
"advice_given": advice, |
|
|
"action_taken": action_taken, |
|
|
"action_name": ADVICE_DESCRIPTIONS[action_taken]["name"], |
|
|
"adoption_message": adoption_message, |
|
|
"outcome_message": outcome_message, |
|
|
"parameter_changes": actual_changes, |
|
|
"changes_summary": changes_str or "No changes", |
|
|
"game_over": state.game_over, |
|
|
"game_result": state.result, |
|
|
"new_state": state.to_dict(), |
|
|
"status_summary": state.get_status_summary(), |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
game_sessions: dict[str, GameState] = {} |
|
|
|
|
|
|
|
|
def create_session( |
|
|
session_id: str, personality: LordPersonality = "cautious" |
|
|
) -> GameState: |
|
|
""" |
|
|
Create a new game session with the given ID. |
|
|
|
|
|
Args: |
|
|
session_id: Unique session identifier. |
|
|
personality: The lord's personality type. |
|
|
|
|
|
Returns: |
|
|
The newly created GameState. |
|
|
""" |
|
|
state = GameState(lord_personality=personality) |
|
|
game_sessions[session_id] = state |
|
|
return state |
|
|
|
|
|
|
|
|
def get_session(session_id: str) -> GameState | None: |
|
|
""" |
|
|
Retrieve a game session by ID. |
|
|
|
|
|
Args: |
|
|
session_id: The session ID to look up. |
|
|
|
|
|
Returns: |
|
|
The GameState if found, None otherwise. |
|
|
""" |
|
|
return game_sessions.get(session_id) |
|
|
|
|
|
|
|
|
def delete_session(session_id: str) -> bool: |
|
|
""" |
|
|
Delete a game session. |
|
|
|
|
|
Args: |
|
|
session_id: The session ID to delete. |
|
|
|
|
|
Returns: |
|
|
True if deleted, False if not found. |
|
|
""" |
|
|
if session_id in game_sessions: |
|
|
del game_sessions[session_id] |
|
|
return True |
|
|
return False |
|
|
|