Files
fortschritts-quest/src/main.py
T
arch_agent f0e30f56ed fix: Rich Markup im Ereignis-Log rendern (Text.from_markup)
Text.append() behandelte [red]..[/] als Literaltext.
Text.from_markup() parst Rich-Markup-Tags korrekt.
2026-07-25 20:11:04 +02:00

245 lines
9.0 KiB
Python

"""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()