# Spec: Interaktives Start-Tutorial Stand: 2026-06-01. Dritte der verbleibenden Mechaniken (nach `use X on Y` und Pathfinding). Setzt das vom Nutzer gewünschte **interaktive Tutorial beim Spielstart, abbrechbar per `skip`** um. Baut auf dem geteilten Game-Loop, `CommandRegistry`/`CommandParser`, `GameIO` und dem datengetriebenen Loader auf. ## 1. Kontext & Ziel Neue Spieler kennen die Befehle nicht. Ziel: ein **interaktiver Walkthrough** zu Beginn eines **Neuen Spiels**, der den Spieler im **echten Startraum** Schritt für Schritt die Kernbefehle ausführen lässt (er wartet, bis der jeweilige Befehl kam), mit Bestätigung/Hinweisen, und am Ende die übrigen Befehle als Tipps nennt. Jederzeit per `skip` überspringbar. Geladene Spielstände zeigen kein Tutorial. Bestätigte Entscheidungen (Brainstorming 2026-06-01): - **Stil**: interaktiv (wait-for-it) im echten Spiel, nicht in einer Sandbox. - **Kern-Bogen** (interaktiv, in Reihenfolge): `look` → `examine` → `take` → `inventory` → `use` → `go` (Richtung) → `go to` (Pathfinding). Danach **Abschluss-Tipps** für den Rest (read, drop, talk/give, use X on Y, map, quests, save, menu). - **Abschluss-Erkennung verb-basiert** (alias-fähig): jede gültige Nutzung des erwarteten Verbs zählt; optional `minArgs`. - **Datengetrieben**: `tutorial.yaml`; Logik (Verb-/Richtungs-Matching) im Code. - **Häufigkeit**: bei jedem Neuen Spiel; `skip` überspringt; keine Persistenz. - **Falscher (aber gültiger) Befehl**: wird **ausgeführt** (echtes Spiel) + sanfter Hinweis, Schritt rückt **nicht** vor. - **Platzierung**: **nach** Banner + Willkommen + erstem Raum-`look`. ## 2. Scope **In Scope:** - `TutorialGuide` (game), in den Game-Loop eingehängt (optional; null = kein Tutorial). - `Tutorial`/`TutorialStep` (model) + `TutorialDto`/`TutorialStepDto` (loader/dto) + `TutorialLoader` (optionales `/world/tutorial.yaml`, eigener Loader — nicht in `World`). - `Game`: optionaler Guide (`setTutorialGuide`) + `begin`/`onCommand`/`skip`-Anbindung. - `App.play(..., withTutorial)`: Guide nur beim Neuen Spiel bauen. - `tutorial.yaml`: Intro, 7 Kern-Schritte, Abschluss-Tipps. - Tests: Guide-Logik, Loader, Game-Integration. **Out of Scope:** - Persistenz „Tutorial gesehen" / Settings-Toggle (eigene spätere Option). - Sandbox-Welt / Zurücksetzen von Tutorial-Aktionen (es ist das echte Spiel). - Erzwungenes `use X on Y` / `talk` etc. (nur in Abschluss-Tipps genannt). - GUI-Sonderdarstellung (Tutorial nutzt die normale styled Ausgabe; `skip` per Eingabe). ## 3. Datenmodell (model) ```java public record TutorialStep( String instruction, // an den Spieler: was tun String expect, // Verb-Name ("look"/"examine"/...) ODER "go_direction" / "go_to_room" int minArgs, // Mindest-Argumentzahl (z.B. 1 für take/examine/use); 0 default String confirm, // Bestätigung bei Erfolg String hint // sanfter Hinweis bei gültigem-aber-falschem Befehl ) {} public record Tutorial(String intro, List steps, String closingTips) { public Tutorial { steps = steps == null ? List.of() : List.copyOf(steps); } public boolean isEmpty() { return steps.isEmpty(); } public static Tutorial none() { return new Tutorial(null, List.of(), null); } } ``` `tutorial.yaml` (ein Mapping, optional): ```yaml intro: | Welcome. Let me show you the ropes — type the commands as I describe them. (Type 'skip' at any time to jump straight into the game.) steps: - instruction: "Look around the room: type 'look'." expect: look confirm: "Good — 'look' (or 'l') always re-describes where you are." hint: "When you're ready, just type: look" - instruction: "Examine something you see: 'examine ' (or 'x ')." expect: examine minArgs: 1 confirm: "That's how you inspect items, exits and people up close." hint: "Try examining something, e.g.: examine lamp" - instruction: "Pick something up: 'take '." expect: take minArgs: 1 confirm: "Taken. Items you carry go into your inventory." hint: "Pick something up, e.g.: take lamp" - instruction: "Check what you're carrying: 'inventory' (or 'i')." expect: inventory confirm: "That's your inventory." hint: "Type: inventory" - instruction: "Use an item: 'use ' (e.g. light the lamp)." expect: use minArgs: 1 confirm: "Some items toggle, read or react when used." hint: "Use something, e.g.: use lamp" - instruction: "Move to another room: 'go ' (try 'go north')." expect: go_direction confirm: "You can move north/south/east/west between connected rooms." hint: "Head somewhere, e.g.: go north" - instruction: "Travel back to a room you've seen: 'go to '." expect: go_to_room confirm: "Nice — 'go to ' auto-walks to any room you've already visited." hint: "Try: go to " closingTips: | That's the core. A few more you'll want: read · drop · talk to · give to · use on (combine things) · map · quests · save · menu. Type 'help' anytime. Good luck. ``` ## 4. `TutorialLoader` (loader) - `loader/dto/TutorialDto` (`intro`, `List steps`, `closingTips`), `loader/dto/TutorialStepDto` (`instruction`, `expect`, `Integer minArgs`, `confirm`, `hint`). - `TutorialLoader.load(basePath)` → `Tutorial`: liest `basePath + "/tutorial.yaml"` optional (fehlt → `Tutorial.none()`), mappt DTO→Modell (`minArgs` null → 0). Nutzt einen eigenen Jackson-`YAMLFactory`-Mapper wie `WorldLoader`. Default-Base `/world` (überschreibbar für Tests). Parse-Fehler → `WorldLoadException`. - Bewusst **getrennt von `World`**: Tutorial ist Onboarding, kein Welt-Inhalt. ## 5. `TutorialGuide` (game) Zustandsbehaftet (ein laufendes Tutorial), hält die Schritte + den Index + die `CommandRegistry` (für alias-fähiges Verb-Matching). ```java public final class TutorialGuide { private final Tutorial tutorial; private final CommandRegistry registry; private int index = 0; private boolean active; private Room roomAtStepStart; // zum Erkennen echter Bewegung public TutorialGuide(Tutorial tutorial, CommandRegistry registry) { this.tutorial = tutorial; this.registry = registry; this.active = !tutorial.isEmpty(); } public boolean isActive() { return active; } /** Druckt Intro + erste Anweisung (am Loop-Start, nach dem ersten look). */ public void begin(GameContext ctx) { if (active) { print intro; enterStep(ctx); } } /** Nach jedem ausgeführten Befehl: prüft den aktuellen Schritt. */ public void onCommand(GameContext ctx, ParsedCommand parsed) { if (!active) return; TutorialStep step = tutorial.steps().get(index); if (satisfies(step, ctx, parsed)) { ctx.getIo().write(step.confirm()); index++; if (index >= tutorial.steps().size()) { ctx.getIo().print(closingTips); active = false; } else enterStep(ctx); } else { ctx.getIo().write(step.hint()); // gültiger-aber-falscher Befehl → Nudge } } /** 'skip' während des Tutorials: Abbruch ohne Zug. */ public void skip(GameContext ctx) { if (active) { ctx.getIo().write("Tutorial skipped."); active = false; } } /** Merkt den aktuellen Raum (für Bewegungs-Erkennung) und druckt die Anweisung. */ private void enterStep(GameContext ctx) { roomAtStepStart = ctx.getPlayer().getCurrentRoom(); ctx.getIo().write(tutorial.steps().get(index).instruction()); } } ``` **`satisfies(step, ctx, parsed)`**: - `expect == "go_direction"` → `sameCommand(parsed.verb(), "go")` **und** `args` nicht leer **und** erstes Arg ist eine Richtung (`Direction.fromString` wirft nicht) **und** der Spieler ist **tatsächlich umgezogen** (`ctx.getPlayer().getCurrentRoom() != roomAtStepStart`). - `expect == "go_to_room"` → `sameCommand(parsed.verb(), "go")` **und** `args` nicht leer **und** erstes Arg ist **keine** Richtung **und** der Spieler ist tatsächlich umgezogen. - sonst (Verb-Name) → `sameCommand(parsed.verb(), step.expect())` **und** `args.size() >= step.minArgs()`. - `sameCommand(a, b)` = `registry.find(a)` und `registry.find(b)` beide präsent und **dieselbe** Command-Instanz (alias-fähig: `l`==`look`, `x`==`examine`, `get`==`take`, `i`==`inventory`). Die Bewegungs-Bedingung verhindert, dass ein **blockierter** Zug (z.B. `go south` in den dunklen Dungeon ohne Licht → kein Raumwechsel) den Schritt vorrückt — wichtig, damit für `go_to_room` danach wirklich zwei Räume besucht sind. Hinweis: `onCommand` wird nur nach einem **erfolgreich aufgelösten** Befehl gerufen; unbekannte Eingaben behandelt der Loop bereits („I don't understand …"). ## 6. Game-Loop-Anbindung (game/Game.java) - Neues optionales Feld `private TutorialGuide tutorialGuide;` + `setTutorialGuide(...)` (Zwei-Phasen-Wiring wie `QuitCommand`/`MenuCommand`). - In `run()` **vor** der `while`-Schleife (nach dem initialen `publishHud`/`maybeEnd`): `if (tutorialGuide != null) tutorialGuide.begin(ctx);` - In der Schleife, **vor** dem Dispatch: wenn `tutorialGuide != null && tutorialGuide.isActive() && parsed.verb().equals("skip")` → `tutorialGuide.skip(ctx); publishHud(); continue;` (kein `turn++`). - **Nach** `cmd.get().execute(...); turn++;`: `if (tutorialGuide != null) tutorialGuide.onCommand(ctx, parsed);` Der Guide ist in normalem/geladenem Spiel `null` → kein Overhead, kein Verhalten. ## 7. App-Anbindung (App.java) - `App.run` lädt das Tutorial einmal beim Start: `Tutorial tutorial = new TutorialLoader().load();` (wie Settings/SaveService). - `play(io, saves, session, boolean withTutorial)`: nach dem Registry-Aufbau, wenn `withTutorial && !tutorial.isEmpty()` → `game.setTutorialGuide(new TutorialGuide( tutorial, registry))`. - `case NEW_GAME -> play(io, saves, newSession(io), true);` `LOAD -> play(io, saves, saves.load(slot.slug()), false);` - Reihenfolge in `play`: Banner → Willkommen → erstes `look` → `game.run()` (darin `guide.begin`). Damit erscheint das Tutorial **nach** dem ersten Raumbild. ## 8. Flow & Meldungen - Neues Spiel → Name → Banner/Willkommen → Raum-`look` → Intro + 1. Anweisung. - Spieler tippt Befehle (echte Ausführung); passender Befehl → `confirm` + nächste Anweisung; falscher (gültiger) → `hint`. Nach dem letzten Schritt → `closingTips`, Guide inaktiv, normales Spiel läuft weiter (Spieler steht, wo das Tutorial endete). - `skip` jederzeit → „Tutorial skipped." → normales Spiel. - Während des Tutorials laufen HUD/Karte/Quests/Autosave normal (es ist der echte Loop). ## 9. Tests - **`TutorialGuideTest`** (Fake-`CommandRegistry` mit echten Command-Instanzen oder reale Registry; `TestIO`): - Alias-Match: `l` erfüllt `look`-Schritt; `x` erfüllt `examine`. - `minArgs`: `take` ohne Arg erfüllt **nicht** (minArgs 1), `take lamp` schon. - `go_direction` (Arg = Richtung) vs `go_to_room` (Arg = keine Richtung). - Falscher gültiger Befehl → `hint`, kein Vorrücken. - Vorrücken + `confirm`; nach letztem Schritt → `closingTips` + `isActive()==false`. - `skip` → inaktiv, „Tutorial skipped.". - **`TutorialLoaderTest`**: lädt Fixture-`tutorial.yaml` → Intro/Steps/ClosingTips; fehlende Datei → `Tutorial.none()` (`isEmpty()`). - **`GameTutorialTest`** (Integration): kleine Welt + reale Registry + `TestIO` mit skript-Eingaben durch `game.run()`; prüft, dass `begin` druckt, Schritte bei passenden Befehlen vorrücken, und `skip` beendet. (App-/GUI-Wiring manuell.) - Konvention: keine GUI-/YAML-abhängigen Domain-Tests außer dem Loader-Fixture. ## 10. Architektur-Werte / Risiken - **Geteilter Loop bleibt zentral**: der Guide hängt sich minimal in `Game` ein; Konsole und GUI verhalten sich identisch (normale styled Ausgabe, `skip` per Eingabe). - **Datengetrieben**: Schritte/Texte in `tutorial.yaml`; nur das Matching ist Code. Verb-Matching nutzt die Registry (alias-fähig, keine hartkodierten Alias-Listen). - **Geringe Kopplung an Content**: Schritte sind verb-/richtungs-basiert (kein fixer Item-Name nötig); ändert sich der Start-Content, bleibt das Tutorial gültig. `go_to_room` funktioniert, weil nach dem `go`-Schritt zwei Räume besucht sind. - **Echtes Spiel, kein Sandbox**: Tutorial-Aktionen sind reale Züge (Turn/Quests/ Autosave laufen). Risiko: der Spieler „verbraucht" den ersten Raum — bewusst akzeptiert (es ist die echte Welt; nichts geht verloren). - **`skip`-Sonderfall**: nur während aktivem Tutorial abgefangen; außerhalb bleibt `skip` ein unbekanntes Verb (harmlos). - Risiko: `expect`-Tokens (`go_direction`/`go_to_room` + Verbnamen) sind ein kleines bespoke Schema; dokumentiert in `tutorial.yaml`-Kommentar und im Loader.