"""Fortschritts-Quest — Automatisches RPG mit lokalem LLM.""" import os import sys import time import random import signal # Add src to path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from rich.console import Console from rich.live import Live from rich.layout import Layout from rich.panel import Panel from rich.table import Table from rich.text import Text from rich.progress import Progress, BarColumn, TextColumn from rich.align import Align from rich import box from character import Character, RACES, CLASSES from engine import GameEngine from llm import NameCache console = Console() SAVE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "saves") os.makedirs(SAVE_DIR, exist_ok=True) BANNER = """ [bold magenta]╔═══════════════════════════════════════════════════════════╗ ║ FORTSCHRITTS-QUEST — Das automatische RPG ║ ║ "Wo du nichts tust und alles passiert" ║ ╚═══════════════════════════════════════════════════════════╝[/] """ def create_character(): """Character-Erstellung mit Zufallsoption.""" console.clear() console.print(BANNER) console.print("[bold]Character-Erstellung[/]\n") # Name name = console.input("[cyan]Name deines Helden (oder Enter für Zufall): [/]").strip() if not name: name = random.choice(["Günther", "Brigitte", "Klaus-Dieter", "Waltraud", "Herbert", "Sieglinde", "Wolfgang", "Margot"]) console.print(f"[dim]Zufälliger Name: {name}[/]") # Race console.print("\n[bold]Verfügbare Rassen:[/]") for i, r in enumerate(RACES): console.print(f" [cyan]{i+1}[/] — {r}") race_choice = console.input(f"\n[cyan]Rasse wählen (1-{len(RACES)}, Enter=Zufall): [/]").strip() if race_choice.isdigit() and 1 <= int(race_choice) <= len(RACES): race = RACES[int(race_choice) - 1] else: race = random.choice(RACES) console.print(f"[dim]Zufällige Rasse: {race}[/]") # Class console.print("\n[bold]Verfügbare Klassen:[/]") for i, c in enumerate(CLASSES): console.print(f" [cyan]{i+1}[/] — {c}") class_choice = console.input(f"\n[cyan]Klasse wählen (1-{len(CLASSES)}, Enter=Zufall): [/]").strip() if class_choice.isdigit() and 1 <= int(class_choice) <= len(CLASSES): char_class = CLASSES[int(class_choice) - 1] else: char_class = random.choice(CLASSES) console.print(f"[dim]Zufällige Klasse: {char_class}[/]") char = Character(name, race, char_class) # Show summary console.print(f"\n[bold green]✅ Character erstellt![/]") console.print(f" Name: [bold]{char.name}[/]") console.print(f" Rasse: {char.race}") console.print(f" Klasse: {char.char_class}") console.print(f" Stufe: {char.level}") console.print(f" HP: {char.hp}/{char.max_hp} | MP: {char.mp}/{char.max_mp}") console.print(f" STR: {char.str} | DEX: {char.dex} | INT: {char.int} | VIT: {char.vit} | LCK: {char.lck}") console.print(f" Gold: {char.gold}") console.input("\n[dim]Enter drücken um zu starten...[/]") return char def render_game(engine: GameEngine) -> Panel: """Rendert das komplette Spiel-UI.""" c = engine.character # === Character Panel === char_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 1)) char_table.add_column("Stat", style="cyan", width=8) char_table.add_column("Value", style="bold") char_table.add_row("Name", f"{c.name}") char_table.add_row("Rasse", c.race) char_table.add_row("Klasse", c.char_class) char_table.add_row("Stufe", str(c.level)) char_table.add_row("XP", f"{c.xp}/{c.xp_to_next}") char_table.add_row("HP", f"{c.hp}/{c.max_hp}") char_table.add_row("MP", f"{c.mp}/{c.max_mp}") char_table.add_row("Angriff", str(c.attack)) char_table.add_row("Vert.", str(c.defense)) char_table.add_row("Gold", f"{c.gold} 💰") char_table.add_row("Kills", str(c.kills)) char_table.add_row("Tode", str(c.deaths)) char_table.add_row("Quests", str(c.quests_done)) # === Equipment Panel === equip_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 1), title="[bold]Ausrüstung[/]") equip_table.add_column("Slot", style="cyan", width=10) equip_table.add_column("Item") slot_names = {"weapon": "Waffe", "armor": "Rüstung", "helmet": "Helm", "boots": "Stiefel", "accessory": "Accessoire"} for slot, name in slot_names.items(): item = c.equipped[slot] equip_table.add_row(name, str(item) if item else "[dim]leer[/]") # === Quest Panel === quest_text = Text() quest_text.append(f"📜 {engine.current_quest}\n", style="cyan") quest_text.append(f"Fortschritt: {engine.quest_progress}/{engine.quest_target}", style="yellow") # === XP Bar === xp_pct = c.xp / c.xp_to_next xp_bar_len = 20 xp_filled = int(xp_pct * xp_bar_len) xp_bar = "█" * xp_filled + "░" * (xp_bar_len - xp_filled) # HP Bar hp_pct = c.hp / c.max_hp hp_filled = int(hp_pct * xp_bar_len) hp_bar = "█" * hp_filled + "░" * (xp_bar_len - hp_filled) # === Log Panel === log_text = Text() for entry in engine.log[-12:]: if isinstance(entry, str): t = Text.from_markup(entry) log_text.append(t) log_text.append("\n") # === Location === loc_text = f"🌍 {engine.current_location}" # === Layout === layout = Layout() # Top: Title bar layout.split_column( Layout(Panel(f"[bold magenta]FORTSCHRITTS-QUEST[/] — {c.name} der {c.race} {c.char_class}", style="magenta"), size=3), Layout(name="main"), Layout(name="bottom", size=8), ) # Main: left=char/equip, center=log, right=quest/stats layout["main"].split_row( Layout(Panel(char_table, title="[bold]Character[/]"), name="left", size=30), Layout(Panel(log_text, title="[bold]Ereignis-Log[/]"), name="center"), Layout(name="right", size=30), ) layout["right"].split_column( Layout(Panel(equip_table), name="equip"), Layout(Panel(quest_text, title="[bold]Aktuelle Quest[/]"), name="quest"), ) # Bottom: bars + location bars = Text() bars.append(f"HP [{hp_bar}] {c.hp}/{c.max_hp}\n", style="red") bars.append(f"XP [{xp_bar}] {c.xp}/{c.xp_to_next}\n", style="green") bars.append(f"{loc_text} | Schritte: {c.steps} | STR {c.str} DEX {c.dex} INT {c.int} VIT {c.vit} LCK {c.lck}") layout["bottom"].update(Panel(bars, style="dim")) return layout def run_game(engine: GameEngine, save_path: str): """Haupt-Game-Loop mit Live-Rendering.""" auto_save_counter = 0 with Live(render_game(engine), console=console, refresh_per_second=2, screen=True) as live: try: while True: # Game step result = engine.step() # Refill cache occasionally if engine.character.steps % 5 == 0: engine.cache.refill_async() # Sell junk periodically if engine.character.steps % 10 == 0: engine._sell_junk() # Auto-save every 50 steps auto_save_counter += 1 if auto_save_counter >= 50: engine.save(save_path) auto_save_counter = 0 # Render live.update(render_game(engine)) # Speed — slower for readability, faster at higher levels speed = max(0.3, 1.5 - engine.character.level * 0.02) time.sleep(speed) except KeyboardInterrupt: live.update(render_game(engine)) console.print("\n[bold yellow]Spiel pausiert.[/]") engine.save(save_path) console.print(f"[green]Spielstand gespeichert: {save_path}[/]") console.print("[dim]Drücke Enter zum Beenden...[/]") input() def main(): console.clear() console.print(BANNER) save_path = os.path.join(SAVE_DIR, "savegame.json") # Check for existing save if os.path.exists(save_path): choice = console.input("[cyan]Spielstand gefunden. Fortsetzen? (j/n): [/]").strip().lower() if choice in ("j", "ja", "y", "yes", ""): console.print("[green]Lade Spielstand...[/]") engine = GameEngine.load(save_path) console.print(f"[dim]Willkommen zurück, {engine.character.name} (Stufe {engine.character.level})![/]") time.sleep(1) run_game(engine, save_path) return # New game char = create_character() engine = GameEngine(char) run_game(engine, save_path) if __name__ == "__main__": main()