ryomo's picture
refactor: organize codebase into "chat" and "mcp_server" directories for feature separation
aaa5822
"""
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.",
}
# Available advice options that the Lord AI can choose from
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).
"""
# Core parameters (0-100, higher is better)
territory: int = 50 # Territory size - affects income and population cap
population: int = 50 # Population - base for tax revenue
treasury: int = 50 # Finances - required for policy execution
satisfaction: int = 50 # Public satisfaction - low values risk rebellion
royal_trust: int = 50 # Trust from the kingdom - affects diplomacy
advisor_trust: int = 50 # Lord's trust in advisor - affects advice acceptance
# Game progress
turn: int = 1
lord_personality: LordPersonality = "cautious"
# Game status
game_over: bool = False
result: str | None = None # "victory_territory", "victory_trust", etc.
# Event tracking
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)
"""
# Defeat conditions
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"
# Victory conditions
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-based acceptance rates and biases
PERSONALITY_TRAITS = {
"cautious": {
"base_acceptance": 0.4, # Low base acceptance
"preferred": ["do_nothing", "improve_diplomacy"],
"disliked": ["expand_territory", "decrease_tax"],
"trust_weight": 0.6, # How much advisor_trust affects acceptance
},
"idealist": {
"base_acceptance": 0.5,
"preferred": ["build_infrastructure", "public_festival", "decrease_tax"],
"disliked": ["increase_tax", "do_nothing"],
"trust_weight": 0.4, # Less influenced by trust, more by ideals
},
"populist": {
"base_acceptance": 0.6, # Higher base acceptance
"preferred": ["decrease_tax", "public_festival"],
"disliked": ["increase_tax", "expand_territory"],
"trust_weight": 0.5,
},
}
# Effects of each action when adopted
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,
# royal_trust is handled specially (can go up or down)
},
"improve_diplomacy": {
"royal_trust": 12,
"treasury": -8,
},
"public_festival": {
"satisfaction": 15,
"treasury": -12,
},
"build_infrastructure": {
"population": 10,
"treasury": -10,
"satisfaction": 5,
},
"do_nothing": {},
}
# All possible actions (derived from ACTION_EFFECTS keys)
ALL_ACTIONS: list[str] = list(ACTION_EFFECTS.keys())
# Probability that lord chooses a preferred action when rejecting advice
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]
# Calculate acceptance probability
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))
# Determine if advice is adopted
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:
# Lord rejects and takes alternative action based on personality
# Exclude: the original advice and disliked actions
excluded_actions = {advice} | set(traits["disliked"])
available_actions = [a for a in ALL_ACTIONS if a not in excluded_actions]
# If no actions available (edge case), fall back to do_nothing
if not available_actions:
action_taken = "do_nothing"
else:
# High probability: choose from preferred actions
# Low probability: choose from other available actions
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:
# Choose from non-preferred available actions
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:
# All available actions are preferred, pick from preferred
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
# Special handling for expand_territory (risky)
if action_taken == "expand_territory":
if random.random() < 0.6: # 60% success
effects["royal_trust"] = 8
outcome_message = "The military campaign was successful!"
else:
effects["royal_trust"] = -10
effects["territory"] = 0 # Failed expansion
effects["satisfaction"] = -5
outcome_message = "The military campaign failed, damaging our reputation."
# Apply changes to state
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.
"""
# Validate input
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(),
}
# Step 1: Determine if advice is accepted
adopted, action_taken, adoption_message = _calculate_acceptance(state, advice)
# Step 2: Apply effects of the action
actual_changes, outcome_message = _apply_action_effects(state, action_taken)
# Step 3: Update advisor trust
trust_change = _update_advisor_trust(state, adopted)
actual_changes["advisor_trust"] = trust_change
# Advance turn
state.turn += 1
# Check for game over
game_over, result = state.check_game_over()
if game_over:
state.game_over = True
state.result = result
# Build result message
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(),
}
# Global session storage
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