# 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<TutorialStep> 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 <thing>' (or 'x <thing>')."
    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 <item>'."
    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 <item>' (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 <direction>' (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 <room>'."
    expect: go_to_room
    confirm: "Nice — 'go to <room>' auto-walks to any room you've already visited."
    hint: "Try: go to <a room you've already visited>"
closingTips: |
  That's the core. A few more you'll want:
  read · drop · talk to <npc> · give <item> to <npc> ·
  use <item> on <item> (combine things) · map · quests · save · menu.
  Type 'help' anytime. Good luck.
```

## 4. `TutorialLoader` (loader)

- `loader/dto/TutorialDto` (`intro`, `List<TutorialStepDto> 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.
