GameConfigIdea / game_state.py
kwabs22
Port changes from duplicate space to original
9328e91
"""
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