diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51acc7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Environment variables +.env + +# Game specific +data/saves/ +*.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c77b9d7 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# Terminal Quest + +A sophisticated text-based RPG that leverages AI to generate unique content, featuring dynamic character classes, items, and a complex combat system with status effects. + +## Features + +### AI-Powered Content Generation +- Dynamic character class generation using OpenAI's GPT-3.5 +- Intelligent fallback system with predefined classes (Plague Herald, Blood Sovereign, Void Harbinger) +- Balanced stat generation within predefined ranges +- Unique skill and ability creation + +### Advanced Combat System +- Turn-based tactical combat with status effects +- Equipment-based stat modifications +- Skill system with mana management +- Status effects that modify stats and deal damage over time: + - Bleeding: Continuous damage (5 damage/turn) + - Poisoned: Damage + attack reduction (3 damage/turn, -2 attack) + - Weakened: Reduced combat stats (-3 attack, -2 defense) + - Burning: High damage over time (7 damage/turn) + - Cursed: Multi-stat reduction (-2 attack, -1 defense, -10 mana) + +### Comprehensive Item System +- Multiple item types: + - Weapons with attack modifiers + - Armor with defense and health bonuses + - Accessories with unique stat combinations + - Consumables with healing and status effects +- Durability system for equipment (40-100 based on rarity) +- Rarity system (Common to Legendary) with drop chances: + - Common: 15% + - Uncommon: 10% + - Rare: 5% + - Epic: 3% + - Legendary: 1% +- Dynamic shop system with item descriptions and stats + +### Character Classes +- Plague Herald: Disease and decay specialist (90 HP, 14 ATK, 4 DEF, 80 MP) +- Blood Sovereign: Vampiric powers and blood magic (100 HP, 16 ATK, 5 DEF, 70 MP) +- Void Harbinger: Cosmic void manipulation (85 HP, 12 ATK, 3 DEF, 100 MP) +- AI-generated custom classes within balanced stat ranges + +### Character Progression +- Experience-based leveling system +- Level-up bonuses: + - Health: +20 + - Mana: +15 + - Attack: +5 + - Defense: +3 +- Equipment management with three slots (weapon, armor, accessory) +- Inventory system with consumables and equipment +- Gold-based economy with configurable sell prices + +## Installation + +### Prerequisites +- Python 3.12+ +- OpenAI API key (for AI-generated content) + +### Setup +1. Clone the repository: +```bash +git clone https://github.com/yourusername/terminal-quest.git +cd terminal-quest +``` +2. Install dependencies: +```bash +pip3 install -r requirements.txt +``` + +3. Create a `.env` file: +```env +OPENAI_API_KEY=your_key_here +``` + +4. Run the game: +```bash +python3 main.py +``` + +## Project Structure + +``` +terminal_quest/ +├── src/ # Source code +│ ├── models/ # Game entities and data structures +│ │ ├── character.py # Base Character, Player, and Enemy classes +│ │ ├── character_classes.py # Character class definitions +│ │ ├── items.py # Item system implementation +│ │ ├── skills.py # Skill system +│ │ └── status_effects.py # Status effects system +│ ├── services/ # Game systems +│ │ ├── ai_generator.py # OpenAI integration +│ │ ├── combat.py # Combat system +│ │ └── shop.py # Shop system +│ ├── utils/ # Utilities +│ │ └── display.py # UI utilities +│ └── config/ # Configuration +│ └── settings.py # Game settings and constants +├── data/ # Game data +│ ├── items.json # Item definitions +│ └── fallbacks.json # Fallback content +├── main.py # Game entry point +├── requirements.txt # Project dependencies +├── .env # Environment variables +└── .gitignore +``` + +## Best Practices + +### Code Organization +- Modular design with clear separation of concerns +- Configuration centralized in settings.py +- Type hints throughout the codebase +- Comprehensive documentation and comments + +### Error Handling +- Graceful fallback system for AI failures +- Input validation for all user interactions +- Proper exception handling for file and API operations + +### Game Balance +- Configurable game constants in settings.py +- Balanced stat ranges for characters and items +- Progressive difficulty scaling +- Fair item drop rates and shop prices + +### Performance +- Efficient status effect processing +- Minimal API calls with caching +- Optimized combat calculations + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Follow the established code style: + - Use type hints + - Follow PEP 8 + - Add docstrings for functions and classes + - Update tests if applicable +4. Submit a pull request + +## Future Enhancements +- Quest system +- More character classes +- Additional status effects +- Enhanced AI integration +- Saving/loading system +- Multiplayer support + +## License +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3285c23 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,44 @@ +# Terminal Quest Documentation + +Terminal Quest is a text-based RPG that uses AI to generate unique content. The game features dynamic character classes, items, and combat systems. + +## Getting Started + +### Prerequisites +- Python 3.8+ +- OpenAI API key + +### Installation +1. Clone the repository +2. Install dependencies: `pip install -r requirements.txt` +3. Create a `.env` file and add your OpenAI API key: + ``` + OPENAI_API_KEY=your_key_here + ``` +4. Run the game: `python main.py` + +## Game Features + +### Character Classes +- AI-generated unique character classes +- Each class has special skills and base stats +- Fallback classes when AI generation fails + +### Combat System +- Turn-based combat +- Status effects (bleeding, poison, etc.) +- Skill-based abilities +- Random encounters + +### Items and Equipment +- Various item types (weapons, armor, consumables) +- Equipment affects character stats +- Items can be bought in shops or dropped by enemies +- Inventory management system + +### Status Effects +- Dynamic status effect system +- Effects can modify stats or deal damage over time +- Multiple effects can be active simultaneously + +## Project Structure \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5145a1a --- /dev/null +++ b/main.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 + +import os +import random +from dotenv import load_dotenv +from src.services.ai_generator import generate_character_class, generate_enemy +from src.services.combat import combat +from src.services.shop import Shop +from src.utils.display import clear_screen, type_text +from src.config.settings import GAME_BALANCE, STARTING_INVENTORY +from src.models.character import Player, get_fallback_enemy +from src.models.character_classes import fallback_classes + +def generate_unique_classes(count: int = 3): + """Generate unique character classes with fallback system""" + classes = [] + used_names = set() + max_attempts = 3 + + # Try to generate classes with API + for _ in range(max_attempts): + try: + char_class = generate_character_class() + if char_class and char_class.name not in used_names: + print(f"\nSuccessfully generated: {char_class.name}") + classes.append(char_class) + used_names.add(char_class.name) + if len(classes) >= count: + return classes + except Exception as e: + print(f"\nAttempt failed: {e}") + break + + # If we don't have enough classes, use fallbacks + if len(classes) < count: + print("\nUsing fallback character classes...") + for c in fallback_classes: + if len(classes) < count and c.name not in used_names: + classes.append(c) + used_names.add(c.name) + + return classes + +def show_stats(player: Player): + """Display player stats""" + clear_screen() + print(f"\n{'='*40}") + print(f"Name: {player.name} | Class: {player.char_class.name} | Level: {player.level}") + print(f"Health: {player.health}/{player.max_health}") + print(f"Mana: {player.mana}/{player.max_mana}") + print(f"Attack: {player.get_total_attack()} | Defense: {player.get_total_defense()}") + print(f"EXP: {player.exp}/{player.exp_to_level}") + print(f"Gold: {player.inventory['Gold']}") + + # Show equipment + print("\nEquipment:") + for slot, item in player.equipment.items(): + if item: + print(f"{slot.title()}: {item.name}") + else: + print(f"{slot.title()}: None") + + # Show inventory + print("\nInventory:") + for item in player.inventory['items']: + print(f"- {item.name}") + print(f"{'='*40}\n") + +def main(): + # Load environment variables + load_dotenv() + + clear_screen() + type_text("Welcome to Terminal Quest! 🗡️", 0.05) + player_name = input("\nEnter your hero's name: ") + + # Generate or get character classes + print("\nGenerating character classes...") + classes = generate_unique_classes(3) + + # Character selection + print("\nChoose your class:") + for i, char_class in enumerate(classes, 1): + print(f"\n{i}. {char_class.name}") + print(f" {char_class.description}") + print(f" Health: {char_class.base_health} | Attack: {char_class.base_attack} | Defense: {char_class.base_defense} | Mana: {char_class.base_mana}") + print("\n Skills:") + for skill in char_class.skills: + print(f" - {skill.name}: {skill.description} (Damage: {skill.damage}, Mana: {skill.mana_cost})") + + # Class selection input + while True: + try: + choice = int(input("\nYour choice (1-3): ")) - 1 + if 0 <= choice < len(classes): + player = Player(player_name, classes[choice]) + break + except ValueError: + pass + print("Invalid choice, please try again.") + + # Initialize shop + shop = Shop() + + # Main game loop + while player.health > 0: + show_stats(player) + print("\nWhat would you like to do?") + print("1. Fight an Enemy") + print("2. Visit Shop") + print("3. Rest (Heal 20 HP and 15 MP)") + print("4. Manage Equipment") + print("5. Quit Game") + + choice = input("\nYour choice (1-5): ") + + if choice == "1": + enemy = generate_enemy() or get_fallback_enemy() + combat(player, enemy) + input("\nPress Enter to continue...") + + elif choice == "2": + shop.show_inventory(player) + while True: + print("\n1. Buy Item") + print("2. Sell Item") + print("3. Leave Shop") + shop_choice = input("\nYour choice (1-3): ") + + if shop_choice == "1": + try: + item_index = int(input("Enter item number to buy: ")) - 1 + shop.buy_item(player, item_index) + except ValueError: + print("Invalid choice!") + elif shop_choice == "2": + if not player.inventory['items']: + type_text("\nNo items to sell!") + continue + print("\nYour items:") + for i, item in enumerate(player.inventory['items'], 1): + print(f"{i}. {item.name} - Worth: {int(item.value * GAME_BALANCE['SELL_PRICE_MULTIPLIER'])} Gold") + try: + item_index = int(input("Enter item number to sell: ")) - 1 + shop.sell_item(player, item_index) + except ValueError: + print("Invalid choice!") + elif shop_choice == "3": + break + + elif choice == "3": + healed = False + if player.health < player.max_health: + player.health = min(player.max_health, player.health + 20) + healed = True + if player.mana < player.max_mana: + player.mana = min(player.max_mana, player.mana + 15) + healed = True + + if healed: + type_text("You rest and recover some health and mana...") + else: + type_text("You are already at full health and mana!") + + elif choice == "4": + # Equipment management + if not player.inventory['items']: + type_text("\nNo items to equip!") + continue + + print("\nYour items:") + equippable_items = [item for item in player.inventory['items'] if hasattr(item, 'equip')] + if not equippable_items: + type_text("\nNo equipment items found!") + continue + + for i, item in enumerate(equippable_items, 1): + print(f"{i}. {item.name} ({item.item_type.value})") + + try: + item_index = int(input("\nChoose item to equip (0 to cancel): ")) - 1 + if 0 <= item_index < len(equippable_items): + item = equippable_items[item_index] + old_item = player.equip_item(item, item.item_type.value) + player.inventory['items'].remove(item) + if old_item: + player.inventory['items'].append(old_item) + type_text(f"\nEquipped {item.name}!") + except ValueError: + print("Invalid choice!") + + elif choice == "5": + type_text("\nThanks for playing Terminal Quest! Goodbye! 👋") + break + + if player.health <= 0: + type_text("\nGame Over! Your hero has fallen in battle... 💀") + type_text(f"Final Level: {player.level}") + type_text(f"Gold Collected: {player.inventory['Gold']}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e5992d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +openai>=1.12.0 +python-dotenv>=1.0.0 +colorama>=0.4.6 \ No newline at end of file diff --git a/src/config/settings.py b/src/config/settings.py new file mode 100644 index 0000000..2c354cd --- /dev/null +++ b/src/config/settings.py @@ -0,0 +1,117 @@ +from typing import Dict, Tuple + +# Game version +VERSION = "1.0.0" + +# Game balance settings +GAME_BALANCE = { + # Level up settings + "BASE_EXP_TO_LEVEL": 100, + "LEVEL_UP_EXP_MULTIPLIER": 1.5, + "LEVEL_UP_HEALTH_INCREASE": 20, + "LEVEL_UP_MANA_INCREASE": 15, + "LEVEL_UP_ATTACK_INCREASE": 5, + "LEVEL_UP_DEFENSE_INCREASE": 3, + + # Combat settings + "RUN_CHANCE": 0.4, + "DAMAGE_RANDOMNESS_RANGE": (-3, 3), + + # Shop settings + "SELL_PRICE_MULTIPLIER": 0.5, # Items sell for half their buy price +} + +# Character stat ranges +STAT_RANGES: Dict[str, Tuple[int, int]] = { + # Character class stat ranges + "CLASS_HEALTH": (85, 120), + "CLASS_ATTACK": (12, 18), + "CLASS_DEFENSE": (3, 8), + "CLASS_MANA": (60, 100), + + # Enemy stat ranges + "ENEMY_HEALTH": (30, 100), + "ENEMY_ATTACK": (8, 25), + "ENEMY_DEFENSE": (2, 10), + "ENEMY_EXP": (20, 100), + "ENEMY_GOLD": (10, 100), + + # Skill stat ranges + "SKILL_DAMAGE": (15, 30), + "SKILL_MANA_COST": (10, 25) +} + +# Starting inventory +STARTING_INVENTORY = { + "Health Potion": 2, + "Mana Potion": 2, + "Gold": 0, + "items": [] +} + +# Item settings +ITEM_SETTINGS = { + # Durability ranges + "COMMON_DURABILITY": (40, 60), + "RARE_DURABILITY": (60, 80), + "EPIC_DURABILITY": (80, 100), + + # Drop chances by rarity + "DROP_CHANCES": { + "COMMON": 0.15, + "UNCOMMON": 0.10, + "RARE": 0.05, + "EPIC": 0.03, + "LEGENDARY": 0.01 + } +} + +# Status effect settings +STATUS_EFFECT_SETTINGS = { + "BLEEDING": { + "DURATION": 3, + "DAMAGE": 5, + "CHANCE": 0.7 + }, + "POISONED": { + "DURATION": 4, + "DAMAGE": 3, + "ATTACK_REDUCTION": 2, + "CHANCE": 0.6 + }, + "WEAKENED": { + "DURATION": 2, + "ATTACK_REDUCTION": 3, + "DEFENSE_REDUCTION": 2, + "CHANCE": 0.8 + }, + "BURNING": { + "DURATION": 2, + "DAMAGE": 7, + "CHANCE": 0.65 + }, + "CURSED": { + "DURATION": 3, + "DAMAGE": 2, + "ATTACK_REDUCTION": 2, + "DEFENSE_REDUCTION": 1, + "MANA_REDUCTION": 10, + "CHANCE": 0.5 + } +} + +# Display settings +DISPLAY_SETTINGS = { + "TYPE_SPEED": 0.03, + "COMBAT_MESSAGE_DELAY": 1.0, + "LEVEL_UP_DELAY": 2.0 +} + +# AI Generation settings +AI_SETTINGS = { + "MAX_RETRIES": 3, + "TEMPERATURE": 0.8, + "MAX_TOKENS": 500, + "PRESENCE_PENALTY": 0.6, + "FREQUENCY_PENALTY": 0.6 +} \ No newline at end of file diff --git a/src/models/character.py b/src/models/character.py new file mode 100644 index 0000000..9e7fb19 --- /dev/null +++ b/src/models/character.py @@ -0,0 +1,99 @@ +from dataclasses import dataclass +from typing import Dict, List, Optional +import random +from .character_classes import CharacterClass +from .items import Item, Equipment +from .status_effects import StatusEffect + +class Character: + def __init__(self): + self.status_effects: Dict[str, StatusEffect] = {} + self.equipment: Dict[str, Optional[Equipment]] = { + "weapon": None, + "armor": None, + "accessory": None + } + + def apply_status_effects(self): + """Process all active status effects""" + effects_to_remove = [] + for effect_name, effect in self.status_effects.items(): + effect.tick(self) + effect.duration -= 1 + if effect.duration <= 0: + effects_to_remove.append(effect_name) + + for effect_name in effects_to_remove: + self.status_effects[effect_name].remove(self) + + def get_total_attack(self) -> int: + """Calculate total attack including equipment bonuses""" + total = self.attack + for equipment in self.equipment.values(): + if equipment and "attack" in equipment.stat_modifiers: + total += equipment.stat_modifiers["attack"] + return total + + def get_total_defense(self) -> int: + """Calculate total defense including equipment bonuses""" + total = self.defense + for equipment in self.equipment.values(): + if equipment and "defense" in equipment.stat_modifiers: + total += equipment.stat_modifiers["defense"] + return total + +class Player(Character): + def __init__(self, name: str, char_class: CharacterClass): + super().__init__() + self.name = name + self.char_class = char_class + self.health = char_class.base_health + self.max_health = char_class.base_health + self.attack = char_class.base_attack + self.defense = char_class.base_defense + self.mana = char_class.base_mana + self.max_mana = char_class.base_mana + self.inventory = { + "Health Potion": 2, + "Mana Potion": 2, + "Gold": 0, + "items": [] + } + self.level = 1 + self.exp = 0 + self.exp_to_level = 100 + self.skills = char_class.skills + + def equip_item(self, item: Equipment, slot: str) -> Optional[Equipment]: + """Equip an item and return the previously equipped item if any""" + if slot not in self.equipment: + return None + + old_item = self.equipment[slot] + if old_item: + old_item.unequip(self) + + self.equipment[slot] = item + item.equip(self) + return old_item + +class Enemy(Character): + def __init__(self, name: str, health: int, attack: int, defense: int, exp: int, gold: int): + super().__init__() + self.name = name + self.health = health + self.attack = attack + self.defense = defense + self.exp = exp + self.gold = gold + +def get_fallback_enemy() -> Enemy: + """Return a random fallback enemy when AI generation fails""" + enemies = [ + Enemy("Goblin", 30, 8, 2, 20, 10), + Enemy("Skeleton", 45, 12, 4, 35, 20), + Enemy("Orc", 60, 15, 6, 50, 30), + Enemy("Dark Mage", 40, 20, 3, 45, 25), + Enemy("Dragon", 100, 25, 10, 100, 75) + ] + return random.choice(enemies) \ No newline at end of file diff --git a/src/models/character_classes.py b/src/models/character_classes.py new file mode 100644 index 0000000..dd7cf11 --- /dev/null +++ b/src/models/character_classes.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +from typing import List +from .skills import Skill + +@dataclass +class CharacterClass: + name: str + description: str + base_health: int + base_attack: int + base_defense: int + base_mana: int + skills: List[Skill] + +fallback_classes = [ + CharacterClass( + name="Plague Herald", + description="A corrupted physician who weaponizes disease and decay. Masters of pestilence who spread suffering through calculated afflictions.", + base_health=90, + base_attack=14, + base_defense=4, + base_mana=80, + skills=[ + Skill(name="Virulent Outbreak", damage=25, mana_cost=20, description="Unleashes a devastating plague that corrupts flesh and spirit"), + Skill(name="Miasmic Shroud", damage=15, mana_cost=15, description="Surrounds self with toxic vapors that decay all they touch") + ] + ), + CharacterClass( + name="Blood Sovereign", + description="A noble cursed with vampiric powers. Sustains their immortality through the essence of others while commanding crimson forces.", + base_health=100, + base_attack=16, + base_defense=5, + base_mana=70, + skills=[ + Skill(name="Crimson Feast", damage=28, mana_cost=25, description="Drains life force through cursed bloodletting"), + Skill(name="Sanguine Storm", damage=20, mana_cost=20, description="Conjures a tempest of crystallized blood shards") + ] + ), + CharacterClass( + name="Void Harbinger", + description="A being touched by the cosmic void. Channels the emptiness between stars to unmake reality itself.", + base_health=85, + base_attack=12, + base_defense=3, + base_mana=100, + skills=[ + Skill(name="Null Cascade", damage=30, mana_cost=30, description="Tears a rift in space that consumes all it touches"), + Skill(name="Entropy Surge", damage=22, mana_cost=25, description="Accelerates the decay of matter and spirit") + ] + ) +] \ No newline at end of file diff --git a/src/models/items.py b/src/models/items.py new file mode 100644 index 0000000..44fc706 --- /dev/null +++ b/src/models/items.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass, field +from typing import Dict, List, Optional +from enum import Enum + +class ItemType(Enum): + WEAPON = "weapon" + ARMOR = "armor" + ACCESSORY = "accessory" + CONSUMABLE = "consumable" + MATERIAL = "material" + +class ItemRarity(Enum): + COMMON = "Common" + UNCOMMON = "Uncommon" + RARE = "Rare" + EPIC = "Epic" + LEGENDARY = "Legendary" + +@dataclass +class Item: + name: str + description: str + item_type: ItemType + value: int + rarity: ItemRarity + drop_chance: float = 0.1 + + def use(self, target: 'Character') -> bool: + """Base use method, should be overridden by specific item types""" + return False + +@dataclass +class Equipment(Item): + stat_modifiers: Dict[str, int] = field(default_factory=dict) + durability: int = 50 + max_durability: int = 50 + + def equip(self, character: 'Character'): + """Apply stat modifiers to character""" + for stat, value in self.stat_modifiers.items(): + current = getattr(character, stat, 0) + setattr(character, stat, current + value) + + def unequip(self, character: 'Character'): + """Remove stat modifiers from character""" + for stat, value in self.stat_modifiers.items(): + current = getattr(character, stat, 0) + setattr(character, stat, current - value) + + def repair(self, amount: int = None): + """Repair item durability""" + if amount is None: + self.durability = self.max_durability + else: + self.durability = min(self.max_durability, self.durability + amount) + +@dataclass +class Consumable(Item): + effects: List[Dict] = field(default_factory=list) + + def use(self, target: 'Character') -> bool: + """Apply consumable effects to target""" + from .status_effects import BLEEDING, POISONED, WEAKENED, BURNING, CURSED + + status_effect_map = { + "BLEEDING": BLEEDING, + "POISONED": POISONED, + "WEAKENED": WEAKENED, + "BURNING": BURNING, + "CURSED": CURSED + } + + for effect in self.effects: + if effect.get('heal_health'): + target.health = min(target.max_health, + target.health + effect['heal_health']) + if effect.get('heal_mana'): + target.mana = min(target.max_mana, + target.mana + effect['heal_mana']) + if effect.get('status_effect'): + effect_name = effect['status_effect'] + if effect_name in status_effect_map: + status_effect_map[effect_name].apply(target) + return True + +# Example items +RUSTY_SWORD = Equipment( + name="Rusty Sword", + description="An old sword with some rust spots", + item_type=ItemType.WEAPON, + value=20, + rarity=ItemRarity.COMMON, + stat_modifiers={"attack": 3}, + durability=50, + max_durability=50, + drop_chance=0.15 +) + +LEATHER_ARMOR = Equipment( + name="Leather Armor", + description="Basic protection made of leather", + item_type=ItemType.ARMOR, + value=25, + rarity=ItemRarity.COMMON, + stat_modifiers={"defense": 2, "max_health": 10}, + durability=40, + max_durability=40, + drop_chance=0.12 +) + +HEALING_SALVE = Consumable( + name="Healing Salve", + description="A medicinal salve that heals wounds", + item_type=ItemType.CONSUMABLE, + value=15, + rarity=ItemRarity.COMMON, + effects=[{"heal_health": 30}], + drop_chance=0.2 +) + +POISON_VIAL = Consumable( + name="Poison Vial", + description="A vial of toxic substance", + item_type=ItemType.CONSUMABLE, + value=20, + rarity=ItemRarity.UNCOMMON, + effects=[{"status_effect": "POISONED"}], + drop_chance=0.1 +) + +# More powerful items +VAMPIRIC_BLADE = Equipment( + name="Vampiric Blade", + description="A cursed blade that drains life force", + item_type=ItemType.WEAPON, + value=75, + rarity=ItemRarity.RARE, + stat_modifiers={"attack": 8, "max_health": 15}, + durability=60, + max_durability=60, + drop_chance=0.05 +) + +CURSED_AMULET = Equipment( + name="Cursed Amulet", + description="An amulet pulsing with dark energy", + item_type=ItemType.ACCESSORY, + value=100, + rarity=ItemRarity.EPIC, + stat_modifiers={"attack": 5, "max_mana": 25, "defense": -2}, + durability=40, + max_durability=40, + drop_chance=0.03 +) \ No newline at end of file diff --git a/src/models/skills.py b/src/models/skills.py new file mode 100644 index 0000000..fdb902c --- /dev/null +++ b/src/models/skills.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +@dataclass +class Skill: + name: str + damage: int + mana_cost: int + description: str \ No newline at end of file diff --git a/src/models/status_effects.py b/src/models/status_effects.py new file mode 100644 index 0000000..b3f8a8e --- /dev/null +++ b/src/models/status_effects.py @@ -0,0 +1,91 @@ +from dataclasses import dataclass +from typing import Dict, Optional +import random + +@dataclass +class StatusEffect: + name: str + description: str + duration: int + stat_modifiers: Optional[Dict[str, int]] = None + tick_damage: int = 0 + chance_to_apply: float = 1.0 + + def apply(self, target: 'Character') -> bool: + """Attempts to apply the status effect to a target""" + if random.random() <= self.chance_to_apply: + # If same effect exists, refresh duration + if self.name in target.status_effects: + target.status_effects[self.name].duration = max( + self.duration, + target.status_effects[self.name].duration + ) + else: + target.status_effects[self.name] = self + # Apply initial stat modifications + if self.stat_modifiers: + for stat, modifier in self.stat_modifiers.items(): + current_value = getattr(target, stat, 0) + setattr(target, stat, current_value + modifier) + return True + return False + + def tick(self, target: 'Character'): + """Apply the effect for one turn""" + if self.tick_damage: + damage = self.tick_damage + # Status effect damage ignores defense + target.health -= damage + return damage + return 0 + + def remove(self, target: 'Character'): + """Remove the effect and revert any stat changes""" + if self.stat_modifiers: + for stat, modifier in self.stat_modifiers.items(): + current_value = getattr(target, stat, 0) + setattr(target, stat, current_value - modifier) + target.status_effects.pop(self.name, None) + +# Predefined status effects +BLEEDING = StatusEffect( + name="Bleeding", + description="Taking damage over time from blood loss", + duration=3, + tick_damage=5, + chance_to_apply=0.7 +) + +POISONED = StatusEffect( + name="Poisoned", + description="Taking poison damage and reduced attack", + duration=4, + tick_damage=3, + stat_modifiers={"attack": -2}, + chance_to_apply=0.6 +) + +WEAKENED = StatusEffect( + name="Weakened", + description="Reduced attack and defense from exhaustion", + duration=2, + stat_modifiers={"attack": -3, "defense": -2}, + chance_to_apply=0.8 +) + +BURNING = StatusEffect( + name="Burning", + description="Taking fire damage over time", + duration=2, + tick_damage=7, + chance_to_apply=0.65 +) + +CURSED = StatusEffect( + name="Cursed", + description="A dark curse reducing all stats", + duration=3, + tick_damage=2, + stat_modifiers={"attack": -2, "defense": -1, "max_mana": -10}, + chance_to_apply=0.5 +) \ No newline at end of file diff --git a/src/services/ai_generator.py b/src/services/ai_generator.py new file mode 100644 index 0000000..cde430d --- /dev/null +++ b/src/services/ai_generator.py @@ -0,0 +1,152 @@ +from openai import OpenAI +import json +import time +import os +from typing import Optional +from ..models.character_classes import CharacterClass +from ..models.skills import Skill +from ..models.character import Enemy +from ..config.settings import AI_SETTINGS, STAT_RANGES + +def setup_openai(): + api_key = os.getenv('OPENAI_API_KEY') + if not api_key: + raise ValueError("No OpenAI API key found in environment variables") + return OpenAI(api_key=api_key) + +def generate_content(prompt: str, retries: int = None) -> Optional[str]: + if retries is None: + retries = AI_SETTINGS["MAX_RETRIES"] + + client = setup_openai() + + for attempt in range(retries): + try: + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + { + "role": "system", + "content": "You are a dark fantasy RPG content generator focused on creating balanced game elements. Respond only with valid JSON." + }, + { + "role": "user", + "content": prompt + } + ], + temperature=AI_SETTINGS["TEMPERATURE"], + max_tokens=AI_SETTINGS["MAX_TOKENS"], + presence_penalty=AI_SETTINGS["PRESENCE_PENALTY"], + frequency_penalty=AI_SETTINGS["FREQUENCY_PENALTY"] + ) + content = response.choices[0].message.content.strip() + json.loads(content) # Validate JSON + return content + except Exception as e: + print(f"\nAttempt {attempt + 1} failed: {e}") + if attempt == retries - 1: + return None + time.sleep(1) + +def generate_character_class() -> Optional[CharacterClass]: + prompt = f"""Create a unique dark fantasy character class following these guidelines: +- Must be morally ambiguous or corrupted +- Powers should involve darkness, corruption, or forbidden magic +- Stats must be balanced for gameplay +- Skills should have clear combat applications + +Return as JSON: +{{ + "name": "unique class name", + "description": "2-3 sentences about the class's dark nature and powers", + "base_health": "number between {STAT_RANGES['CLASS_HEALTH'][0]}-{STAT_RANGES['CLASS_HEALTH'][1]}", + "base_attack": "number between {STAT_RANGES['CLASS_ATTACK'][0]}-{STAT_RANGES['CLASS_ATTACK'][1]}", + "base_defense": "number between {STAT_RANGES['CLASS_DEFENSE'][0]}-{STAT_RANGES['CLASS_DEFENSE'][1]}", + "base_mana": "number between {STAT_RANGES['CLASS_MANA'][0]}-{STAT_RANGES['CLASS_MANA'][1]}", + "skills": [ + {{ + "name": "primary offensive skill name", + "damage": "number between {STAT_RANGES['SKILL_DAMAGE'][0]}-{STAT_RANGES['SKILL_DAMAGE'][1]}", + "mana_cost": "number between {STAT_RANGES['SKILL_MANA_COST'][0]}-{STAT_RANGES['SKILL_MANA_COST'][1]}", + "description": "dark-themed skill effect" + }}, + {{ + "name": "secondary utility skill name", + "damage": "number between {STAT_RANGES['SKILL_DAMAGE'][0]}-{STAT_RANGES['SKILL_DAMAGE'][1]}", + "mana_cost": "number between {STAT_RANGES['SKILL_MANA_COST'][0]}-{STAT_RANGES['SKILL_MANA_COST'][1]}", + "description": "dark-themed utility effect" + }} + ] +}}""" + + content = generate_content(prompt) + if not content: + return None + + try: + data = json.loads(content) + # Validate and normalize stats + stats = { + "base_health": (STAT_RANGES['CLASS_HEALTH'][0], STAT_RANGES['CLASS_HEALTH'][1]), + "base_attack": (STAT_RANGES['CLASS_ATTACK'][0], STAT_RANGES['CLASS_ATTACK'][1]), + "base_defense": (STAT_RANGES['CLASS_DEFENSE'][0], STAT_RANGES['CLASS_DEFENSE'][1]), + "base_mana": (STAT_RANGES['CLASS_MANA'][0], STAT_RANGES['CLASS_MANA'][1]) + } + + for stat, (min_val, max_val) in stats.items(): + data[stat] = max(min_val, min(max_val, int(data[stat]))) + + # Process skills + skills = [] + for skill in data["skills"]: + skill_data = { + "name": str(skill["name"]), + "damage": max(STAT_RANGES['SKILL_DAMAGE'][0], min(STAT_RANGES['SKILL_DAMAGE'][1], int(skill["damage"]))), + "mana_cost": max(STAT_RANGES['SKILL_MANA_COST'][0], min(STAT_RANGES['SKILL_MANA_COST'][1], int(skill["mana_cost"]))), + "description": str(skill["description"]) + } + skills.append(Skill(**skill_data)) + + return CharacterClass(**{**data, "skills": skills}) + except Exception as e: + print(f"\nError processing character class: {e}") + return None + +def generate_enemy() -> Optional[Enemy]: + prompt = """Create a dark fantasy enemy with these guidelines: +- Should fit a dark fantasy setting +- Stats must be balanced for player combat +- Can be undead, corrupted, or otherworldly + +Return as JSON: +{ + "name": "enemy name", + "health": "number between 30-100", + "attack": "number between 8-25", + "defense": "number between 2-10", + "exp": "number between 20-100", + "gold": "number between 10-100" +}""" + + content = generate_content(prompt) + if not content: + return None + + try: + data = json.loads(content) + # Validate and normalize stats + stats = { + "health": (30, 100), + "attack": (8, 25), + "defense": (2, 10), + "exp": (20, 100), + "gold": (10, 100) + } + + for stat, (min_val, max_val) in stats.items(): + data[stat] = max(min_val, min(max_val, int(data[stat]))) + + return Enemy(**data) + except Exception as e: + print(f"\nError processing enemy: {e}") + return None \ No newline at end of file diff --git a/src/services/combat.py b/src/services/combat.py new file mode 100644 index 0000000..605956a --- /dev/null +++ b/src/services/combat.py @@ -0,0 +1,166 @@ +from typing import Optional, List +from ..models.character import Player, Enemy +from ..models.items import Item +from ..utils.display import type_text, clear_screen +from ..config.settings import GAME_BALANCE, DISPLAY_SETTINGS +import random +import time + +def calculate_damage(attacker: 'Character', defender: 'Character', base_damage: int) -> int: + """Calculate damage considering attack, defense and randomness""" + damage = max(0, attacker.get_total_attack() + base_damage - defender.get_total_defense()) + rand_min, rand_max = GAME_BALANCE["DAMAGE_RANDOMNESS_RANGE"] + return damage + random.randint(rand_min, rand_max) + +def process_status_effects(character: 'Character') -> List[str]: + """Process all status effects and return messages""" + messages = [] + character.apply_status_effects() + + for effect_name, effect in character.status_effects.items(): + messages.append(f"{character.name} is affected by {effect_name}") + if effect.tick_damage: + messages.append(f"{effect_name} deals {effect.tick_damage} damage") + + return messages + +def check_for_drops(enemy: Enemy) -> Optional[Item]: + """Check for item drops from enemy""" + from ..models.items import RUSTY_SWORD, LEATHER_ARMOR, HEALING_SALVE, POISON_VIAL, VAMPIRIC_BLADE, CURSED_AMULET + possible_drops = [RUSTY_SWORD, LEATHER_ARMOR, HEALING_SALVE, POISON_VIAL, VAMPIRIC_BLADE, CURSED_AMULET] + + for item in possible_drops: + if random.random() < item.drop_chance: + return item + return None + +def combat(player: Player, enemy: Enemy) -> bool: + """ + Handles combat between player and enemy. + Returns True if player wins, False if player runs or dies. + """ + clear_screen() + type_text(f"\nA wild {enemy.name} appears!") + time.sleep(DISPLAY_SETTINGS["COMBAT_MESSAGE_DELAY"]) + + while enemy.health > 0 and player.health > 0: + # Process status effects at the start of turn + effect_messages = process_status_effects(player) + effect_messages.extend(process_status_effects(enemy)) + for msg in effect_messages: + type_text(msg) + + print(f"\n{'-'*40}") + print(f"{enemy.name} HP: {enemy.health}") + print(f"Your HP: {player.health}/{player.max_health}") + print(f"Your MP: {player.mana}/{player.max_mana}") + + # Show active status effects + if player.status_effects: + effects = [f"{name}({effect.duration})" for name, effect in player.status_effects.items()] + print(f"Status Effects: {', '.join(effects)}") + + print(f"\nWhat would you like to do?") + print("1. Attack") + print("2. Use Skill") + print("3. Use Item") + print("4. Try to Run") + + choice = input("\nYour choice (1-4): ") + + if choice == "1": + # Basic attack + damage = calculate_damage(player, enemy, 0) + enemy.health -= damage + type_text(f"\nYou deal {damage} damage to the {enemy.name}!") + + elif choice == "2": + # Skill usage + print("\nChoose a skill:") + for i, skill in enumerate(player.skills, 1): + print(f"{i}. {skill.name} (Damage: {skill.damage}, Mana: {skill.mana_cost})") + print(f" {skill.description}") + + try: + skill_choice = int(input("\nYour choice: ")) - 1 + if 0 <= skill_choice < len(player.skills): + skill = player.skills[skill_choice] + if player.mana >= skill.mana_cost: + damage = calculate_damage(player, enemy, skill.damage) + enemy.health -= damage + player.mana -= skill.mana_cost + type_text(f"\nYou used {skill.name} and dealt {damage} damage!") + else: + type_text("\nNot enough mana!") + continue + except ValueError: + type_text("\nInvalid choice!") + continue + + elif choice == "3": + # Item usage + if not player.inventory['items']: + type_text("\nNo items in inventory!") + continue + + print("\nChoose an item to use:") + for i, item in enumerate(player.inventory['items'], 1): + print(f"{i}. {item.name}") + print(f" {item.description}") + + try: + item_choice = int(input("\nYour choice: ")) - 1 + if 0 <= item_choice < len(player.inventory['items']): + item = player.inventory['items'][item_choice] + if item.use(player): + player.inventory['items'].pop(item_choice) + type_text(f"\nUsed {item.name}!") + else: + type_text("\nCannot use this item!") + continue + except ValueError: + type_text("\nInvalid choice!") + continue + + elif choice == "4": + # Try to run + if random.random() < GAME_BALANCE["RUN_CHANCE"]: + type_text("\nYou successfully ran away!") + return False + else: + type_text("\nCouldn't escape!") + + # Enemy turn + if enemy.health > 0: + damage = calculate_damage(enemy, player, 0) + player.health -= damage + type_text(f"The {enemy.name} deals {damage} damage to you!") + + if enemy.health <= 0: + type_text(f"\nYou defeated the {enemy.name}!") + player.exp += enemy.exp + player.inventory["Gold"] += enemy.gold + type_text(f"You gained {enemy.exp} EXP and {enemy.gold} Gold!") + + # Check for item drops + dropped_item = check_for_drops(enemy) + if dropped_item: + player.inventory['items'].append(dropped_item) + type_text(f"The {enemy.name} dropped a {dropped_item.name}!") + + # Level up check + if player.exp >= player.exp_to_level: + player.level += 1 + player.exp -= player.exp_to_level + player.exp_to_level = int(player.exp_to_level * GAME_BALANCE["LEVEL_UP_EXP_MULTIPLIER"]) + player.max_health += GAME_BALANCE["LEVEL_UP_HEALTH_INCREASE"] + player.health = player.max_health + player.max_mana += GAME_BALANCE["LEVEL_UP_MANA_INCREASE"] + player.mana = player.max_mana + player.attack += GAME_BALANCE["LEVEL_UP_ATTACK_INCREASE"] + player.defense += GAME_BALANCE["LEVEL_UP_DEFENSE_INCREASE"] + type_text(f"\n🎉 Level Up! You are now level {player.level}!") + time.sleep(DISPLAY_SETTINGS["LEVEL_UP_DELAY"]) + return True + + return False \ No newline at end of file diff --git a/src/services/shop.py b/src/services/shop.py new file mode 100644 index 0000000..3d23d1c --- /dev/null +++ b/src/services/shop.py @@ -0,0 +1,66 @@ +from typing import List, Dict +from ..models.character import Player +from ..models.items import Item, Equipment, Consumable, RUSTY_SWORD, LEATHER_ARMOR, HEALING_SALVE, POISON_VIAL, VAMPIRIC_BLADE, CURSED_AMULET +from ..utils.display import type_text, clear_screen +from ..config.settings import GAME_BALANCE, DISPLAY_SETTINGS + +class Shop: + def __init__(self): + self.inventory: List[Item] = [ + RUSTY_SWORD, + LEATHER_ARMOR, + HEALING_SALVE, + POISON_VIAL, + VAMPIRIC_BLADE, + CURSED_AMULET + ] + + def show_inventory(self, player: Player): + """Display shop inventory""" + clear_screen() + type_text("\nWelcome to the Shop!") + print(f"\nYour Gold: {player.inventory['Gold']}") + + print("\nAvailable Items:") + for i, item in enumerate(self.inventory, 1): + print(f"\n{i}. {item.name} - {item.value} Gold") + print(f" {item.description}") + if isinstance(item, Equipment): + mods = [f"{stat}: {value}" for stat, value in item.stat_modifiers.items()] + print(f" Stats: {', '.join(mods)}") + print(f" Durability: {item.durability}/{item.max_durability}") + elif isinstance(item, Consumable): + effects = [] + for effect in item.effects: + if 'heal_health' in effect: + effects.append(f"Heals {effect['heal_health']} HP") + if 'heal_mana' in effect: + effects.append(f"Restores {effect['heal_mana']} MP") + if 'status_effect' in effect: + effects.append(f"Applies {effect['status_effect'].name}") + print(f" Effects: {', '.join(effects)}") + print(f" Rarity: {item.rarity.value}") + + def buy_item(self, player: Player, item_index: int) -> bool: + """Process item purchase""" + if 0 <= item_index < len(self.inventory): + item = self.inventory[item_index] + if player.inventory['Gold'] >= item.value: + player.inventory['Gold'] -= item.value + player.inventory['items'].append(item) + type_text(f"\nYou bought {item.name}!") + return True + else: + type_text("\nNot enough gold!") + return False + + def sell_item(self, player: Player, item_index: int) -> bool: + """Process item sale""" + if 0 <= item_index < len(player.inventory['items']): + item = player.inventory['items'][item_index] + sell_value = int(item.value * GAME_BALANCE["SELL_PRICE_MULTIPLIER"]) + player.inventory['Gold'] += sell_value + player.inventory['items'].pop(item_index) + type_text(f"\nSold {item.name} for {sell_value} Gold!") + return True + return False \ No newline at end of file diff --git a/src/utils/display.py b/src/utils/display.py new file mode 100644 index 0000000..504373f --- /dev/null +++ b/src/utils/display.py @@ -0,0 +1,19 @@ +import os +import sys +import time +from ..config.settings import DISPLAY_SETTINGS + +def clear_screen(): + """Clear the terminal screen.""" + os.system('cls' if os.name == 'nt' else 'clear') + +def type_text(text: str, delay: float = None): + """Print text with a typewriter effect.""" + if delay is None: + delay = DISPLAY_SETTINGS["TYPE_SPEED"] + + for char in text: + sys.stdout.write(char) + sys.stdout.flush() + time.sleep(delay) + print() \ No newline at end of file