File size: 15,766 Bytes
389bf98 ea5a5fd 389bf98 abe8a34 389bf98 ea5a5fd 353b2d2 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd 353b2d2 ea5a5fd 353b2d2 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd b48bb47 ea5a5fd 389bf98 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
"""
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
|