Skip to content

Instantly share code, notes, and snippets.

@Pinacolada64
Last active June 28, 2025 00:04
Show Gist options
  • Save Pinacolada64/83e738ee99384003540149045038cf4e to your computer and use it in GitHub Desktop.
Save Pinacolada64/83e738ee99384003540149045038cf4e to your computer and use it in GitHub Desktop.
Advanced command parser and game states written with Gemini AI's help
import abc
import logging
from datetime import datetime, timedelta
from typing import Optional
from astral import LocationInfo, sun, moon
import pytz # python time zones
from enum import Enum
from combat import CombatSystem, Monster
# Configure logging
logging.basicConfig(level=logging.DEBUG,
format='%(levelname)10s | %(funcName)15s() | %(message)s')
class Season(Enum):
SPRING = "spring"
SUMMER = "summer"
AUTUMN = "autumn" # Or FALL = "fall"
WINTER = "winter"
UNKNOWN = "unknown" # For initial state or errors
# --- NEW: Terrain Enumeration ---
class Terrain(Enum):
OUTDOORS = "outdoors"
IN_BUILDING = "in_building"
INDOORS_CAVE = "indoors_cave"
SNOWY = "snowy" # Specific for snowy areas
FOREST = "forest" # Specific for forests
# Add more as needed, e.g.:
WATER = "water"
# URBAN = "urban"
DESERT = "desert"
def __str__(self):
return self.value
# --- Game Clock and Location Information ---
# In GameClock class definition:
class GameClock:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(GameClock, cls).__new__(cls)
return cls._instance
def __init__(self, start_date=None, location_name="Puyallup, WA", region="USA",
timezone_name="America/Los_Angeles", latitude=47.195, longitude=-122.290):
if not hasattr(self, '_initialized'):
self.location_info = LocationInfo(location_name, region, timezone_name, latitude, longitude)
local_tz = pytz.timezone(timezone_name)
self.current_datetime = start_date if start_date else datetime.now(pytz.utc).astimezone(local_tz)
if self.current_datetime.tzinfo is None: # Ensure it's localized if a naive datetime was passed
self.current_datetime = local_tz.localize(self.current_datetime)
# NEW: Clock control
self._is_fixed_time = False
# False means game time advances with player actions (default)
# True means game time runs on wall clock (real) time
self._fixed_time_minutes_per_action = 5 # How many minutes advance per player action in fixed mode
self._current_season = self._determine_season() # Determine initial season
logging.info(f"GameClock initialized. Current game time: {self.current_datetime}")
logging.info(f"Game Location: {self.location_info.name} ({self.location_info.timezone})")
self._initialized = True
def get_current_datetime(self):
# If in wall clock mode, return actual current time
if not self._is_fixed_time:
local_tz = pytz.timezone(self.location_info.timezone)
return datetime.now(pytz.utc).astimezone(local_tz)
return self.current_datetime
def advance_time(self, minutes=None):
"""Advances game time by a specified number of minutes.
If in fixed time mode, it advances by a set amount or specific minutes."""
if self._is_fixed_time:
# In fixed time, use the configured minutes per action or override
minutes_to_advance = minutes if minutes is not None else self._fixed_time_minutes_per_action
self.current_datetime += timedelta(minutes=minutes_to_advance)
logging.debug(
f"Game time advanced by {minutes_to_advance} minutes (fixed mode). New time: {self.current_datetime}")
else:
# In wall clock mode, time advances automatically, so this call does nothing
logging.debug("Game time is running on wall clock; advance_time call ignored.")
pass # No explicit advancement needed; get_current_datetime handles it.
# Always update season when time is logically advanced or retrieved for consistency
self._current_season = self._determine_season()
# NEW: Methods for debug control
def set_fixed_time_mode(self, enable: bool):
"""Sets whether the game clock advances only on player actions (True) or runs on real-world time (False)."""
self._is_fixed_time = enable
mode_str = "fixed (per-action advancement)" if enable else "wall clock (real-time)"
logging.info(f"GameClock mode set to: {mode_str}.")
print(f"Game time is now {mode_str}.")
def is_fixed_time_mode(self):
"""Returns True if the clock is in fixed time mode, False if in wall clock mode."""
return self._is_fixed_time
def set_datetime(self, year, month, day, hour=0, minute=0, second=0):
"""Sets the game's current date and time to a specific value."""
try:
local_tz = pytz.timezone(self.location_info.timezone)
new_dt = local_tz.localize(datetime(year, month, day, hour, minute, second))
self.current_datetime = new_dt
self._current_season = self._determine_season() # Recalculate season immediately
logging.info(f"Game clock set to: {self.current_datetime}. Season: {self._current_season.value}")
print(
f"Game clock set to: {self.current_datetime.strftime('%Y-%m-%d %H:%M:%S')}."
f" Current season: {self._current_season.value.capitalize()}.")
return True
except Exception as e:
logging.error(f"Failed to set datetime: {e}")
print(f"Error setting time: {e}. Please use YYYY MM DD HH MM SS format.")
return False
def jump_to_season(self, season_name: str):
"""Attempts to jump to a date representing the middle of the specified season."""
season_map = {
"spring": (3, 20), # March 20th
"summer": (6, 20), # June 20th
"autumn": (9, 20), # September 20th
"winter": (12, 20) # December 20th
}
season_name_lower = season_name.lower()
if season_name_lower in season_map:
month, day = season_map[season_name_lower]
current_year = self.current_datetime.year
# Use noon for a consistent time of day
return self.set_datetime(current_year, month, day, 12, 0, 0)
else:
print(f"Invalid season '{season_name}'. Choose from: {', '.join(season_map.keys())}.")
logging.warning(f"Attempted to jump to invalid season: {season_name}.")
return False
def get_current_season(self):
"""Returns the current season as a Season Enum member."""
return self._current_season
def _determine_season(self):
"""Determines the current season based on the month of current_datetime."""
month = self.current_datetime.month
if 3 <= month <= 5:
logging.info("Season has advanced into Spring.")
return Season.SPRING
elif 6 <= month <= 8:
logging.info("Season has advanced into summer.")
return Season.SUMMER
elif 9 <= month <= 11:
logging.info("Season has advanced into Autumn.")
return Season.AUTUMN
elif month == 12 or 1 <= month <= 2:
logging.info("Season has advanced into Winter.")
return Season.WINTER
else:
logging.warning("Season is unknown!")
return Season.UNKNOWN # Should not happen with valid dates
def get_solar_event_info(self):
"""Returns string description of current solar event (day, night, dawn, dusk)."""
s = sun.sun(self.location_info.observer, date=self.current_datetime.date(), tzinfo=self.location_info.timezone)
current_time = self.current_datetime.astimezone(None).time()
dawn_start = s['dawn'].time()
sunrise_start = s['sunrise'].time()
sunset_end = s['sunset'].time()
dusk_end = s['dusk'].time()
if dawn_start <= current_time < sunrise_start:
return "the faint glow of dawn breaking"
elif sunrise_start <= current_time < sunset_end:
return "daylight"
elif sunset_end <= current_time < dusk_end:
return "the twilight hours"
else:
return "night"
def get_detailed_solar_description(self):
"""Returns a detailed string description for the window."""
s = sun.sun(self.location_info.observer, date=self.current_datetime.date(), tzinfo=self.location_info.timezone)
current_dt_local = self.current_datetime.astimezone(None)
current_time = current_dt_local.time()
dawn_start = s['dawn'].time()
sunrise_start = s['sunrise'].time()
noon = s['noon'].time()
sunset_end = s['sunset'].time()
dusk_end = s['dusk'].time()
is_night = (current_time >= dusk_end) or (current_time < dawn_start)
if dawn_start <= current_time < sunrise_start:
return "the first light of dawn breaking, painting the horizon with soft colors."
elif sunrise_start <= current_time < noon:
return "the early morning sun shining brightly."
elif noon <= current_time < sunset_end:
return "the afternoon sun casting long shadows."
elif sunset_end <= current_time < dusk_end:
return "the golden light of dusk, as the sun dips below the horizon."
elif is_night:
moon_phase = self.get_moon_phase_description()
return (f"night, with stars twinkling faintly. "
f" The moon, currently {moon_phase}, hangs in the sky.")
else:
return "the shifting light, though the exact time is unclear."
def get_moon_phase_description(self):
"""Returns a string description of the current moon phase."""
phase = moon.phase(self.current_datetime.date())
if 0 <= phase < 1.84:
return "a New Moon"
if 1.84 <= phase < 5.53:
return "a Waxing Crescent moon"
if 5.53 <= phase < 9.22:
return "a First Quarter moon"
if 9.22 <= phase < 12.91:
return "a Waxing Gibbous moon"
if 12.91 <= phase < 16.61:
return "a Full Moon"
if 16.61 <= phase < 20.3:
return "a Waning Gibbous moon"
if 20.3 <= phase < 24.99:
return "a Last Quarter moon"
if 24.99 <= phase < 27.78:
return "a Waning Crescent moon"
return "a New Moon"
def set_logging_level(self, level_name: str):
"""Sets the global logging level for the application."""
level_map = {
"debug": logging.DEBUG,
"info": logging.INFO,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL
}
level = level_map.get(level_name.lower())
if level is not None:
logging.getLogger().setLevel(level) # Set the root logger level
print(f"Logging level set to {level_name.upper()}.")
logging.info(f"Logging level changed to {level_name.upper()}.")
return True
else:
print(f"Invalid logging level '{level_name}'. Choose from:")
for level in level_map.values():
print(level)
logging.warning(f"Attempted to set invalid logging level: {level_name}.")
return False
# GameClock initialized with actual current time in Puyallup
location_timezone_name = "America/Los_Angeles"
local_timezone = pytz.timezone(location_timezone_name)
current_actual_time = datetime.now(pytz.utc).astimezone(local_timezone)
game_clock = GameClock(start_date=current_actual_time)
# --- 1. Receiver: Game Objects ---
class Player:
def __init__(self, name="Hero"):
self.name = name
self.inventory = []
self.current_room = None
self.description = "As good-looking as ever."
# NEW: Combat stats
self.max_health = 100 # Maximum health
self.current_health = 100 # Current health
self.attack_power = 20 # Base attack damage
logging.info(f"Player '{self.name}' initialized (HP: {self.current_health}, ATK: {self.attack_power}).")
# NEW: Combat methods for Player
def is_alive(self):
return self.current_health > 0
def take_damage(self, amount):
self.current_health -= amount
if self.current_health < 0:
self.current_health = 0
logging.debug(f"{self.name} took {amount} damage. Remaining HP: {self.current_health}.")
print(f"You take {amount} damage! ({self.current_health}/{self.max_health} HP remaining)")
if not self.is_alive():
print("You collapse, unable to continue...")
def get_item_from_inventory(self, item_name):
"""Helper to get an item from inventory by name (string match, not item object)."""
return next((item for item in self.inventory if item_name.lower() in item.aliases), None)
def add_item(self, item):
self.inventory.append(item)
item.current_terrain_types = [] # Item is now in inventory, no terrain type
logging.debug(f"{self.name} picked up the {item.name}.")
print(f"{self.name} picked up the {item.name}.")
def describe_self(self):
# Method to describe the player themselves
print(f"\nYou look at yourself. {self.description}\n")
# You could also add more details here, like:
# TODO: print(f"You are currently wearing: (not implemented yet)")
# TODO: print(f"You are carrying: {', '.join([item.name for item in self.inventory]) if
# self.inventory else 'nothing'}.")
def remove_item(self, item):
if item in self.inventory:
self.inventory.remove(item)
# Item is about to be dropped into current_room, so its location will be set there
logging.debug(f"{self.name} dropped the {item.name}.")
print(f"{self.name} dropped the {item.name}.")
return True
logging.warning(f"{self.name} tried to drop {item.name} but doesn't have it.")
print(f"{self.name} doesn't have the {item.name}.")
return False
def get_all_interactable_items(self):
"""Returns a combined list of items in the current room and in player's inventory."""
return self.current_room.items + self.inventory
def move_to(self, room):
# Also update player.move_to to show monsters:
self.current_room = room
logging.debug(f"{self.name} moved to {room.name}.")
# NEW: Display the transition message if it exists
if room.transition_message:
print(f"\n{room.transition_message}")
print(room.name)
print(room.get_full_description())
print(f"Exits: {', '.join(room.exits.keys())}")
if room.items:
print(f"Ye see: {', '.join([item.name for item in room.items])}.")
# NEW: Display monsters
if room.monsters:
print(f"You see a {', '.join([m.name for m in room.monsters])} here.")
def look(self):
logging.debug(f"Player '{self.name}' performed general 'look' action.")
print(f"You are in the {self.current_room.name}.")
print(self.current_room.get_full_description())
if self.current_room.items:
item_names_and_desc = []
for item in self.current_room.items:
item_names_and_desc.append(item.name) # For the quick list, just the name
print(f"You see: {', '.join(item_names_and_desc)}.")
else:
print("There are no items here.")
print(f"Exits: {', '.join(self.current_room.exits.keys())}.")
print(f"Your inventory: {', '.join([item.name for item in self.inventory]) if self.inventory else 'empty'}.")
def examine_item(self, item):
logging.debug(f"Player '{self.name}' examining item: {item.name}.")
# Pass the current room's terrain types to get the context-sensitive description
print(f"\n{item.name}: {item.get_description(self.current_room.terrain_types)}\n")
class Item:
def __init__(self, name, base_description: str, readable: bool = False, read_text: str = "",
terrain_descriptions: Optional[dict] = None, aliases=None,
season_descriptions=None):
self.name = name
self.base_description = base_description
self.readable = readable
self.read_text = read_text
self.terrain_descriptions = terrain_descriptions if terrain_descriptions is not None else {}
# NEW: Dictionary for season-specific item descriptions
self.season_descriptions = season_descriptions if season_descriptions is not None else {}
self.current_terrain_types = [] # This is for current *location* terrain, not item's inherent terrain
self.aliases = set()
self.aliases.add(name.lower())
if aliases:
for alias in aliases:
self.aliases.add(alias.lower())
for word in name.lower().split():
self.aliases.add(word)
logging.debug(f"Item '{self.name}' created with aliases: {self.aliases}.")
def get_description(self, current_terrain_types):
"""Returns the appropriate description based on the item's current terrain types and season."""
# Prioritize inventory description if applicable
if not current_terrain_types and "inventory" in self.terrain_descriptions:
return self.terrain_descriptions["inventory"]
# NEW: Check for season-specific description
current_season = game_clock.get_current_season()
if current_season in self.season_descriptions:
return self.season_descriptions[current_season]
# Then check for specific terrain type descriptions
if Terrain.SNOWY in current_terrain_types and Terrain.SNOWY in self.terrain_descriptions:
return self.terrain_descriptions[Terrain.SNOWY]
if Terrain.FOREST in current_terrain_types and Terrain.FOREST in self.terrain_descriptions:
return self.terrain_descriptions[Terrain.FOREST]
if Terrain.INDOORS_CAVE in current_terrain_types and Terrain.INDOORS_CAVE in self.terrain_descriptions:
return self.terrain_descriptions[Terrain.INDOORS_CAVE]
if Terrain.IN_BUILDING in current_terrain_types and Terrain.IN_BUILDING in self.terrain_descriptions:
return self.terrain_descriptions[Terrain.IN_BUILDING]
# Finally, if OUTDOORS is present and no more specific terrain matched, use it
if Terrain.OUTDOORS in current_terrain_types and Terrain.OUTDOORS in self.terrain_descriptions:
return self.terrain_descriptions[Terrain.OUTDOORS]
# If no specific season or terrain description matches, return the base description
return self.base_description
class Room:
def __init__(self, name, description, transition_message, terrain_types, has_window=False,
season_descriptions=None):
self.name = name
self.base_description = description
self.transition_message = transition_message
self.exits = {}
self.items = []
self.terrain_types = terrain_types
self.has_window = has_window
self.season_descriptions = season_descriptions if season_descriptions is not None else {}
self.monsters = [] # NEW: List to hold monsters in the room
logging.debug(
f"Room '{self.name}' created (terrain: {[t.value for t in self.terrain_types]}, has_window: {self.has_window}).")
def add_exit(self, direction, room):
self.exits[direction] = room
logging.debug(f"Added exit '{direction}' from '{self.name}' to '{room.name}'.")
def add_item(self, item):
self.items.append(item)
item.current_terrain_types = self.terrain_types
logging.debug(f"Added item '{item.name}' to room '{self.name}'.")
# NEW: Methods to add/remove monsters
def add_monster(self, monster):
self.monsters.append(monster)
logging.debug(f"Added monster '{monster.name}' to room '{self.name}'.")
def remove_item(self, item):
if item in self.items:
self.items.remove(item)
item.current_terrain_types = []
logging.debug(f"Removed item '{item.name}' from room '{self.name}'.")
return True
logging.warning(f"Attempted to remove '{item.name}' from '{self.name}' but it wasn't there.")
return False
def remove_monster(self, monster):
if monster in self.monsters:
self.monsters.remove(monster)
logging.debug(f"Removed monster '{monster.name}' from room '{self.name}'.")
return True
logging.warning(f"Attempted to remove '{monster.name}' from '{self.name}' but it wasn't there.")
return False
def get_full_description(self):
current_season = game_clock.get_current_season()
# base description is always shown.
full_desc = [self.base_description]
# if seasonal flavor text exists, append it:
if current_season in self.season_descriptions:
full_desc.append(self.season_descriptions[current_season])
solar_desc = game_clock.get_detailed_solar_description()
if self.has_window:
window_desc = f"\nThrough a nearby window, you see {solar_desc}"
full_desc.append(window_desc)
if self.terrain_types == Terrain.OUTDOORS:
full_desc.append(f"\nIn the sky, you see {solar_desc}")
return " ".join(full_desc)
def get_item_from_room(self, item_name):
"""Helper to get an item from the room by name (string match, not item object)."""
# This iterates through the items in the room and checks if the given
# item_name (lowercase) matches the item's name or any of its aliases.
return next((item for item in self.items if item_name.lower() in item.aliases), None)
# --- 2. Command Interface ---
class Command(abc.ABC):
@abc.abstractmethod
def execute(self):
"""
Base method for command execution.
Subclasses should override this with specific command logic.
"""
pass
# --- 3. Concrete Command Classes ---
# Create this new class alongside your other Command classes
class AttackCommand(Command):
def __init__(self, player, combat_system, target_monster=None):
self.player = player
self.combat_system = combat_system
self.target_monster = target_monster # Will be a Monster object, or None if in combat
def execute(self):
"""Usage: attack [monster_name]
Initiates combat with a monster or attacks the current monster.
Example: 'attack goblin'"""
logging.info(
f"Executing AttackCommand (target: {self.target_monster.name if self.target_monster else 'current_monster'}).")
if self.combat_system.in_combat:
# If already in combat, and the target is the current monster, perform attack
if self.target_monster and self.target_monster == self.combat_system.current_monster:
self.combat_system.player_attack(self.player, self.target_monster)
return True
elif not self.target_monster and self.combat_system.current_monster:
# If no target specified but already in combat, assume attack current monster
self.combat_system.player_attack(self.player, self.combat_system.current_monster)
return True
else:
print(f"You are already fighting the {self.combat_system.current_monster.name}. Focus your attack!")
return False
else:
# Not in combat, try to start it
if self.target_monster:
return self.combat_system.start_combat(self.player, self.target_monster)
else:
print("Attack what? You are not in combat.")
logging.warning("AttackCommand failed: no target and not in combat.")
return False
def menu_handler(title: str, choices: dict):
"""
Generate and handle a menu system.
:param title: str, to which " Menu" is automatically appended
:param choices: dict of {"<letter>": "text"} - EXPECTS LOWERCASE KEYS
:return: choice (the letter of the option), or None if user selected "exit"
"""
header = f"--- {title} Menu ---"
while True:
print(f"\n{header}") # Added newline for readability
for item, text in choices.items():
print(f"{item.upper()}: {text}")
print('-' * len(header))
choice = input("Enter your choice: ").strip().lower() # Convert input to lowercase
if not choice: # Empty input means exit
print("Exiting.")
return None
if choice in choices.keys(): # This check now works because both are lowercase
logging.info("Choice '%s' made" % choice)
return choice
else:
print("Invalid choice.")
class DebugCommand(Command):
def __init__(self, game_clock):
self.game_clock = game_clock
logging.debug("DebugCommand instance created.")
def execute(self):
"""Usage: debug
Opens a debug menu for time, season, and logging level manipulation."""
while True:
logging.info("Executing DebugCommand.")
clock_mode = "Wall clock" if not self.game_clock.is_fixed_time_mode() else "Fixed time"
menu_items = {"c": f"Toggle Game Clock Mode (using {clock_mode})",
"d": "Set Specific Date and Time",
"j": "Jump to Season",
"l": "Change Logging Level",
}
choice = menu_handler(title="Debug", choices=menu_items)
if choice == 'c':
self.toggle_clock_mode()
elif choice == 'd':
self.set_specific_datetime()
elif choice == 'j':
self.jump_to_season()
elif choice == 'l':
self.change_logging_level()
elif choice is None:
# print("Exiting Debug Menu.")
logging.debug("Exited Debug Menu.")
return True # Command completed
def toggle_clock_mode(self):
current_mode = self.game_clock.is_fixed_time_mode()
self.game_clock.set_fixed_time_mode(not current_mode)
def set_specific_datetime(self):
date_params = "YYYY MM DD HH MM SS"
print(f"\nEnter target date and time ({date_params}, e.g., 2025 03 15 14 30 00):")
time_input = input("Date> ").strip().split()
if len(time_input) == 6:
try:
year, month, day, hour, minute, second = map(int, time_input)
self.game_clock.set_datetime(year, month, day, hour, minute, second)
except ValueError:
print(f"Invalid number format. Please use integers for {date_params}.")
else:
print(f"Incorrect number of arguments. Please provide {date_params}.")
def jump_to_season(self):
while True:
choices = {"sp": "Spring",
"su": "Summer",
"a": "Autumn",
"w": "Winter"}
season_abbrreviation = menu_handler("Seasons", choices)
if season_abbrreviation:
season_name = choices[season_abbrreviation]
self.game_clock.jump_to_season(season_name)
return True
elif season_abbrreviation is None:
print("No season change.")
def change_logging_level(self):
choices = {"D": "Debug",
"I": "Info",
"W": "Warning",
"E": "Error",
"C": "Critical",
}
choice = menu_handler("Logging Level", choices)
if choice is None:
print("Logging level unchanged.")
logging.info("Logging level unchanged.")
return True
else:
logging_level = choices[choice].upper()
self.game_clock.change_logging_level(logging_level)
class GoCommand(Command):
def __init__(self, player, direction):
self.player = player
self.direction = direction
logging.debug(f"GoCommand instance created for direction: '{self.direction}'.")
def execute(self):
"""Usage: go <direction>
Move in a cardinal direction (north, south, east, west).
Example: 'go north'"""
logging.info(f"Executing GoCommand for direction: '{self.direction}'.")
target_room = self.player.current_room.exits.get(self.direction)
if target_room:
self.player.move_to(target_room)
game_clock.advance_time(minutes=5)
logging.debug(f"GoCommand successful: moved to {target_room.name}.")
return True
else:
logging.warning(f"GoCommand failed: no exit in direction '{self.direction}'.")
print(f"You can't go {self.direction} from here.")
return False
class TakeCommand(Command):
def __init__(self, player, item): # item is now an Item object
self.player = player
self.item = item # Store the actual Item object
logging.debug(f"TakeCommand instance created for item: '{self.item.name}'.")
def execute(self):
"""Usage: take <item_name>
Pick up an item from the current room.
Example: 'take sword'"""
logging.info(f"Executing TakeCommand for item: '{self.item.name}'.")
# Check if the item is still in the room (e.g., player didn't drop it elsewhere before taking)
if self.item in self.player.current_room.items:
self.player.current_room.remove_item(self.item)
self.player.add_item(self.item)
game_clock.advance_time(minutes=1)
logging.debug(f"TakeCommand successful: {self.item.name} taken.")
return True
else:
logging.warning(f"TakeCommand failed: '{self.item.name}' not found in room (after disambiguation).")
print(f"The {self.item.name} is no longer here.") # Should ideally not happen if logic is tight
return False
class DropCommand(Command):
def __init__(self, player, item): # item is now an Item object
self.player = player
self.item = item
logging.debug(f"DropCommand instance created for item: '{self.item.name}'.")
def execute(self):
"""Usage: drop <item_name>
Drop an item from your inventory into the current room.
Example: 'drop key'"""
logging.info(f"Executing DropCommand for item: '{self.item.name}'.")
if self.item in self.player.inventory: # Check if player still has it
self.player.remove_item(self.item)
self.player.current_room.add_item(self.item)
game_clock.advance_time(minutes=1)
logging.debug(f"DropCommand successful: {self.item.name} dropped.")
return True
else:
logging.warning(f"DropCommand failed: '{self.item.name}' not found in inventory (after disambiguation).")
print(f"You don't have the {self.item.name} anymore.")
return False
class FleeCommand(Command):
def __init__(self, player, combat_system, current_monster=None):
self.player = player
self.combat_system = combat_system
self.current_monster = current_monster # Only relevant if in combat
def execute(self):
"""Usage: flee
Attempts to flee from the current combat encounter."""
logging.info("Executing FleeCommand.")
if self.combat_system.in_combat and self.current_monster:
self.combat_system.flee_combat(self.player, self.current_monster)
return True
else:
print("You are not currently in combat.")
logging.warning("FleeCommand failed: not in combat.")
return False
class LookCommand(Command):
def __init__(self, player, target_object=None): # target_object is now an Item object or None
self.player = player
self.target_object = target_object # Could be Item or None
logging.debug(
f"LookCommand instance created (target: {self.target_object.name if self.target_object else 'Room'}).")
def execute(self):
"""Usage: look [object_name]
Describe your current surroundings and items in the room.
If an object name is provided, describe that specific object (in room or inventory).
Examples: 'look', 'look sword', 'look key'"""
logging.info(f"Executing LookCommand (target: {self.target_object.name if self.target_object else 'Room'}).")
if self.target_object: # If an Item object was passed
self.player.examine_item(self.target_object)
logging.debug(f"LookCommand successful: described item '{self.target_object.name}'.")
else: # No specific object, perform general room look
self.player.look()
logging.debug("LookCommand successful: described current room.")
game_clock.advance_time(minutes=1)
return True
class InventoryCommand(Command):
def __init__(self, player):
self.player = player
logging.debug("InventoryCommand instance created.")
def execute(self):
"""Usage: inventory or inv
List items currently in your inventory."""
logging.info("Executing InventoryCommand.")
if self.player.inventory:
logging.debug(f"InventoryCommand showing inventory: {[item.name for item in self.player.inventory]}.")
print(f"Your inventory:")
for num, item in enumerate(self.player.inventory, start=1):
desc = item.get_description(['inventory']) # force inventory desc
print(f"{num: 2}. {item.name.ljust(20)} {desc}")
else:
logging.debug("InventoryCommand showing empty inventory.")
print("Your inventory is empty.")
game_clock.advance_time(minutes=1)
return True
class ReadCommand(Command):
def __init__(self, player, item): # item is now an Item object
self.player = player
self.item = item
logging.debug(f"ReadCommand instance created for item: '{self.item.name}'.")
def execute(self):
"""Usage: read <item_name>
Read the text content of a readable item in your inventory.
Example: 'read manual'"""
logging.info(f"Executing ReadCommand for item: '{self.item.name}'.")
# Ensure the item is still in player's inventory
if self.item in self.player.inventory:
if self.item.readable:
print(f"\nYou read the {self.item.name}:")
print("---")
print(self.item.read_text)
print("---\n")
game_clock.advance_time(minutes=2)
logging.debug(f"ReadCommand successful: '{self.item.name}' read.")
return True
else:
print(f"You can't read the {self.item.name}.")
logging.warning(f"ReadCommand failed: '{self.item.name}' is not readable.")
return False
else:
print(f"You don't have the {self.item.name} anymore.") # Should ideally not happen if logic is tight
logging.warning(f"ReadCommand failed: '{self.item.name}' not in inventory (after disambiguation).")
return False
class HelpCommand(Command):
def __init__(self, parser_commands, target_command_name=None):
self.parser_commands = parser_commands
self.target_command_name = target_command_name
logging.debug(f"HelpCommand instance created (target: {self.target_command_name or 'All'}).")
def execute(self):
"""Usage: help [command_name]
Display a list of all available commands and their usage,
or provide detailed help for a specific command."""
logging.info(f"Executing HelpCommand (target: {self.target_command_name or 'All'}).")
if self.target_command_name:
command_class = self.parser_commands.get(self.target_command_name)
if command_class:
doc = getattr(command_class.execute, '__doc__', None)
if doc:
print(f"\n--- Help for '{self.target_command_name}' ---")
print(doc.strip())
print("--------------------------------------\n")
logging.debug(f"Help for '{self.target_command_name}' displayed.")
else:
print(f"No detailed help available for '{self.target_command_name}'.")
logging.warning(f"No docstring found for '{self.target_command_name}'.")
else:
print(f"Command '{self.target_command_name}' not recognized. Type 'help' for a list of commands.")
logging.warning(f"Help requested for unrecognized command: '{self.target_command_name}'.")
else:
print("\n--- Available Commands ---")
for verb, cmd_class in sorted(self.parser_commands.items()):
doc = getattr(cmd_class.execute, '__doc__', None)
usage_line = "(No usage info available)"
if doc:
lines = doc.strip().split('\n')
usage_line = lines[0].strip()
print(f"- {verb.ljust(12)}: {usage_line}")
print("--------------------------\n")
logging.debug("All commands help displayed.")
return True
# --- 4. Invoker: The Game Parser ---
class GameParser:
def __init__(self, player):
self.player = player
# NEW: Initialize CombatSystem
self.combat_system = CombatSystem(game_clock) # Will be initialized later in game setup
self.commands = {
"debug": DebugCommand,
"go": GoCommand,
"get": TakeCommand,
"take": TakeCommand,
"drop": DropCommand,
"look": LookCommand,
"inventory": InventoryCommand,
"inv": InventoryCommand,
"read": ReadCommand,
"help": HelpCommand,
"attack": AttackCommand, # NEW
"fight": AttackCommand, # NEW alias for attack
"flee": FleeCommand, # NEW
}
# NEW: Disambiguation state
self.disambiguation_pending = False
self.disambiguation_candidates = [] # List of Item objects
self.pending_command_class = None
self.pending_command_verb = None
logging.info("GameParser initialized.")
def _find_matching_items(self, parsed_object_phrase):
"""
Finds items whose aliases match the parsed object phrase.
Returns a list of matching Item objects.
Prioritizes exact matches of the full phrase, then matches by individual words.
"""
all_interactable_items = self.player.get_all_interactable_items()
matches = []
parsed_words = set(parsed_object_phrase.split()) # Use a set for faster lookups
# --- Phase 1: Exact match of the entire phrase to an item's full name or alias ---
for item in all_interactable_items:
if parsed_object_phrase == item.name.lower():
matches.append(item)
# If there's an exact name match, we heavily prioritize it.
# If multiple items have the exact same name (e.g., "red ball", "red ball"),
# we'd still add both and let disambiguation handle it.
# For now, if we found an exact name, it's a very strong candidate.
elif parsed_object_phrase in item.aliases:
# If the entire phrase is one of the item's aliases, it's also a strong match
matches.append(item)
if len(matches) == 1:
return matches # Unique exact match
elif len(matches) > 1:
# If multiple items have the same exact name/alias, disambiguate those first.
# Example: two items both named "key" (unlikely, but possible in complex games).
# Or if "manual" is an alias for both "Telescope Manual" and "Repair Manual".
return matches
# --- Phase 2: Adjective-Noun (partial word) matching ---
# Only proceed if no strong exact matches were found, or multiple strong matches exist
# and we want to allow partials to potentially add more context.
# This part requires careful tuning. For a simple parser, we'll collect any item
# that has *any* of the words in its aliases, and then let disambiguation sort it out.
# This will lead to more disambiguation prompts, but is safer than guessing.
# Clear matches if we are moving to partial match (so we don't duplicate or mix exact/partial)
# Re-collect all potential items if previous phase didn't yield a single unique match.
matches = []
for item in all_interactable_items:
# Check if all words in the input phrase are present in the item's aliases.
# This is a simple form of "adjective-noun" matching: "red ball" -> item with "red" and "ball"
if all(word in item.aliases for word in parsed_words):
matches.append(item)
# If still no perfect matches, look for any word match as a last resort
if not matches:
for item in all_interactable_items:
if any(word in item.aliases for word in parsed_words):
matches.append(item)
return matches
# NEW: _find_matching_monsters method (similar to _find_matching_items)
def _find_matching_monsters(self, parsed_object_phrase):
"""Finds monsters in the current room whose names match the phrase."""
all_monsters_in_room = self.player.current_room.monsters
matches = []
parsed_words = set(parsed_object_phrase.lower().split())
for monster in all_monsters_in_room:
# Check for exact name match
if parsed_object_phrase == monster.name.lower():
matches.append(monster)
# Check if all words in the phrase are in the monster's name (basic alias for now)
elif all(word in monster.name.lower() for word in parsed_words):
matches.append(monster)
# For simplicity, no disambiguation for monsters yet, just return first match or all if multiple match logic.
# In a real scenario, you'd want disambiguation for monsters too.
# For now, let's just return the first if found or an empty list.
if matches:
return [matches[0]]
return []
def parse_and_execute(self, command_string):
logging.info(f"Parsing user input: '{command_string}'.")
original_command = command_string.strip()
# Check if in combat and awaiting a combat-specific command
if self.combat_system.in_combat:
# Special handling for combat commands when in combat
verb = original_command.lower().split(maxsplit=1)[0]
if verb == "attack":
# In combat, 'attack' just means attack the current monster
command = AttackCommand(self.player, self.combat_system, self.combat_system.current_monster)
command.execute()
return True
elif verb == "flee": # Handle 'flee' during combat
command = FleeCommand(self.player, self.combat_system, self.combat_system.current_monster)
command.execute()
return True
elif verb in ["l", "look", "inventory", "inv", "help"]:
# Allow these commands even in combat
pass # Let normal parsing handle it
else:
print("You are in combat! You can only 'attack', 'flee', 'look/inv/help'.")
logging.warning(f"Invalid command '{verb}' during combat.")
return False
# Handle disambiguation responses (this block remains the same)
if self.disambiguation_pending:
resolved_item = None
response_lower = original_command.lower()
for item in self.disambiguation_candidates:
if response_lower in item.aliases or item.name.lower().startswith(response_lower):
resolved_item = item
break
if resolved_item:
logging.debug(f"Disambiguation resolved to: {resolved_item.name}.")
command_class = self.pending_command_class
command_instance = command_class(self.player, resolved_item) # Most common case is an item
# Special handling if the pending command was for a monster (not yet implemented disambig)
if command_class == AttackCommand: # Need to pass combat_system too
command_instance = command_class(self.player, self.combat_system, resolved_item)
command_instance.execute()
self.disambiguation_pending = False
self.disambiguation_candidates = []
self.pending_command_class = None
self.pending_command_verb = None
self.pending_command_args_for_prompt = None
return True
else:
print("I don't understand which one you mean. Please try again or type the full name.")
logging.warning(f"Disambiguation failed for '{original_command}'.")
return False
# Normal command parsing
parts = original_command.lower().split(maxsplit=1)
verb = parts[0]
args = parts[1] if len(parts) > 1 else None
command_class = self.commands.get(verb)
if command_class:
logging.debug(f"Found command class for verb: '{verb}'.")
command_instance = None
# Handle commands that require an item (take, drop, read) or 'look'
if verb in ["take", "drop", "read"] or (verb == "look" and args):
if not args:
print(f"What do you want to {verb}?")
logging.warning(f"Missing arguments for '{verb}' command.")
return False
# NEW: Special handling for "look me"
if verb == "look" and args == "me":
self.player.describe_self()
game_clock.advance_time(minutes=1)
logging.debug("LookCommand successful: described player.")
return True # Command handled, exit parse_and_execute
# Original item-finding logic follows if not "look me"
matching_items = self._find_matching_items(args)
if len(matching_items) == 1:
item = matching_items[0]
command_instance = command_class(self.player, item)
elif len(matching_items) > 1:
self.disambiguation_pending = True
self.disambiguation_candidates = matching_items
self.pending_command_class = command_class
self.pending_command_verb = verb
self.pending_command_args_for_prompt = args
print(f"Which {args} do you mean? You can choose from: " +
", ".join([item.name for item in matching_items]) + ".")
logging.info(
f"Disambiguation required for '{args}'. Candidates: {[i.name for i in matching_items]}.")
return False
else:
print(f"You don't see or have a '{args}'.")
logging.warning(f"No item found for '{args}' with verb '{verb}'.")
return False
# NEW: Handle Attack/Fight commands
elif verb in ["attack", "fight"]:
if not args:
# If no args given, and not already in combat, what to attack?
print("Attack what?")
logging.warning("AttackCommand failed: no target specified.")
return False
matching_monsters = self._find_matching_monsters(args)
if len(matching_monsters) == 1:
monster = matching_monsters[0]
command_instance = AttackCommand(self.player, self.combat_system, monster)
else:
# For simplicity, if no unique monster found or multiple, print message
print(f"There is no '{args}' to attack here, or you need to be more specific.")
logging.warning(f"AttackCommand failed: no unique monster found for '{args}'.")
return False
# Commands that don't need item disambiguation (go, inventory, help, general look)
elif verb == "debug":
command_instance = command_class(game_clock) # maybe?
elif verb == "go":
if args:
command_instance = command_class(self.player, args)
else:
print("Go where?")
logging.warning("Missing direction for 'go' command.")
return False
elif verb in ["inventory", "inv"]:
command_instance = command_class(self.player)
elif verb == "look": # General 'look' (no args)
command_instance = command_class(self.player, None) # Pass None for target_object_name
elif verb == "help":
command_instance = command_class(self.commands, args)
if command_instance:
logging.debug(f"Attempting to execute command instance: {command_instance.__class__.__name__}.")
command_instance.execute()
logging.debug(f"Command instance {command_instance.__class__.__name__} execution completed.")
else:
logging.error(
"Failed to create command instance due to missing arguments or unrecognized command structure.")
print("Invalid command arguments.")
else:
logging.warning(f"Unrecognized command verb: '{verb}'.")
print("I don't understand that command.")
if __name__ == '__main__':
# https://gist.github.com/Pinacolada64/a28d39ad241c5d9e03a6b9c1c1c54ed0
# --- Game Setup ---
logging.info("Setting up game world...")
player = Player()
# NEW: Initialize CombatSystem after game_clock is available
combat_system = CombatSystem(game_clock)
# Correctly pass combat_system to GameParser now that it's created
parser = GameParser(player) # Parser's __init__ will now get combat_system
logging.info("Game world setup complete.")
# NEW: Create a monster
goblin_loot = Item(
"Goblin Ear",
"A shriveled, green goblin ear. Proof of your victory.",
aliases=["ear", "shriveled ear", "green ear"]
)
goblin = Monster(
"Goblin",
"A short, green-skinned creature with beady red eyes and a rusty dagger.",
health=40,
attack_power=10,
loot_item=goblin_loot
)
# Rooms with their terrain types
forest = Room(
"Forest",
"A dense forest with tall trees. Far off in the distance to the north lies a rocky outcrop.",
"You push through dense foliage, the trees towering above you.",
terrain_types=[Terrain.OUTDOORS, Terrain.FOREST],
season_descriptions={ # NEW
Season.SPRING: "The forest is vibrant with new growth, and wildflowers carpet the ground.",
Season.SUMMER: "The forest is lush and green, the canopy so thick little light penetrates.",
Season.AUTUMN: "The air is crisp, and the forest blazes with red, orange, and gold leaves.",
Season.WINTER: "The forest is silent and stark, with bare branches and a thin layer of frost.",
}
)
cave_entrance = Room(
"Cave Entrance",
"A dark opening is to the east in the side of a mountain. "
"Towards the northwest, snaking up between two high hills, a path beckons. "
"At the end of the path, the glint of glass above a rounded dome is visible.",
"You cautiously approach the ominous maw of the cave.", # Example transition message
terrain_types=[Terrain.OUTDOORS, Terrain.INDOORS_CAVE], # Can be both if it's a transition zone
)
dark_cave = Room(
"Dark Cave",
"It's pitch black in here. You can hear dripping water.",
"The darkness swallows you as you step deeper into the earth.", # Example transition message
terrain_types=[Terrain.INDOORS_CAVE]
)
observatory = Room(
"Small Observatory",
"A cozy, circular room filled with dusty charts and a large telescope.",
"You find yourself inside a small, circular building, the air thick with the scent of old paper.",
# Example transition message
[Terrain.IN_BUILDING],
has_window=True,
season_descriptions={
Season.SUMMER: "The summer sky showcases constellations and planets in the heavens."
}
)
snowy_mountaintop = Room(
"Snowy Mountaintop",
"A windswept peak, covered in a thick blanket of snow.",
"The wind whips around you as you ascend to the snowy summit.", # Example transition message
[Terrain.SNOWY],
season_descriptions={
Season.SUMMER: "Surprisingly, patches of green alpine tundra peek through the melting snow.",
# Winter will likely use the base_description as it's already snowy
Season.SPRING: "The mountaintop is still largely snow-covered, but the air has a hint of thaw.",
Season.AUTUMN: "The mountaintop is brutally cold, and the first heavy snows have arrived."
}
)
# Exits
forest.add_exit("north", cave_entrance)
cave_entrance.add_exit("south", forest)
cave_entrance.add_exit("east", dark_cave)
dark_cave.add_exit("west", cave_entrance)
cave_entrance.add_exit("northwest", observatory)
observatory.add_exit("southeast", cave_entrance)
cave_entrance.add_exit("up", snowy_mountaintop)
snowy_mountaintop.add_exit("down", cave_entrance)
# Place the monster in a room
dark_cave.add_monster(goblin) # Let's put a goblin in the dark cave!
# Items (updated to use terrain_descriptions and aliases)
sword = Item(
"Sword",
"A gleaming, well-balanced steel sword, sharp enough to cut through dense foliage.",
terrain_descriptions={
Terrain.FOREST: "A gleaming sword lies half-hidden among the fallen leaves.",
"inventory": "A sharp, reliable sword."
},
aliases=["blade", "weapon", "steel sword"]
)
key = Item(
"Rusty Key",
"A small, old key, heavily corroded. It looks like it hasn't been used in years.",
terrain_descriptions={
Terrain.INDOORS_CAVE: "A rusty key glints faintly on the damp cave floor."
},
aliases=["key", "old key", "small key"]
)
torch = Item(
"Torch",
"A wooden torch with a resin-soaked tip, ready to be lit.",
terrain_descriptions={
Terrain.INDOORS_CAVE: "A torch lies here, its unlit tip promising light in the gloom."
},
aliases=["light", "torch", "wood torch"]
)
telescope_manual_text = """
--- Telescope Operation Manual ---
1. Power On: Locate the red switch on the base.
2. Alignment: Use the manual cranks to align with desired celestial body.
3. Magnification: Adjust the eyepiece for clarity.
4. Caution: Do not look directly at the sun without proper filters!
----------------------------------
"""
telescope_manual = Item(
"Telescope Manual",
"A weathered manual for operating the telescope, its pages yellowed with age.",
readable=True,
read_text=telescope_manual_text,
terrain_descriptions={
Terrain.IN_BUILDING: "A weathered manual rests on a dusty table, next to the telescope."
},
aliases=["manual", "book", "handbook", "instructions", "guide", "telescope book"]
)
# Create a new item to demonstrate disambiguation:
repair_manual = Item(
"Repair Manual",
"A thick, grease-stained manual for repairing mechanical devices.",
readable=True,
read_text="""
--- Repair Manual Excerpt ---
Chapter 1: Troubleshooting Leaks
Check for corroded seals. Apply sealant generously.
Chapter 2: Gear Lubrication
Use high-viscosity grease for optimal performance.
----------------------------
""",
terrain_descriptions={
Terrain.IN_BUILDING: "A repair manual lies discarded under a workbench."
},
aliases=["manual", "book", "guide", "repair book"] # Shares "manual", "book", "guide" with telescope_manual
)
great_coat = Item(
"Great Coat",
base_description="A heavy, wool coat, perfect for cold weather. It's surprisingly clean.",
terrain_descriptions={
Terrain.SNOWY: "A great coat lies here, half-buried in the snow.",
"inventory": "A great coat. It looks very warm and practical, suitable for any weather.",
Terrain.OUTDOORS: "A great coat lies here, crumpled on the ground.",
Terrain.IN_BUILDING: "A great coat lies here, neatly folded on a chair.",
Terrain.INDOORS_CAVE: "A great coat lies here, damp and forgotten on the cave floor."
},
aliases=["coat", "cloak", "garment", "great cloak"],
season_descriptions={ # NEW
Season.SUMMER: "This great coat feels a bit heavy for the warm weather.",
Season.WINTER: "The great coat looks essential for surviving this bitter cold.",
}
)
# Place items in rooms
forest.add_item(sword)
cave_entrance.add_item(torch)
dark_cave.add_item(key)
observatory.add_item(telescope_manual)
observatory.add_item(repair_manual) # Place the repair manual in the same room
snowy_mountaintop.add_item(great_coat)
# --- Game Loop ---
print("\n--- Welcome to the Text Adventure! ---")
print("Type 'help' for commands, 'help <command>' for specific info, 'quit' to exit.")
player.move_to(forest)
while True:
try:
current_game_time = game_clock.get_current_datetime().strftime("%m/%d/%Y %H:%M:%S")
print(f"\n[Game Time: {current_game_time}]")
user_input = input("> ").strip()
if user_input.lower() == "quit":
logging.info("User requested to quit. Exiting game.")
print("Thanks for playing!")
break
parser.parse_and_execute(user_input)
except Exception as e:
logging.critical(f"An unhandled error occurred in the game loop: {e}", exc_info=True)
print(f"An unexpected error occurred: {e}. Please report this!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment