""" GameState - Rich state tracking for game configs with logic gates. Provides comprehensive tracking beyond the basic Player class: - Inventory, money, knowledge (existing) - People met, locations discovered - Missions/quests (active, completed, failed) - Boolean flags and numeric counters - Choice history for analytics JSON-serializable for save/load functionality. """ from typing import Dict, List, Set, Any, Optional from dataclasses import dataclass, field import json import time @dataclass class GameState: """ Comprehensive game state tracking. All fields are JSON-serializable for save/load. """ # ==================== Character Resources (from existing Player) ==================== inventory: List[str] = field(default_factory=list) money: int = 0 knowledge: Dict[str, Any] = field(default_factory=dict) # ==================== People/NPCs ==================== people_met: Set[str] = field(default_factory=set) npc_reputation: Dict[str, int] = field(default_factory=dict) # NPC -> affinity score # ==================== Locations ==================== locations_visited: Set[str] = field(default_factory=set) locations_discovered: Set[str] = field(default_factory=set) # Known but not visited current_location: str = "" current_state: str = "" # ==================== Missions/Quests ==================== missions_active: Dict[str, Dict] = field(default_factory=dict) # id -> {status, progress, data} missions_completed: Set[str] = field(default_factory=set) missions_failed: Set[str] = field(default_factory=set) # ==================== Flags (boolean switches) ==================== flags: Dict[str, bool] = field(default_factory=dict) # ==================== Counters (numeric trackers) ==================== counters: Dict[str, int] = field(default_factory=dict) # ==================== History Tracking ==================== choice_history: List[Dict] = field(default_factory=list) # [{state, choice, timestamp}] state_visit_counts: Dict[str, int] = field(default_factory=dict) # ==================== Metadata ==================== game_start_time: float = field(default_factory=time.time) total_choices_made: int = 0 # ==================== Item Methods ==================== def add_item(self, item: str) -> None: """Add an item to inventory (no duplicates).""" if item not in self.inventory: self.inventory.append(item) def add_items(self, items: List[str]) -> None: """Add multiple items to inventory.""" for item in items: self.add_item(item) def remove_item(self, item: str) -> bool: """Remove an item from inventory. Returns True if successful.""" if item in self.inventory: self.inventory.remove(item) return True return False def has_item(self, item: str) -> bool: """Check if player has an item.""" return item in self.inventory def item_count(self, item: str) -> int: """Count occurrences of an item (for stackable items).""" return self.inventory.count(item) # ==================== Money Methods ==================== def add_money(self, amount: int) -> None: """Add money to player's balance.""" self.money += amount def remove_money(self, amount: int) -> bool: """Remove money. Returns True if player had enough.""" if self.money >= amount: self.money -= amount return True return False def has_money(self, amount: int) -> bool: """Check if player has at least this much money.""" return self.money >= amount # ==================== People Methods ==================== def meet_person(self, person: str) -> None: """Mark an NPC as met.""" self.people_met.add(person) def has_met(self, person: str) -> bool: """Check if player has met an NPC.""" return person in self.people_met def adjust_reputation(self, npc: str, change: int) -> None: """Adjust reputation with an NPC.""" self.npc_reputation[npc] = self.npc_reputation.get(npc, 0) + change def get_reputation(self, npc: str) -> int: """Get reputation with an NPC (default 0).""" return self.npc_reputation.get(npc, 0) # ==================== Location Methods ==================== def visit_location(self, location: str) -> None: """Mark a location as visited.""" self.locations_visited.add(location) self.locations_discovered.discard(location) # No longer just discovered def discover_location(self, location: str) -> None: """Mark a location as discovered (but not visited).""" if location not in self.locations_visited: self.locations_discovered.add(location) def has_visited(self, location: str) -> bool: """Check if player has visited a location.""" return location in self.locations_visited def has_discovered(self, location: str) -> bool: """Check if player knows about a location (visited or discovered).""" return location in self.locations_visited or location in self.locations_discovered # ==================== Mission Methods ==================== def start_mission(self, mission_id: str, data: Dict = None) -> None: """Start a new mission/quest.""" self.missions_active[mission_id] = data or {"status": "active", "progress": 0} def update_mission(self, mission_id: str, updates: Dict) -> None: """Update mission data.""" if mission_id in self.missions_active: self.missions_active[mission_id].update(updates) def complete_mission(self, mission_id: str) -> None: """Mark a mission as completed.""" if mission_id in self.missions_active: del self.missions_active[mission_id] self.missions_completed.add(mission_id) def fail_mission(self, mission_id: str) -> None: """Mark a mission as failed.""" if mission_id in self.missions_active: del self.missions_active[mission_id] self.missions_failed.add(mission_id) def is_mission_complete(self, mission_id: str) -> bool: """Check if a mission is completed.""" return mission_id in self.missions_completed def is_mission_active(self, mission_id: str) -> bool: """Check if a mission is currently active.""" return mission_id in self.missions_active def is_mission_failed(self, mission_id: str) -> bool: """Check if a mission has failed.""" return mission_id in self.missions_failed def get_mission_data(self, mission_id: str) -> Optional[Dict]: """Get data for an active mission.""" return self.missions_active.get(mission_id) # ==================== Flag Methods ==================== def set_flag(self, flag: str, value: bool = True) -> None: """Set a boolean flag.""" self.flags[flag] = value def clear_flag(self, flag: str) -> None: """Clear (set to False) a flag.""" self.flags[flag] = False def has_flag(self, flag: str) -> bool: """Check if a flag is set (True).""" return self.flags.get(flag, False) def toggle_flag(self, flag: str) -> bool: """Toggle a flag and return new value.""" new_value = not self.flags.get(flag, False) self.flags[flag] = new_value return new_value # ==================== Counter Methods ==================== def set_counter(self, name: str, value: int) -> None: """Set a counter to a specific value.""" self.counters[name] = value def increment_counter(self, name: str, amount: int = 1) -> int: """Increment a counter and return new value.""" self.counters[name] = self.counters.get(name, 0) + amount return self.counters[name] def decrement_counter(self, name: str, amount: int = 1) -> int: """Decrement a counter and return new value.""" return self.increment_counter(name, -amount) def get_counter(self, name: str, default: int = 0) -> int: """Get a counter value.""" return self.counters.get(name, default) # ==================== Knowledge Methods ==================== def update_knowledge(self, key: str, value: Any = True) -> None: """Add or update knowledge.""" self.knowledge[key] = value def has_knowledge(self, key: str) -> bool: """Check if player has a knowledge entry.""" return key in self.knowledge def get_knowledge(self, key: str, default: Any = None) -> Any: """Get a knowledge value.""" return self.knowledge.get(key, default) def remove_knowledge(self, key: str) -> None: """Remove a knowledge entry.""" self.knowledge.pop(key, None) # ==================== History Methods ==================== def record_choice(self, state: str, choice: str) -> None: """Record a player choice for history tracking.""" self.choice_history.append({ "state": state, "choice": choice, "timestamp": time.time() }) self.total_choices_made += 1 self.state_visit_counts[state] = self.state_visit_counts.get(state, 0) + 1 def get_visit_count(self, state: str) -> int: """Get number of times a state was visited.""" return self.state_visit_counts.get(state, 0) def get_last_choices(self, n: int = 5) -> List[Dict]: """Get the last N choices made.""" return self.choice_history[-n:] if self.choice_history else [] # ==================== Serialization ==================== def to_dict(self) -> Dict: """Convert to JSON-serializable dict for saving.""" return { "inventory": self.inventory, "money": self.money, "knowledge": self.knowledge, "people_met": list(self.people_met), "npc_reputation": self.npc_reputation, "locations_visited": list(self.locations_visited), "locations_discovered": list(self.locations_discovered), "current_location": self.current_location, "current_state": self.current_state, "missions_active": self.missions_active, "missions_completed": list(self.missions_completed), "missions_failed": list(self.missions_failed), "flags": self.flags, "counters": self.counters, "choice_history": self.choice_history, "state_visit_counts": self.state_visit_counts, "game_start_time": self.game_start_time, "total_choices_made": self.total_choices_made } @classmethod def from_dict(cls, data: Dict) -> 'GameState': """Reconstruct GameState from dict (for loading saves).""" state = cls() state.inventory = data.get("inventory", []) state.money = data.get("money", 0) state.knowledge = data.get("knowledge", {}) state.people_met = set(data.get("people_met", [])) state.npc_reputation = data.get("npc_reputation", {}) state.locations_visited = set(data.get("locations_visited", [])) state.locations_discovered = set(data.get("locations_discovered", [])) state.current_location = data.get("current_location", "") state.current_state = data.get("current_state", "") state.missions_active = data.get("missions_active", {}) state.missions_completed = set(data.get("missions_completed", [])) state.missions_failed = set(data.get("missions_failed", [])) state.flags = data.get("flags", {}) state.counters = data.get("counters", {}) state.choice_history = data.get("choice_history", []) state.state_visit_counts = data.get("state_visit_counts", {}) state.game_start_time = data.get("game_start_time", time.time()) state.total_choices_made = data.get("total_choices_made", 0) return state def to_json(self) -> str: """Serialize to JSON string.""" return json.dumps(self.to_dict(), indent=2) @classmethod def from_json(cls, json_str: str) -> 'GameState': """Deserialize from JSON string.""" return cls.from_dict(json.loads(json_str)) def reset(self) -> None: """Reset state to initial values (for new game).""" self.inventory.clear() self.money = 0 self.knowledge.clear() self.people_met.clear() self.npc_reputation.clear() self.locations_visited.clear() self.locations_discovered.clear() self.current_location = "" self.current_state = "" self.missions_active.clear() self.missions_completed.clear() self.missions_failed.clear() self.flags.clear() self.counters.clear() self.choice_history.clear() self.state_visit_counts.clear() self.game_start_time = time.time() self.total_choices_made = 0 class Player: """ Backwards-compatible Player class that wraps GameState. Existing configs using lambda consequences will continue to work. """ def __init__(self, game_state: GameState = None): self._state = game_state or GameState() # Existing API (unchanged for backwards compatibility) @property def inventory(self) -> List[str]: return self._state.inventory @property def money(self) -> int: return self._state.money @money.setter def money(self, value: int): self._state.money = value @property def knowledge(self) -> Dict[str, Any]: return self._state.knowledge def add_item(self, item: str) -> None: self._state.add_item(item) def has_item(self, item: str) -> bool: return self._state.has_item(item) def update_knowledge(self, topic: str) -> None: self._state.update_knowledge(topic, True) # NEW: Expose full state for advanced usage @property def game_state(self) -> GameState: return self._state