diff --git a/Semesterprojekt/docs/superpowers/plans/2026-06-01-tutorial.md b/Semesterprojekt/docs/superpowers/plans/2026-06-01-tutorial.md new file mode 100644 index 0000000..bd8d10a --- /dev/null +++ b/Semesterprojekt/docs/superpowers/plans/2026-06-01-tutorial.md @@ -0,0 +1,822 @@ +# Interactive Start-of-Game Tutorial Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add an interactive, skippable start-of-game walkthrough that makes the player perform the core commands (look→examine→take→inventory→use→go→go-to) in the real starting room, then lists the rest, shown only on New Game. + +**Architecture:** A stateful `TutorialGuide` (game package) is layered onto the real `Game` loop (optional; null in normal/loaded play). It prints an instruction, and after each executed command checks — via alias-aware registry lookup, with movement steps requiring an actual room change — whether the step is satisfied (confirm + advance) or not (gentle hint). Steps come from an optional `tutorial.yaml` loaded by a standalone `TutorialLoader` into a `Tutorial`/`TutorialStep` model. `skip` ends it. `App` wires the guide only for New Game. + +**Tech Stack:** Java 25, Maven, Jackson (YAML), JUnit 5 + AssertJ, Lombok. + +--- + +## File Structure + +**Create:** +- `model/Tutorial.java`, `model/TutorialStep.java` — domain records. +- `loader/dto/TutorialDto.java`, `loader/dto/TutorialStepDto.java` — YAML DTOs. +- `loader/TutorialLoader.java` — reads optional `/world/tutorial.yaml` → `Tutorial`. +- `game/TutorialGuide.java` — the guide. +- `src/main/resources/world/tutorial.yaml` — intro + 7 steps + closing tips. +- `src/test/resources/world/tutorial.yaml` — small 2-step loader fixture. + +**Modify:** +- `game/Game.java` — optional guide field + `setTutorialGuide` + loop hooks. +- `App.java` — `play(..., withTutorial)`, build guide on New Game. +- `docs/enhancement-ideas.md` — note the tutorial. + +--- + +## Task 1: Tutorial model, DTO, loader, content + +**Files:** +- Create: `src/main/java/thb/jeanluc/adventure/model/Tutorial.java` +- Create: `src/main/java/thb/jeanluc/adventure/model/TutorialStep.java` +- Create: `src/main/java/thb/jeanluc/adventure/loader/dto/TutorialDto.java` +- Create: `src/main/java/thb/jeanluc/adventure/loader/dto/TutorialStepDto.java` +- Create: `src/main/java/thb/jeanluc/adventure/loader/TutorialLoader.java` +- Create: `src/main/resources/world/tutorial.yaml` +- Create: `src/test/resources/world/tutorial.yaml` +- Test: `src/test/java/thb/jeanluc/adventure/loader/TutorialLoaderTest.java` + +- [ ] **Step 1: Write the failing test** + +`src/test/java/thb/jeanluc/adventure/loader/TutorialLoaderTest.java`: +```java +package thb.jeanluc.adventure.loader; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.model.Tutorial; + +import static org.assertj.core.api.Assertions.assertThat; + +class TutorialLoaderTest { + + @Test + void loadsTheTestFixture() { + Tutorial t = new TutorialLoader().load(); // test-classpath /world/tutorial.yaml shadows prod + assertThat(t.isEmpty()).isFalse(); + assertThat(t.intro()).isNotBlank(); + assertThat(t.closingTips()).isNotBlank(); + assertThat(t.steps()).hasSize(2); + assertThat(t.steps().getFirst().expect()).isEqualTo("look"); + assertThat(t.steps().getFirst().minArgs()).isZero(); + } + + @Test + void missingFileReturnsEmptyTutorial() { + Tutorial t = new TutorialLoader("/no-such-base").load(); + assertThat(t.isEmpty()).isTrue(); + assertThat(t.steps()).isEmpty(); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn -q test -Dtest=TutorialLoaderTest` +Expected: FAIL — `Tutorial`/`TutorialLoader` do not exist. + +- [ ] **Step 3: Create the model records** + +`src/main/java/thb/jeanluc/adventure/model/TutorialStep.java`: +```java +package thb.jeanluc.adventure.model; + +/** + * One interactive tutorial step: an instruction, the command that satisfies it + * ({@code expect} is a verb name like "look", or "go_direction"/"go_to_room"), + * a minimum argument count, a success confirmation, and a hint for a valid but + * wrong command. + */ +public record TutorialStep( + String instruction, + String expect, + int minArgs, + String confirm, + String hint +) { +} +``` + +`src/main/java/thb/jeanluc/adventure/model/Tutorial.java`: +```java +package thb.jeanluc.adventure.model; + +import java.util.List; + +/** Onboarding walkthrough: an intro, ordered steps, and closing tips. */ +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(); + } + + /** An absent tutorial (no steps). */ + public static Tutorial none() { + return new Tutorial(null, List.of(), null); + } +} +``` + +- [ ] **Step 4: Create the DTOs** + +`src/main/java/thb/jeanluc/adventure/loader/dto/TutorialStepDto.java`: +```java +package thb.jeanluc.adventure.loader.dto; + +/** YAML representation of a tutorial step. */ +public record TutorialStepDto( + String instruction, + String expect, + Integer minArgs, + String confirm, + String hint +) { +} +``` + +`src/main/java/thb/jeanluc/adventure/loader/dto/TutorialDto.java`: +```java +package thb.jeanluc.adventure.loader.dto; + +import java.util.List; + +/** YAML representation of the tutorial document. */ +public record TutorialDto(String intro, List steps, String closingTips) { +} +``` + +- [ ] **Step 5: Create `TutorialLoader`** + +`src/main/java/thb/jeanluc/adventure/loader/TutorialLoader.java`: +```java +package thb.jeanluc.adventure.loader; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import thb.jeanluc.adventure.loader.dto.TutorialDto; +import thb.jeanluc.adventure.loader.dto.TutorialStepDto; +import thb.jeanluc.adventure.model.Tutorial; +import thb.jeanluc.adventure.model.TutorialStep; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Loads the optional onboarding tutorial from {@code /tutorial.yaml}. + * Kept separate from {@link WorldLoader}: the tutorial is onboarding, not world + * content. Returns {@link Tutorial#none()} when the file is absent. + */ +public class TutorialLoader { + + /** Default classpath base directory. */ + public static final String DEFAULT_BASE = "/world"; + + private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory()); + private final String basePath; + + public TutorialLoader() { + this(DEFAULT_BASE); + } + + public TutorialLoader(String basePath) { + this.basePath = basePath; + } + + /** Reads the tutorial; absent file → {@link Tutorial#none()}. */ + public Tutorial load() { + String resource = basePath + "/tutorial.yaml"; + try (InputStream in = getClass().getResourceAsStream(resource)) { + if (in == null) { + return Tutorial.none(); + } + TutorialDto dto = yaml.readValue(in, TutorialDto.class); + if (dto == null) { + return Tutorial.none(); + } + List steps = new ArrayList<>(); + if (dto.steps() != null) { + for (TutorialStepDto s : dto.steps()) { + steps.add(new TutorialStep( + s.instruction(), + s.expect(), + s.minArgs() == null ? 0 : s.minArgs(), + s.confirm(), + s.hint())); + } + } + return new Tutorial(dto.intro(), steps, dto.closingTips()); + } catch (IOException e) { + throw new WorldLoadException("Failed to parse " + resource, e); + } + } +} +``` + +- [ ] **Step 6: Create the production `tutorial.yaml`** + +`src/main/resources/world/tutorial.yaml`: +```yaml +intro: | + Welcome to the manor. Let me show you the ropes — just type the commands as + I describe them. (Type 'skip' at any time to jump straight into the game.) +steps: + - instruction: "First, look around the room: type 'look'." + expect: look + confirm: "Good — 'look' (or 'l') re-describes wherever you are." + hint: "When you're ready, just type: look" + - instruction: "Examine something you noticed: '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. Things 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 ' — try lighting the lamp." + expect: use + minArgs: 1 + confirm: "Some items toggle, read, or react when you use them." + hint: "Use something, e.g.: use lamp" + - instruction: "Now move to another room: 'go ' (try 'go north')." + expect: go_direction + confirm: "You can move north / south / east / west between connected rooms." + hint: "Head to a connected room, e.g.: go north" + - instruction: "Travel back to a room you've already 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. +``` + +- [ ] **Step 7: Create the test fixture `tutorial.yaml`** + +`src/test/resources/world/tutorial.yaml` (small, deterministic for the loader test): +```yaml +intro: | + Test intro. +steps: + - instruction: "Look: type 'look'." + expect: look + confirm: "Looked." + hint: "Type look." + - instruction: "Take: type 'take '." + expect: take + minArgs: 1 + confirm: "Took it." + hint: "Take something." +closingTips: | + Test tips. +``` + +- [ ] **Step 8: Run tests to verify they pass** + +Run: `mvn -q test -Dtest=TutorialLoaderTest` +Expected: PASS (2 tests). + +- [ ] **Step 9: Commit** + +```bash +git add src/main/java/thb/jeanluc/adventure/model/Tutorial.java \ + src/main/java/thb/jeanluc/adventure/model/TutorialStep.java \ + src/main/java/thb/jeanluc/adventure/loader/dto/TutorialDto.java \ + src/main/java/thb/jeanluc/adventure/loader/dto/TutorialStepDto.java \ + src/main/java/thb/jeanluc/adventure/loader/TutorialLoader.java \ + src/main/resources/world/tutorial.yaml \ + src/test/resources/world/tutorial.yaml \ + src/test/java/thb/jeanluc/adventure/loader/TutorialLoaderTest.java +git commit -m "feat(tutorial): Tutorial model + TutorialLoader + tutorial.yaml content" +``` + +--- + +## Task 2: `TutorialGuide` + +**Files:** +- Create: `src/main/java/thb/jeanluc/adventure/game/TutorialGuide.java` +- Test: `src/test/java/thb/jeanluc/adventure/game/TutorialGuideTest.java` + +- [ ] **Step 1: Write the failing test** + +`src/test/java/thb/jeanluc/adventure/game/TutorialGuideTest.java`: +```java +package thb.jeanluc.adventure.game; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.command.CommandParser; +import thb.jeanluc.adventure.command.CommandRegistry; +import thb.jeanluc.adventure.command.ParsedCommand; +import thb.jeanluc.adventure.command.impl.ExamineCommand; +import thb.jeanluc.adventure.command.impl.GoCommand; +import thb.jeanluc.adventure.command.impl.InventoryCommand; +import thb.jeanluc.adventure.command.impl.LookCommand; +import thb.jeanluc.adventure.command.impl.TakeCommand; +import thb.jeanluc.adventure.command.impl.UseCommand; +import thb.jeanluc.adventure.io.TestIO; +import thb.jeanluc.adventure.model.Direction; +import thb.jeanluc.adventure.model.Player; +import thb.jeanluc.adventure.model.Room; +import thb.jeanluc.adventure.model.Tutorial; +import thb.jeanluc.adventure.model.TutorialStep; +import thb.jeanluc.adventure.model.World; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class TutorialGuideTest { + + private CommandRegistry registry() { + CommandRegistry r = new CommandRegistry(); + r.register(new LookCommand(), "look", "l"); + r.register(new ExamineCommand(), "examine", "x"); + r.register(new TakeCommand(), "take", "get"); + r.register(new InventoryCommand(), "inventory", "i"); + r.register(new UseCommand(), "use"); + r.register(new GoCommand(), "go", "move", "walk"); + return r; + } + + private final Room kitchen = new Room("kitchen", "Kitchen", "d"); + private final Room hallway = new Room("hallway", "Hallway", "d"); + + private GameContext ctx() { + kitchen.addExit(Direction.NORTH, hallway); + Map rooms = new LinkedHashMap<>(); + rooms.put("kitchen", kitchen); + rooms.put("hallway", hallway); + World w = new World(rooms, Map.of(), Map.of(), "t", "w"); + Player p = new Player(kitchen, 0); + return new GameContext(w, p, new TestIO()); + } + + private ParsedCommand parse(String s) { + return new CommandParser().parse(s); + } + + private String out(GameContext ctx) { + return ((TestIO) ctx.getIo()).allOutput(); + } + + private TutorialStep step(String instr, String expect, int minArgs) { + return new TutorialStep(instr, expect, minArgs, "OK:" + expect, "HINT:" + expect); + } + + @Test + void beginPrintsIntroAndFirstInstruction() { + GameContext ctx = ctx(); + Tutorial t = new Tutorial("INTRO", List.of(step("do look", "look", 0)), "TIPS"); + TutorialGuide g = new TutorialGuide(t, registry()); + g.begin(ctx); + assertThat(out(ctx)).contains("INTRO").contains("do look"); + assertThat(g.isActive()).isTrue(); + } + + @Test + void aliasSatisfiesVerbStepAndAdvancesToTips() { + GameContext ctx = ctx(); + Tutorial t = new Tutorial("INTRO", List.of(step("do look", "look", 0)), "TIPS"); + TutorialGuide g = new TutorialGuide(t, registry()); + g.begin(ctx); + g.onCommand(ctx, parse("l")); // alias of look + assertThat(out(ctx)).contains("OK:look").contains("TIPS"); + assertThat(g.isActive()).isFalse(); + } + + @Test + void minArgsGatesTakeStep() { + GameContext ctx = ctx(); + Tutorial t = new Tutorial("I", List.of(step("take it", "take", 1)), "TIPS"); + TutorialGuide g = new TutorialGuide(t, registry()); + g.begin(ctx); + g.onCommand(ctx, parse("take")); // no arg -> hint, not satisfied + assertThat(out(ctx)).contains("HINT:take"); + assertThat(g.isActive()).isTrue(); + g.onCommand(ctx, parse("take lamp")); // satisfied + assertThat(out(ctx)).contains("OK:take"); + } + + @Test + void goDirectionRequiresAnActualMove() { + GameContext ctx = ctx(); + Tutorial t = new Tutorial("I", List.of(step("move", "go_direction", 0)), "TIPS"); + TutorialGuide g = new TutorialGuide(t, registry()); + g.begin(ctx); + // typed a direction but did NOT move -> hint + g.onCommand(ctx, parse("go north")); + assertThat(out(ctx)).contains("HINT:go_direction"); + assertThat(g.isActive()).isTrue(); + // now actually move, then the same command satisfies + ctx.getPlayer().setCurrentRoom(hallway); + g.onCommand(ctx, parse("go north")); + assertThat(out(ctx)).contains("OK:go_direction"); + assertThat(g.isActive()).isFalse(); + } + + @Test + void goToRoomNeedsNonDirectionArgAndMove() { + GameContext ctx = ctx(); + Tutorial t = new Tutorial("I", List.of(step("travel", "go_to_room", 0)), "TIPS"); + TutorialGuide g = new TutorialGuide(t, registry()); + g.begin(ctx); + ctx.getPlayer().setCurrentRoom(hallway); // moved + g.onCommand(ctx, parse("go kitchen")); // "kitchen" is not a direction + assertThat(out(ctx)).contains("OK:go_to_room"); + } + + @Test + void skipDeactivates() { + GameContext ctx = ctx(); + Tutorial t = new Tutorial("I", List.of(step("do look", "look", 0)), "TIPS"); + TutorialGuide g = new TutorialGuide(t, registry()); + g.begin(ctx); + g.skip(ctx); + assertThat(out(ctx)).contains("Tutorial skipped"); + assertThat(g.isActive()).isFalse(); + } + + @Test + void emptyTutorialIsInactive() { + TutorialGuide g = new TutorialGuide(Tutorial.none(), registry()); + assertThat(g.isActive()).isFalse(); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn -q test -Dtest=TutorialGuideTest` +Expected: FAIL — `TutorialGuide` does not exist. + +- [ ] **Step 3: Create `TutorialGuide`** + +`src/main/java/thb/jeanluc/adventure/game/TutorialGuide.java`: +```java +package thb.jeanluc.adventure.game; + +import thb.jeanluc.adventure.command.Command; +import thb.jeanluc.adventure.command.CommandRegistry; +import thb.jeanluc.adventure.command.ParsedCommand; +import thb.jeanluc.adventure.model.Direction; +import thb.jeanluc.adventure.model.Room; +import thb.jeanluc.adventure.model.Tutorial; +import thb.jeanluc.adventure.model.TutorialStep; + +import java.util.Optional; + +/** + * Drives the interactive start-of-game walkthrough, layered onto the real game + * loop. Prints an instruction, then on each executed command decides whether the + * current step is satisfied (confirm + advance) or not (hint). Movement steps + * require an actual room change. Alias-aware via the {@link CommandRegistry}. + */ +public final class TutorialGuide { + + private final Tutorial tutorial; + private final CommandRegistry registry; + private int index; + private boolean active; + private Room roomAtStepStart; + + public TutorialGuide(Tutorial tutorial, CommandRegistry registry) { + this.tutorial = tutorial; + this.registry = registry; + this.active = !tutorial.isEmpty(); + } + + public boolean isActive() { + return active; + } + + /** Prints the intro and the first instruction (call once at loop start). */ + public void begin(GameContext ctx) { + if (!active) { + return; + } + if (tutorial.intro() != null && !tutorial.intro().isBlank()) { + ctx.getIo().write(tutorial.intro()); + } + enterStep(ctx); + } + + /** Evaluates the current step against the command that was just executed. */ + 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()) { + if (tutorial.closingTips() != null && !tutorial.closingTips().isBlank()) { + ctx.getIo().write(tutorial.closingTips()); + } + active = false; + } else { + enterStep(ctx); + } + } else { + ctx.getIo().write(step.hint()); + } + } + + /** Ends the tutorial early (the 'skip' command). */ + public void skip(GameContext ctx) { + if (active) { + ctx.getIo().write("Tutorial skipped."); + active = false; + } + } + + private void enterStep(GameContext ctx) { + roomAtStepStart = ctx.getPlayer().getCurrentRoom(); + ctx.getIo().write(tutorial.steps().get(index).instruction()); + } + + private boolean satisfies(TutorialStep step, GameContext ctx, ParsedCommand parsed) { + String expect = step.expect(); + if ("go_direction".equals(expect)) { + return isGo(parsed) && firstIsDirection(parsed) && moved(ctx); + } + if ("go_to_room".equals(expect)) { + return isGo(parsed) && !firstIsDirection(parsed) && moved(ctx); + } + return sameCommand(parsed.verb(), expect) && parsed.args().size() >= step.minArgs(); + } + + private boolean isGo(ParsedCommand p) { + return sameCommand(p.verb(), "go") && !p.args().isEmpty(); + } + + private boolean firstIsDirection(ParsedCommand p) { + if (p.args().isEmpty()) { + return false; + } + try { + Direction.fromString(p.args().get(0)); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private boolean moved(GameContext ctx) { + return ctx.getPlayer().getCurrentRoom() != roomAtStepStart; + } + + private boolean sameCommand(String a, String b) { + Optional ca = registry.find(a); + Optional cb = registry.find(b); + return ca.isPresent() && cb.isPresent() && ca.get() == cb.get(); + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `mvn -q test -Dtest=TutorialGuideTest` +Expected: PASS (7 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/main/java/thb/jeanluc/adventure/game/TutorialGuide.java \ + src/test/java/thb/jeanluc/adventure/game/TutorialGuideTest.java +git commit -m "feat(game): TutorialGuide interactive walkthrough engine" +``` + +--- + +## Task 3: Wire the guide into `Game` and `App` + +**Files:** +- Modify: `src/main/java/thb/jeanluc/adventure/game/Game.java` +- Modify: `src/main/java/thb/jeanluc/adventure/App.java` +- Test: `src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java` + +- [ ] **Step 1: Write the failing test** + +`src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java`: +```java +package thb.jeanluc.adventure.game; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.command.CommandParser; +import thb.jeanluc.adventure.command.CommandRegistry; +import thb.jeanluc.adventure.command.impl.LookCommand; +import thb.jeanluc.adventure.io.TestIO; +import thb.jeanluc.adventure.model.Player; +import thb.jeanluc.adventure.model.Room; +import thb.jeanluc.adventure.model.Tutorial; +import thb.jeanluc.adventure.model.TutorialStep; +import thb.jeanluc.adventure.model.World; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class GameTutorialTest { + + @Test + void guideBeginsAndAdvancesThenSkipEndsViaLoop() { + Room kitchen = new Room("kitchen", "Kitchen", "d"); + World w = new World(Map.of("kitchen", kitchen), Map.of(), Map.of(), "t", "w"); + TestIO io = new TestIO(); + // scripted player input: a matching 'look', then 'skip'; then EOF (null) ends run() + io.enqueue("look").enqueue("skip"); + GameContext ctx = new GameContext(new GameSession(w, new Player(kitchen, 0), "s"), io); + + CommandRegistry registry = new CommandRegistry(); + registry.register(new LookCommand(), "look", "l"); + + Tutorial t = new Tutorial("INTRO", + List.of(new TutorialStep("do look", "look", 0, "STEP1-OK", "hint"), + new TutorialStep("do look again", "look", 0, "STEP2-OK", "hint")), + "TIPS"); + Game game = new Game(ctx, registry, new CommandParser()); + game.setTutorialGuide(new TutorialGuide(t, registry)); + + game.run(); + + String out = io.allOutput(); + assertThat(out).contains("INTRO"); // begin printed + assertThat(out).contains("STEP1-OK"); // first 'look' advanced step 1 + assertThat(out).contains("Tutorial skipped"); // 'skip' ended it before step 2 finished + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `mvn -q test -Dtest=GameTutorialTest` +Expected: FAIL — `Game.setTutorialGuide` does not exist. + +- [ ] **Step 3: Add the guide hooks to `Game`** + +In `src/main/java/thb/jeanluc/adventure/game/Game.java`: + +1. Add the import: +```java +import thb.jeanluc.adventure.game.TutorialGuide; +``` +(Same package — skip the import; reference `TutorialGuide` directly.) + +2. Add a field after `private boolean running = true;`: +```java + /** Optional onboarding guide; null in normal/loaded play. */ + private TutorialGuide tutorialGuide; + + /** Wires the interactive tutorial (New Game only). */ + public void setTutorialGuide(TutorialGuide tutorialGuide) { + this.tutorialGuide = tutorialGuide; + } +``` + +3. In `run()`, after the initial `publishHud(); maybeEnd();` and before `while (running) {`: +```java + if (tutorialGuide != null) { + tutorialGuide.begin(ctx); + } +``` + +4. Inside the loop, replace the body from the `parsed.verb().isEmpty()` check through the dispatch with this (adds the skip interception and the post-command hook): +```java + ParsedCommand parsed = parser.parse(input); + if (parsed.verb().isEmpty()) { + continue; + } + if (tutorialGuide != null && tutorialGuide.isActive() + && parsed.verb().equals("skip")) { + tutorialGuide.skip(ctx); + publishHud(); + continue; + } + Optional cmd = registry.find(parsed.verb()); + if (cmd.isEmpty()) { + ctx.getIo().write("I don't understand '" + parsed.verb() + "'. Type 'help'."); + } else { + cmd.get().execute(ctx, parsed.args()); + ctx.getSession().incrementTurn(); + if (ctx.getSession().getTurn() % 10 == 0) { + ctx.save(); + } + if (tutorialGuide != null) { + tutorialGuide.onCommand(ctx, parsed); + } + } + publishHud(); + maybeEnd(); +``` + +- [ ] **Step 4: Wire `App`** + +In `src/main/java/thb/jeanluc/adventure/App.java`: + +1. Add imports: +```java +import thb.jeanluc.adventure.game.TutorialGuide; +import thb.jeanluc.adventure.loader.TutorialLoader; +import thb.jeanluc.adventure.model.Tutorial; +``` + +2. Change the two `play(...)` call sites in `run()`: +```java + case NEW_GAME -> play(io, saves, newSession(io), true); +``` +and inside the LOAD branch: +```java + play(io, saves, saves.load(slot.slug()), false); +``` + +3. Change the `play` signature and add the guide wiring (after the `HelpCommand` is registered, before the banner): +```java + private static void play(GameIO io, SaveService saves, GameSession session, boolean withTutorial) { +``` +and, immediately after `registry.register(new HelpCommand(registry), "help", "?");`: +```java + if (withTutorial) { + Tutorial tutorial = new TutorialLoader().load(); + if (!tutorial.isEmpty()) { + game.setTutorialGuide(new TutorialGuide(tutorial, registry)); + } + } +``` + +- [ ] **Step 5: Run tests + full suite** + +Run: `mvn -q test -Dtest=GameTutorialTest` +Expected: PASS. +Run: `mvn -q clean test` +Expected: BUILD SUCCESS, all green (existing GameTest / loop tests unaffected — guide is null there). + +- [ ] **Step 6: Manual console smoke (optional)** + +Run: `mvn -q -DskipTests compile dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt >/dev/null 2>&1` then +`printf '1\nHero\nlook\nskip\nquit\n4\n' | java -cp "target/classes:$(cat /tmp/cp.txt)" thb.jeanluc.adventure.App` +Expected: New Game → name → room shown → tutorial intro + "look" instruction → after `look`, confirmation + next instruction → `skip` → "Tutorial skipped." → normal play → `quit` → menu → Quit. + +- [ ] **Step 7: Commit** + +```bash +git add src/main/java/thb/jeanluc/adventure/game/Game.java \ + src/main/java/thb/jeanluc/adventure/App.java \ + src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java +git commit -m "feat: wire TutorialGuide into the game loop; show on New Game only" +``` + +--- + +## Task 4: Docs + +**Files:** +- Modify: `docs/enhancement-ideas.md` + +- [ ] **Step 1: Add a tutorial note** + +In `docs/enhancement-ideas.md`, under the "Festgelegte Erweiterungs-Entscheidungen" +area (near the pathfinding ✅ note), add: + +> ✅ **Tutorial** umgesetzt (Branch `feature/tutorial`): interaktiver Walkthrough +> beim Neuen Spiel (look→examine→take→inventory→use→go→go to), verb-/alias-basierte +> Abschluss-Erkennung (Bewegungs-Schritte verlangen echten Raumwechsel), Abschluss- +> Tipps, jederzeit per `skip` abbrechbar. Datengetrieben (`tutorial.yaml`, +> `TutorialLoader`), als `TutorialGuide` in den Game-Loop eingehängt; nur bei Neuem +> Spiel, nicht beim Laden. + +- [ ] **Step 2: Commit** + +```bash +git add docs/enhancement-ideas.md +git commit -m "docs: note interactive start-of-game tutorial" +``` + +--- + +## Final Verification + +- [ ] `mvn -q clean test` — all tests pass (197 prior + TutorialLoader/TutorialGuide/GameTutorial suites). +- [ ] Manual console smoke (Task 3 Step 6) behaves as described. +- [ ] Manual: Load a saved game → NO tutorial appears. +- [ ] `git status` clean. +```