From 055a256b0348ec5f560a0f61b2eb904df2db6083 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 21:10:13 +0200 Subject: [PATCH] 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) --- .../specs/2026-06-01-tutorial-design.md | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 Semesterprojekt/docs/superpowers/specs/2026-06-01-tutorial-design.md diff --git a/Semesterprojekt/docs/superpowers/specs/2026-06-01-tutorial-design.md b/Semesterprojekt/docs/superpowers/specs/2026-06-01-tutorial-design.md new file mode 100644 index 0000000..c7eb349 --- /dev/null +++ b/Semesterprojekt/docs/superpowers/specs/2026-06-01-tutorial-design.md @@ -0,0 +1,254 @@ +# 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.