Files
Jander_Semester2/Semesterprojekt/docs/superpowers/specs/2026-06-01-tutorial-design.md
Jean-Luc Makiola 055a256b03 docs: spec for interactive start-of-game tutorial
Interactive walkthrough layered on the real game loop via a TutorialGuide
(optional in Game): core arc look->examine->take->inventory->use->go->go to,
verb-based alias-aware completion (movement steps require an actual room
change), closing tips, skippable via 'skip'. Data-driven tutorial.yaml +
separate TutorialLoader (not in World). Shown only on New Game.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:10:13 +02:00

13 KiB

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): lookexaminetakeinventoryusego (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)

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):

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).

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 lookgame.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.