Fortschritts-Quest v1.0 — Automatisches RPG mit lokalem LLM
This commit is contained in:
+243
@@ -0,0 +1,243 @@
|
||||
"""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):
|
||||
log_text.append(entry + "\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()
|
||||
Reference in New Issue
Block a user