|
# Single file Text Adventure Game: Procedural Worlds & Rule-Based Foes |
|
# Standard Library Only - Python Implementation |
|
|
|
import random |
|
import sys |
|
import textwrap # For wrapping long descriptions |
|
import time # Optional: for slight pauses |
|
|
|
# --- Constants --- |
|
DIRECTIONS = ["north", "south", "east", "west"] # Basic directions |
|
MAX_ROOMS = 7 # Minimum 5 required, using 7 for a bit more space |
|
WIN_CONDITION_ENEMY_TYPE = "Dragon" # Type of enemy to defeat to win |
|
PLAYER_BASE_HEALTH = 50 |
|
PLAYER_BASE_ATTACK = 5 |
|
|
|
# --- Helper Functions --- |
|
def wrap_text(text, width=70): |
|
"""Wraps text for better display.""" |
|
return "\n".join(textwrap.wrap(text, width)) |
|
|
|
def get_opposite_direction(direction): |
|
"""Gets the opposite direction.""" |
|
opposites = {"north": "south", "south": "north", "east": "west", "west": "east"} |
|
return opposites.get(direction) |
|
|
|
# --- Class Definitions --- |
|
|
|
class Item: |
|
"""Represents an item in the game.""" |
|
def __init__(self, name, description, effect_type, effect_value=0): |
|
self.name = name |
|
self.description = description |
|
# effect_type: 'heal', 'weapon', 'boost_attack', 'junk', 'key' (optional) |
|
self.effect_type = effect_type |
|
# effect_value: amount healed, damage dealt/bonus, attack increase, etc. |
|
self.effect_value = effect_value |
|
|
|
def __str__(self): |
|
return self.name |
|
|
|
class Character: |
|
"""Base class for Player and Enemy.""" |
|
def __init__(self, name, description, health, attack, current_room_id): |
|
self.name = name |
|
self.description = description |
|
self.max_health = health |
|
self.health = health |
|
self.attack_power = attack |
|
self.current_room_id = current_room_id # ID of the room the character is in |
|
|
|
def is_alive(self): |
|
"""Checks if the character's health is above 0.""" |
|
return self.health > 0 |
|
|
|
def take_damage(self, damage): |
|
"""Reduces character health by the damage amount.""" |
|
self.health -= damage |
|
if self.health < 0: |
|
self.health = 0 |
|
print(f"{self.name} takes {damage} damage! Remaining health: {self.health}/{self.max_health}") |
|
|
|
def attack(self, target): |
|
"""Performs an attack on the target character.""" |
|
# Add a bit of randomness to damage for variability |
|
damage = max(1, self.attack_power + random.randint(-1, 2)) |
|
print(f"{self.name} attacks {target.name}!") |
|
# Optional: Add a small pause for readability |
|
# time.sleep(0.5) |
|
target.take_damage(damage) |
|
|
|
|
|
class Player(Character): |
|
"""Represents the player character.""" |
|
def __init__(self, current_room_id): |
|
super().__init__("Player", "It's you!", health=PLAYER_BASE_HEALTH, attack=PLAYER_BASE_ATTACK, current_room_id=current_room_id) |
|
self.inventory = [] |
|
self.base_attack_power = PLAYER_BASE_ATTACK |
|
self.equipped_weapon = None |
|
|
|
def add_item(self, item): |
|
"""Adds an item to the player's inventory and handles weapon equipping.""" |
|
self.inventory.append(item) |
|
print(f"You pick up the {item.name}.") |
|
# Simple auto-equip best weapon logic |
|
if item.effect_type == 'weapon': |
|
if self.equipped_weapon is None or item.effect_value > self.equipped_weapon.effect_value: |
|
self.equip_weapon(item) |
|
|
|
def equip_weapon(self, weapon_item): |
|
"""Equips a weapon item, updating attack power.""" |
|
if weapon_item.effect_type == 'weapon': |
|
# If replacing, show message maybe? For simplicity, just equip. |
|
self.equipped_weapon = weapon_item |
|
# Attack power is base + weapon bonus |
|
self.attack_power = self.base_attack_power + weapon_item.effect_value |
|
print(f"You equip the {weapon_item.name}. Your attack is now {self.attack_power}.") |
|
else: |
|
print(f"{weapon_item.name} is not a weapon.") |
|
|
|
def use_item(self, item_name, game_state): |
|
"""Uses an item from the inventory, applying its effect.""" |
|
item_to_use = None |
|
item_index = -1 |
|
# Find the item by name (case-insensitive) |
|
for i, item in enumerate(self.inventory): |
|
if item.name.lower() == item_name.lower(): |
|
item_to_use = item |
|
item_index = i |
|
break |
|
|
|
if not item_to_use: |
|
print(f"You don't have '{item_name}'.") |
|
return False # Indicate item not used |
|
|
|
effect = item_to_use.effect_type |
|
value = item_to_use.effect_value |
|
used = False |
|
|
|
if effect == 'heal': |
|
heal_amount = min(value, self.max_health - self.health) # Can't heal above max |
|
if heal_amount > 0: |
|
self.health += heal_amount |
|
print(f"You use the {item_to_use.name} and restore {heal_amount} HP. Current health: {self.health}/{self.max_health}") |
|
used = True |
|
else: |
|
print("Your health is already full.") |
|
elif effect == 'boost_attack': # Permanent boost |
|
self.base_attack_power += value |
|
# Recalculate total attack power with equipped weapon |
|
self.attack_power = self.base_attack_power + (self.equipped_weapon.effect_value if self.equipped_weapon else 0) |
|
print(f"You consume the {item_to_use.name}. Your base attack permanently increases by {value}! Current attack: {self.attack_power}") |
|
used = True |
|
# Add other effects like 'key' if implementing puzzles |
|
elif effect == 'weapon': |
|
print(f"You wield the {item_to_use.name}. Use 'inventory' to see stats. Items are usually equipped automatically on pickup if better.") |
|
# Allow explicit equipping via 'use' ? Could be complex. Auto-equip is simpler. |
|
elif effect == 'junk': |
|
print(f"The {item_to_use.name} doesn't seem to do anything useful.") |
|
# Don't consume junk items, maybe? Or maybe using it reveals it's junk. |
|
used = False # Keep it simple: doesn't consume, does nothing. |
|
else: |
|
print(f"You can't figure out how to use the {item_to_use.name} right now.") |
|
|
|
# Remove consumable items from inventory after use |
|
if used and item_to_use.effect_type != 'weapon': # Weapons are equipped, not consumed by 'use' |
|
del self.inventory[item_index] |
|
|
|
return used # Return whether item was successfully used and consumed |
|
|
|
def show_inventory(self): |
|
"""Displays the player's current inventory and equipped weapon.""" |
|
if not self.inventory: |
|
print("Your inventory is empty.") |
|
else: |
|
print("Inventory:") |
|
for item in self.inventory: |
|
# Show item value for weapons and healing items |
|
details = "" |
|
if item.effect_type == 'weapon': |
|
details = f"(+{item.effect_value} Atk)" |
|
elif item.effect_type == 'heal': |
|
details = f"(+{item.effect_value} HP)" |
|
elif item.effect_type == 'boost_attack': |
|
details = f"(+{item.effect_value} Base Atk)" |
|
|
|
print(f"- {item.name} {details} [{item.description}]") |
|
|
|
if self.equipped_weapon: |
|
print(f"Equipped Weapon: {self.equipped_weapon.name} (+{self.equipped_weapon.effect_value} Attack)") |
|
else: |
|
print("Equipped Weapon: None") |
|
|
|
|
|
class Enemy(Character): |
|
"""Represents an enemy character with AI.""" |
|
def __init__(self, name, description, health, attack, current_room_id, ai_type, enemy_id): |
|
super().__init__(name, description, health, attack, current_room_id) |
|
self.ai_type = ai_type # e.g., 'goblin', 'orc', 'spider', 'dragon' |
|
self.id = enemy_id # Unique ID for tracking |
|
|
|
class Room: |
|
"""Represents a location (room) in the game world.""" |
|
def __init__(self, room_id, name, description): |
|
self.id = room_id |
|
self.name = name |
|
self.description = description |
|
self.exits = {} # Mapping: {"north": room_id, "south": room_id, ...} |
|
self.items = [] # List of Item objects currently in this room |
|
self.enemy_ids = [] # List of enemy IDs currently in this room |
|
|
|
def add_exit(self, direction, adjacent_room_id): |
|
"""Adds a one-way exit from this room.""" |
|
self.exits[direction] = adjacent_room_id |
|
|
|
def link_rooms(self, direction, adjacent_room): |
|
"""Creates a two-way link between this room and an adjacent one.""" |
|
opposite = get_opposite_direction(direction) |
|
if opposite: |
|
self.add_exit(direction, adjacent_room.id) |
|
adjacent_room.add_exit(opposite, self.id) |
|
else: |
|
print(f"Warning: Invalid direction '{direction}' for linking rooms.") |
|
|
|
def describe(self, game_state): |
|
"""Prints a description of the room, including items, enemies, and exits.""" |
|
print("\n" + "=" * len(self.name)) |
|
print(self.name) |
|
print("=" * len(self.name)) |
|
print(wrap_text(self.description)) |
|
|
|
# Show items |
|
if self.items: |
|
print("\nYou see the following items:") |
|
for item in self.items: |
|
print(f"- {item.name}") |
|
else: |
|
# Optional: Add flavor text for empty rooms |
|
# print("\nThe room seems empty of useful items.") |
|
pass |
|
|
|
# Show enemies |
|
living_enemies = [game_state.enemies[eid] for eid in self.enemy_ids if game_state.enemies[eid].is_alive()] |
|
if living_enemies: |
|
print("\nDanger! Enemies present:") |
|
for enemy in living_enemies: |
|
print(f"- {enemy.name} ({enemy.description}) [{enemy.health}/{enemy.max_health} HP]") |
|
# Optional: Add flavor text if no enemies |
|
# else: |
|
# print("\nYou see no immediate threats.") |
|
|
|
|
|
# Show exits |
|
if self.exits: |
|
print("\nAvailable Exits:") |
|
print(", ".join([d.capitalize() for d in self.exits.keys()])) |
|
else: |
|
print("\nThere are no obvious exits from here.") |
|
|
|
|
|
class GameState: |
|
"""Holds the entire state of the game, including rooms, entities, and status.""" |
|
def __init__(self): |
|
self.rooms = {} # Dictionary mapping {room_id: Room object} |
|
self.enemies = {} # Dictionary mapping {enemy_id: Enemy object} |
|
self.player = None |
|
self.game_over = False |
|
self.player_won = False |
|
self.enemy_id_counter = 0 # Simple counter for unique enemy IDs |
|
|
|
def get_room(self, room_id): |
|
"""Safely retrieves a room object by its ID.""" |
|
return self.rooms.get(room_id) |
|
|
|
def get_current_room(self): |
|
"""Gets the Room object where the player currently is.""" |
|
if self.player: |
|
return self.get_room(self.player.current_room_id) |
|
return None |
|
|
|
def add_enemy_to_room(self, enemy_id, room_id): |
|
"""Adds an enemy ID to a room's list and updates the enemy's location.""" |
|
enemy = self.enemies.get(enemy_id) |
|
room = self.get_room(room_id) |
|
if enemy and room: |
|
if enemy_id not in room.enemy_ids: |
|
room.enemy_ids.append(enemy_id) |
|
enemy.current_room_id = room_id # Ensure enemy object knows its room |
|
|
|
def remove_enemy_from_room(self, enemy_id, room_id): |
|
"""Removes an enemy ID from a room's list.""" |
|
room = self.get_room(room_id) |
|
if room and enemy_id in room.enemy_ids: |
|
try: |
|
room.enemy_ids.remove(enemy_id) |
|
except ValueError: |
|
# Should not happen if check passes, but good practice |
|
pass # Enemy already removed |
|
|
|
|
|
# --- Procedural Generation --- |
|
|
|
def generate_world(game_state): |
|
"""Generates the game world: rooms, items, enemies based on constants.""" |
|
print("Generating world...") |
|
|
|
# 1. Generate Rooms (ensure at least 5) |
|
num_rooms = max(5, MAX_ROOMS) |
|
room_ids = list(range(num_rooms)) |
|
for i in room_ids: |
|
game_state.rooms[i] = generate_room(i) |
|
|
|
# 2. Create Layout/Connections |
|
# Simple strategy: Try to connect rooms sequentially, then add a few random links. |
|
for i in range(num_rooms - 1): |
|
room1 = game_state.get_room(i) |
|
room2 = game_state.get_room(i+1) |
|
|
|
# Try to find an available direction pair |
|
possible_dirs = list(DIRECTIONS) |
|
random.shuffle(possible_dirs) |
|
connected = False |
|
for d1 in possible_dirs: |
|
d2 = get_opposite_direction(d1) |
|
# Check if both exits are free in the respective rooms |
|
if d1 not in room1.exits and d2 not in room2.exits: |
|
room1.link_rooms(d1, room2) |
|
# print(f"DEBUG: Linked Room {i} ({d1}) <-> Room {i+1} ({d2})") # Debug |
|
connected = True |
|
break |
|
if not connected: |
|
# Fallback: If no ideal pair found (e.g., room full), just force a one-way link if possible |
|
# This might create dead ends or non-reciprocal paths, adding challenge/quirkiness |
|
for d1 in possible_dirs: |
|
if d1 not in room1.exits: |
|
room1.add_exit(d1, room2.id) |
|
# print(f"DEBUG: Forced link Room {i} --({d1})--> Room {i+1}") # Debug |
|
break |
|
|
|
|
|
# Add a couple of random extra connections for complexity (potential loops/shortcuts) |
|
extra_links = num_rooms // 3 |
|
for _ in range(extra_links): |
|
r_id1, r_id2 = random.sample(room_ids, 2) # Pick two distinct rooms |
|
room1 = game_state.get_room(r_id1) |
|
room2 = game_state.get_room(r_id2) |
|
|
|
possible_dirs = list(DIRECTIONS) |
|
random.shuffle(possible_dirs) |
|
linked = False |
|
for d1 in possible_dirs: |
|
d2 = get_opposite_direction(d1) |
|
# Check if exits are free and they aren't already linked directly |
|
if d1 not in room1.exits and d2 not in room2.exits and room1.exits.get(d1) != r_id2: |
|
room1.link_rooms(d1, room2) |
|
# print(f"DEBUG: Added extra link Room {r_id1} ({d1}) <-> Room {r_id2} ({d2})") # Debug |
|
linked = True |
|
break |
|
# If linking fails, just skip (world remains connected anyway) |
|
|
|
|
|
# 3. Generate and Place Items (at least 3 types) |
|
item_templates = [ |
|
# (Name, Description, Type, Value) |
|
("Healing Potion", "A fizzing green liquid. Restores health.", "heal", 15), |
|
("Large Healing Potion", "A potent blue potion. Restores more health.", "heal", 30), |
|
("Rusty Sword", "Barely holds an edge.", "weapon", 2), |
|
("Iron Sword", "A standard, reliable blade.", "weapon", 4), |
|
("Steel Longsword", "A well-crafted weapon.", "weapon", 6), |
|
("Strength Elixir", "A thick, metallic tasting liquid.", "boost_attack", 1), |
|
("Mysterious Orb", "It hums faintly.", "junk", 0), |
|
("Tattered Scroll", "Illegible magical script.", "junk", 0), |
|
] |
|
# Ensure at least 3 *types* are possible (heal, weapon, junk/boost) - guaranteed by templates |
|
num_items_to_place = num_rooms + random.randint(0, num_rooms // 2) # Place a decent number of items |
|
|
|
# Ensure at least one healing and one weapon exists somewhere |
|
placed_heal = False |
|
placed_weapon = False |
|
|
|
for i in range(num_items_to_place): |
|
template = random.choice(item_templates) |
|
# Force placement of needed types if missing near the end |
|
if i >= num_items_to_place - 2: |
|
if not placed_heal: |
|
template = next(t for t in item_templates if t[2] == 'heal') |
|
elif not placed_weapon: |
|
template = next(t for t in item_templates if t[2] == 'weapon') |
|
|
|
item = Item(*template) |
|
# Place in a random room (avoiding start room 0 maybe?) |
|
room_id = random.choice(room_ids[1:]) if num_rooms > 1 else 0 |
|
game_state.get_room(room_id).items.append(item) |
|
# print(f"DEBUG: Placed {item.name} in Room {room_id}") # Debug |
|
|
|
if item.effect_type == 'heal': placed_heal = True |
|
if item.effect_type == 'weapon': placed_weapon = True |
|
|
|
# If somehow still missing vital types (unlikely), add them to start room |
|
if not placed_heal: game_state.get_room(0).items.append(Item(*next(t for t in item_templates if t[2] == 'heal'))) |
|
if not placed_weapon: game_state.get_room(0).items.append(Item(*next(t for t in item_templates if t[2] == 'weapon'))) |
|
|
|
|
|
# 4. Generate and Place Enemies (at least 2 types + Boss) |
|
enemy_templates = [ |
|
# (Name, Description, Health Range, Attack Range, AI Type) |
|
("Goblin Sneak", "Small, quick, and vicious.", (10, 15), (2, 4), "goblin"), |
|
("Orc Grunt", "Muscular and wielding a crude axe.", (20, 30), (4, 6), "orc"), |
|
("Giant Spider", "Clicks its mandibles menacingly.", (15, 22), (3, 5), "spider"), |
|
# Ensure boss template exists |
|
(WIN_CONDITION_ENEMY_TYPE, "A colossal beast wreathed in smoke!", (60, 80), (8, 12), "dragon"), |
|
] |
|
# Ensure at least 2 normal types + boss type are defined |
|
normal_enemy_templates = [t for t in enemy_templates if t[4] != 'dragon'] |
|
boss_template = next(t for t in enemy_templates if t[4] == 'dragon') |
|
|
|
num_enemies_to_place = num_rooms // 2 + random.randint(1, 3) # Adjust density as needed |
|
|
|
# Place the boss in the last room (or a random distant room) |
|
boss_room_id = num_rooms - 1 |
|
name, desc, health_r, attack_r, ai_type = boss_template |
|
health = random.randint(*health_r) |
|
attack = random.randint(*attack_r) |
|
enemy_id = game_state.enemy_id_counter |
|
game_state.enemy_id_counter += 1 |
|
boss = Enemy(name, desc, health, attack, boss_room_id, ai_type, enemy_id) |
|
game_state.enemies[enemy_id] = boss |
|
game_state.add_enemy_to_room(enemy_id, boss_room_id) |
|
# print(f"DEBUG: Placed BOSS {boss.name} in Room {boss_room_id}") # Debug |
|
|
|
# Place other enemies |
|
for _ in range(num_enemies_to_place - 1): # -1 because boss is already placed |
|
template = random.choice(normal_enemy_templates) |
|
name, desc, health_r, attack_r, ai_type = template |
|
health = random.randint(*health_r) |
|
attack = random.randint(*attack_r) |
|
enemy_id = game_state.enemy_id_counter |
|
game_state.enemy_id_counter += 1 |
|
|
|
# Place in random room, avoiding start room (0) and boss room |
|
possible_room_ids = [rid for rid in room_ids if rid != 0 and rid != boss_room_id] |
|
if not possible_room_ids: possible_room_ids = [0] # Fallback if only start/boss room exist |
|
room_id = random.choice(possible_room_ids) |
|
|
|
enemy = Enemy(name, desc, health, attack, room_id, ai_type, enemy_id) |
|
game_state.enemies[enemy_id] = enemy |
|
game_state.add_enemy_to_room(enemy_id, room_id) |
|
# print(f"DEBUG: Placed {enemy.name} in Room {room_id}") # Debug |
|
|
|
|
|
# 5. Create and Place Player |
|
start_room_id = 0 |
|
game_state.player = Player(start_room_id) |
|
|
|
print("World generation complete.") |
|
|
|
|
|
def generate_room(room_id): |
|
"""Generates a single room with a procedural name and description.""" |
|
# Templates for generation |
|
environments = ["Cavern", "Forest Glade", "Ruined Chamber", "Dusty Corridor", "Dank Cellar", "Overgrown Path", "Stone Hall", "Musty Library", "Chapel Ruins"] |
|
adj_primary = ["Dimly lit", "Echoing", "Ancient", "Eerie", "Cold", "Damp", "Shadowy", "Crumbling", "Vast", "Narrow", "Quiet", "Drafty"] |
|
adj_secondary = [" unsettling", " mysterious", " forgotten", " foreboding", " strangely peaceful", " unnervingly silent", " filled with debris", " surprisingly ornate"] |
|
details = [ |
|
"Water drips rhythmically from unseen heights.", |
|
"Strange symbols cover the walls, faded with time.", |
|
"The air is thick with the smell of dust and decay.", |
|
"You hear faint rustling sounds from the corners.", |
|
"A cool breeze whispers through cracks in the stone.", |
|
"Broken furniture lies overturned and decaying.", |
|
"Flickering torchlight (or bioluminescence?) casts dancing shadows.", |
|
"The floor is uneven and treacherous.", |
|
"Cobwebs hang like macabre decorations.", |
|
"A faint, unpleasant odor hangs in the air." |
|
] |
|
|
|
env = random.choice(environments) |
|
adj1 = random.choice(adj_primary) |
|
adj2 = random.choice(adj_secondary) |
|
detail1 = random.choice(details) |
|
detail2 = random.choice(details) |
|
while detail1 == detail2: # Ensure different details |
|
detail2 = random.choice(details) |
|
|
|
name = f"Room {room_id}: {adj1} {env}" |
|
description = f"You stand in a {adj1.lower()} {env.lower()} that feels{adj2}. {detail1} {detail2}" |
|
|
|
return Room(room_id, name, description) |
|
|
|
|
|
# --- AI Logic --- |
|
|
|
def run_ai_turn(game_state): |
|
"""Processes AI actions for all living enemies in the game.""" |
|
player_room_id = game_state.player.current_room_id |
|
# Process a copy of IDs, as movement might change dict/lists during iteration |
|
# Only process enemies that are currently alive |
|
enemy_ids_to_process = [eid for eid, enemy in game_state.enemies.items() if enemy.is_alive()] |
|
|
|
if not enemy_ids_to_process: |
|
return # No living enemies left |
|
|
|
# print("--- AI Turn ---") # Optional debug message |
|
for enemy_id in enemy_ids_to_process: |
|
enemy = game_state.enemies.get(enemy_id) |
|
# Double check if enemy still exists and is alive (could be defeated mid-turn by multi-attack?) |
|
if not enemy or not enemy.is_alive(): |
|
continue |
|
|
|
current_room = game_state.get_room(enemy.current_room_id) |
|
if not current_room: continue # Should not happen normally |
|
|
|
# Determine if player is in the same room as the enemy |
|
player_in_room = enemy.current_room_id == player_room_id |
|
|
|
# --- Apply AI rules based on enemy type --- |
|
acted = False # Track if the enemy performed an action |
|
if enemy.ai_type == "goblin": |
|
acted = ai_goblin(enemy, game_state, player_in_room, current_room) |
|
elif enemy.ai_type == "orc": |
|
acted = ai_orc(enemy, game_state, player_in_room, current_room) |
|
elif enemy.ai_type == "spider": |
|
acted = ai_spider(enemy, game_state, player_in_room, current_room) |
|
elif enemy.ai_type == "dragon": |
|
acted = ai_dragon(enemy, game_state, player_in_room, current_room) |
|
# Add more AI types here... |
|
|
|
# Check if player was defeated by this enemy's action |
|
if not game_state.player.is_alive(): |
|
game_state.game_over = True |
|
return # Stop AI processing if player is dead |
|
|
|
|
|
# --- Specific AI Type Implementations --- |
|
|
|
def ai_goblin(enemy, game_state, player_in_room, current_room): |
|
"""Rule-based AI for Goblins: Aggressive, but may flee when low health.""" |
|
if player_in_room: |
|
# Rule 1: If health < 30% and random chance, try to flee. |
|
if enemy.health < enemy.max_health * 0.3 and random.random() < 0.4: # 40% chance to flee |
|
if flee(enemy, game_state, current_room): |
|
print(f"The cowardly {enemy.name} tries to flee!") |
|
return True # Action taken: fled |
|
|
|
# Rule 2: Otherwise, attack the player. |
|
enemy.attack(game_state.player) |
|
return True # Action taken: attacked |
|
else: |
|
# Rule 3: If player not present, small chance to patrol randomly. |
|
if random.random() < 0.25: # 25% chance to move |
|
if move_randomly(enemy, game_state, current_room): |
|
# Optional: print(f"You hear scurrying sounds nearby.") # Player might hear movement |
|
return True # Action taken: moved |
|
return False # No action taken |
|
|
|
def ai_orc(enemy, game_state, player_in_room, current_room): |
|
"""Rule-based AI for Orcs: Aggressive, less likely to flee, may patrol.""" |
|
if player_in_room: |
|
# Rule 1: Orcs are tough, low chance to flee only if near death. |
|
if enemy.health < enemy.max_health * 0.15 and random.random() < 0.1: # 10% chance if very low HP |
|
if flee(enemy, game_state, current_room): |
|
print(f"The wounded {enemy.name} attempts a clumsy retreat!") |
|
return True |
|
|
|
# Rule 2: Always attack player if present and didn't flee. |
|
enemy.attack(game_state.player) |
|
return True |
|
else: |
|
# Rule 3: Lower chance to patrol compared to goblins. They are less active. |
|
if random.random() < 0.1: # 10% chance to move |
|
if move_randomly(enemy, game_state, current_room): |
|
# Optional: print(f"You hear heavy footsteps fading away.") |
|
return True |
|
return False |
|
|
|
def ai_spider(enemy, game_state, player_in_room, current_room): |
|
"""Rule-based AI for Spiders: Territorial ambush predators.""" |
|
if player_in_room: |
|
# Rule 1: Always attack if player is in the room. |
|
enemy.attack(game_state.player) |
|
return True |
|
# Rule 2: Spiders generally stay put, guarding their territory (no patrol). |
|
# Optional enhancement: Could add logic to chase if player *just* left the room. |
|
return False |
|
|
|
def ai_dragon(enemy, game_state, player_in_room, current_room): |
|
"""Rule-based AI for the Dragon Boss: Powerful, stationary guardian.""" |
|
if player_in_room: |
|
# Rule 1: Always attack the player with great force. |
|
print(f"The mighty {enemy.name} roars and attacks!") |
|
enemy.attack(game_state.player) |
|
# Maybe add a special attack chance? (Optional enhancement) |
|
# if random.random() < 0.2: print("It breathes fire!") # Flavor |
|
return True |
|
# Rule 2: The Dragon never leaves its lair. |
|
return False |
|
|
|
# --- AI Helper Actions --- |
|
|
|
def flee(enemy, game_state, current_room): |
|
"""Enemy attempts to move to a random adjacent room NOT containing the player.""" |
|
player_room_id = game_state.player.current_room_id |
|
possible_exits = list(current_room.exits.items()) |
|
safe_exits = [(d, rid) for d, rid in possible_exits if rid != player_room_id] |
|
|
|
# Prefer fleeing to a room without the player |
|
target_exits = safe_exits if safe_exits else possible_exits # Flee anywhere if no safe exit |
|
|
|
if target_exits: |
|
direction, new_room_id = random.choice(target_exits) |
|
return move_enemy(enemy, new_room_id, game_state) |
|
return False # Cannot flee (no exits or only exit leads to player) |
|
|
|
def move_randomly(enemy, game_state, current_room): |
|
"""Enemy moves to a random adjacent connected room.""" |
|
possible_exits = list(current_room.exits.values()) |
|
if possible_exits: |
|
new_room_id = random.choice(possible_exits) |
|
return move_enemy(enemy, new_room_id, game_state) |
|
return False # Cannot move (no exits) |
|
|
|
def move_enemy(enemy, new_room_id, game_state): |
|
"""Moves an enemy entity from its current room to a new room ID.""" |
|
old_room_id = enemy.current_room_id |
|
# Update room lists |
|
game_state.remove_enemy_from_room(enemy.id, old_room_id) |
|
game_state.add_enemy_to_room(enemy.id, new_room_id) |
|
# print(f"DEBUG: AI moved {enemy.name} from room {old_room_id} to {new_room_id}") # Debug message |
|
return True |
|
|
|
|
|
# --- Command Parsing and Execution --- |
|
|
|
def parse_command(input_str): |
|
"""Parses player input string into command and list of arguments.""" |
|
parts = input_str.lower().strip().split() |
|
if not parts: |
|
return None, [] # Return None command if input is empty |
|
command = parts[0] |
|
args = parts[1:] |
|
return command, args |
|
|
|
def execute_command(game_state, command, args): |
|
"""Executes the parsed player command, modifying game_state.""" |
|
player = game_state.player |
|
current_room = game_state.get_current_room() |
|
|
|
if not current_room: |
|
print("Error: Player is in an invalid room.") |
|
game_state.game_over = True |
|
return False # Indicate turn should not proceed |
|
|
|
action_taken = True # Assume player action takes a turn unless specified otherwise |
|
|
|
# --- Handle Commands --- |
|
if command == "help": |
|
print("\n--- Available Commands ---") |
|
print(" go [direction] - Move north, south, east, or west.") |
|
print(" look - Describe the current room and its contents.") |
|
print(" take [item] - Pick up an item from the room.") |
|
print(" inventory (i) - Show your inventory and equipped items.") |
|
print(" use [item] - Use an item from your inventory.") |
|
print(" attack [enemy] - Attack an enemy in the current room.") |
|
print(" status - Show your current health and stats.") |
|
print(" help - Show this help message again.") |
|
print(" quit - Exit the game.") |
|
action_taken = False # Viewing help doesn't take a turn |
|
|
|
elif command == "quit": |
|
print("Are you sure you want to quit? (yes/no)") |
|
confirm = input("> ").lower().strip() |
|
if confirm == 'yes': |
|
print("Quitting game. Goodbye!") |
|
game_state.game_over = True |
|
else: |
|
print("Quit cancelled.") |
|
action_taken = False # Cancelling quit doesn't take a turn |
|
|
|
elif command == "look": |
|
current_room.describe(game_state) |
|
action_taken = False # Looking around doesn't usually take a game turn |
|
|
|
elif command == "status": |
|
print("\n--- Player Status ---") |
|
print(f"Health: {player.health}/{player.max_health}") |
|
print(f"Base Attack: {player.base_attack_power}") |
|
print(f"Total Attack: {player.attack_power}") |
|
player.show_inventory() |
|
action_taken = False # Checking status doesn't take a turn |
|
|
|
elif command == "go": |
|
if not args: |
|
print("Go where? (Specify a direction like 'go north')") |
|
action_taken = False |
|
else: |
|
direction = args[0] |
|
if direction not in DIRECTIONS: |
|
print(f"Unknown direction '{direction}'. Try: {', '.join(DIRECTIONS)}.") |
|
action_taken = False |
|
else: |
|
next_room_id = current_room.exits.get(direction) |
|
if next_room_id is not None: |
|
player.current_room_id = next_room_id |
|
print(f"\nYou move {direction}.") |
|
# Describe the new room immediately after moving |
|
game_state.get_current_room().describe(game_state) |
|
else: |
|
print("You bump into a wall. You can't go that way.") |
|
action_taken = False # Failed move doesn't take a turn |
|
|
|
elif command == "take": |
|
if not args: |
|
print("Take what?") |
|
action_taken = False |
|
else: |
|
item_name_query = " ".join(args) |
|
item_to_take = None |
|
item_index = -1 |
|
|
|
# Find item by exact or partial match (prefer exact) |
|
for i, item in enumerate(current_room.items): |
|
if item.name.lower() == item_name_query: |
|
item_to_take = item |
|
item_index = i |
|
break |
|
# If no exact match, try partial match (e.g., "take potion") |
|
if not item_to_take: |
|
found_partial = [] |
|
for i, item in enumerate(current_room.items): |
|
if item_name_query in item.name.lower(): |
|
found_partial.append((item, i)) |
|
if len(found_partial) == 1: |
|
item_to_take, item_index = found_partial[0] |
|
elif len(found_partial) > 1: |
|
print(f"Which '{item_name_query}' did you mean?") |
|
for j, (item, _) in enumerate(found_partial): print(f" {j+1}. {item.name}") |
|
# Basic choice handling - can be expanded |
|
action_taken = False # Taking requires clarification |
|
# Else: No partial match found either |
|
|
|
if item_to_take: |
|
player.add_item(item_to_take) |
|
del current_room.items[item_index] # Remove item from room |
|
else: |
|
print(f"There is no '{item_name_query}' here.") |
|
action_taken = False # Failed take doesn't take a turn |
|
|
|
elif command in ["inventory", "i"]: |
|
player.show_inventory() |
|
action_taken = False # Checking inventory doesn't take a turn |
|
|
|
elif command == "use": |
|
if not args: |
|
print("Use what?") |
|
action_taken = False |
|
else: |
|
item_name_query = " ".join(args) |
|
# player.use_item returns True if item was used+consumed, False otherwise |
|
action_taken = player.use_item(item_name_query, game_state) |
|
|
|
elif command == "attack": |
|
if not args: |
|
print("Attack what?") |
|
action_taken = False |
|
else: |
|
enemy_name_query = " ".join(args) # Allow multi-word enemy names |
|
target_enemy = None |
|
|
|
# Get living enemies in the current room |
|
living_enemies = {eid: game_state.enemies[eid] for eid in current_room.enemy_ids if game_state.enemies[eid].is_alive()} |
|
|
|
if not living_enemies: |
|
print("There are no enemies here to attack.") |
|
action_taken = False |
|
else: |
|
# Try exact match first |
|
for eid, enemy in living_enemies.items(): |
|
if enemy.name.lower() == enemy_name_query: |
|
target_enemy = enemy |
|
break |
|
|
|
# If no exact match, try partial match (first word often enough) |
|
if not target_enemy: |
|
first_word_query = args[0] |
|
potential_targets = [] |
|
for eid, enemy in living_enemies.items(): |
|
# Check if query matches start of name or is contained within |
|
if enemy.name.lower().startswith(first_word_query) or first_word_query in enemy.name.lower().split(): |
|
potential_targets.append(enemy) |
|
|
|
if len(potential_targets) == 1: |
|
target_enemy = potential_targets[0] |
|
elif len(potential_targets) > 1: |
|
print(f"Which enemy do you want to attack?") |
|
for i, e in enumerate(potential_targets): |
|
print(f" {i+1}. {e.name} ({e.health}/{e.max_health} HP)") |
|
try: |
|
choice = input(f"Enter number (1-{len(potential_targets)}): ").strip() |
|
index = int(choice) - 1 |
|
if 0 <= index < len(potential_targets): |
|
target_enemy = potential_targets[index] |
|
else: |
|
print("Invalid choice.") |
|
action_taken = False # Requires valid choice |
|
except ValueError: |
|
print("Invalid input. Please enter a number.") |
|
action_taken = False # Requires valid choice |
|
# Else: No partial match found either |
|
|
|
# Perform the attack if a target was successfully identified |
|
if target_enemy: |
|
if target_enemy.is_alive(): |
|
player.attack(target_enemy) |
|
# Check if enemy died from the attack |
|
if not target_enemy.is_alive(): |
|
print(f"You defeated the {target_enemy.name}!") |
|
# Check win condition: Was this the boss? |
|
if target_enemy.name == WIN_CONDITION_ENEMY_TYPE: |
|
game_state.player_won = True |
|
game_state.game_over = True # End game immediately on win |
|
else: |
|
print(f"The {target_enemy.name} is already defeated.") |
|
action_taken = False # Attacking dead enemy wastes no turn |
|
elif action_taken: # Only print if target wasn't found and choice wasn't pending |
|
print(f"Cannot find an enemy called '{enemy_name_query}' here.") |
|
action_taken = False # Failed attack doesn't take a turn |
|
|
|
else: |
|
print(f"Unknown command: '{command}'. Type 'help' for commands.") |
|
action_taken = False # Unknown command doesn't take a turn |
|
|
|
return action_taken # Return whether the player's action consumed their turn |
|
|
|
|
|
# --- Main Game Loop --- |
|
|
|
def game_loop(): |
|
"""Initializes the game state and runs the main interactive loop.""" |
|
# Initialization |
|
game_state = GameState() |
|
# Optional: Set seed for reproducible testing |
|
# random.seed(42) |
|
generate_world(game_state) |
|
|
|
player = game_state.player |
|
if not player: |
|
print("Fatal Error: Player object not created during world generation.") |
|
return |
|
|
|
# Welcome Message |
|
print("\n" + "*" * 30) |
|
print(" Welcome to the Depths of Procedura!") |
|
print("*" * 30) |
|
print(wrap_text(f"Your quest is to navigate the generated dungeon, gather what aid you can, and defeat the fearsome {WIN_CONDITION_ENEMY_TYPE} lurking within.")) |
|
print("\nType 'help' for a list of commands.") |
|
|
|
# Show initial room description |
|
game_state.get_current_room().describe(game_state) |
|
|
|
# --- Main Loop --- |
|
while not game_state.game_over: |
|
print("-" * 20) # Separator for turns |
|
# Display Player Prompt |
|
try: |
|
raw_input = input("What do you do? > ").strip() |
|
if not raw_input: |
|
print("Please enter a command or type 'help'.") |
|
continue # Skip turn if input is empty |
|
|
|
command, args = parse_command(raw_input) |
|
|
|
# Execute Player Command & Check if Turn Passes |
|
player_turn_taken = execute_command(game_state, command, args) |
|
|
|
# Check game end conditions immediately after player action (win/loss/quit) |
|
if game_state.game_over: |
|
break # Exit loop if game ended |
|
|
|
# --- AI Turn --- |
|
# AI only acts if the player performed an action that took a turn |
|
if player_turn_taken: |
|
# Check if there are any enemies in the player's current room that might react immediately |
|
current_room = game_state.get_current_room() |
|
living_enemies_in_room = [game_state.enemies[eid] for eid in current_room.enemy_ids if game_state.enemies[eid].is_alive()] |
|
|
|
if living_enemies_in_room: |
|
print("\n--- Enemy Actions ---") |
|
# Run AI for all enemies (handles movement and attacks) |
|
run_ai_turn(game_state) |
|
# else: No enemies in room, AI turn might still involve movement elsewhere if implemented globally |
|
|
|
# Check player death after AI turn |
|
if not player.is_alive(): |
|
game_state.game_over = True |
|
# Player death message is handled in take_damage generally, but confirm here. |
|
# print("You succumb to your wounds...") # Redundant if take_damage prints well |
|
|
|
# Graceful exit on Ctrl+C / Ctrl+D |
|
except KeyboardInterrupt: |
|
print("\nCaught interrupt signal. Quitting game.") |
|
game_state.game_over = True |
|
except EOFError: |
|
print("\nEnd of input detected. Quitting game.") |
|
game_state.game_over = True |
|
# Catch other potential errors during gameplay loop if needed |
|
# except Exception as e: |
|
# print(f"\nAn unexpected error occurred: {e}") |
|
# print("Attempting to exit gracefully.") |
|
# game_state.game_over = True |
|
|
|
|
|
# --- Game Over --- |
|
print("\n" + "=" * 30) |
|
if game_state.player_won: |
|
print(" *** Victory! ***") |
|
print(f"You have vanquished the mighty {WIN_CONDITION_ENEMY_TYPE}!") |
|
print(" Congratulations!") |
|
elif player and not player.is_alive(): |
|
print(" --- You Have Died ---") |
|
print(" Your adventure ends here.") |
|
else: # Game ended via 'quit' or other means |
|
print(" --- Game Over ---") |
|
print("=" * 30 + "\n") |
|
|
|
|
|
# --- Start the game --- |
|
if __name__ == "__main__": |
|
game_loop() |