# 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. ```