From c315d3f4b78a4429581dc24b6f8432afa7de6692 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 21:53:27 +0200 Subject: [PATCH] feat: wire TutorialGuide into the game loop; show on New Game only Co-Authored-By: Claude Sonnet 4.6 --- .../main/java/thb/jeanluc/adventure/App.java | 16 +++++-- .../java/thb/jeanluc/adventure/game/Game.java | 20 ++++++++ .../adventure/game/GameTutorialTest.java | 47 +++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java index 8444308..ff6367d 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java @@ -21,6 +21,9 @@ import thb.jeanluc.adventure.command.impl.UseCommand; import thb.jeanluc.adventure.game.Game; import thb.jeanluc.adventure.game.GameContext; import thb.jeanluc.adventure.game.GameSession; +import thb.jeanluc.adventure.game.TutorialGuide; +import thb.jeanluc.adventure.loader.TutorialLoader; +import thb.jeanluc.adventure.model.Tutorial; import thb.jeanluc.adventure.io.ConsoleIO; import thb.jeanluc.adventure.io.GameIO; import thb.jeanluc.adventure.io.text.Banner; @@ -70,12 +73,12 @@ public final class App { boolean running = true; while (running) { switch (MainMenu.show(io)) { - case NEW_GAME -> play(io, saves, newSession(io)); + case NEW_GAME -> play(io, saves, newSession(io), true); case LOAD -> { SaveSlotInfo slot = MainMenu.pickSlot(io, saves); if (slot != null) { try { - play(io, saves, saves.load(slot.slug())); + play(io, saves, saves.load(slot.slug()), false); } catch (SaveException e) { io.write("Could not load: " + e.getUserMessage()); } @@ -96,7 +99,7 @@ public final class App { } /** Builds the registry, runs one game to completion, autosaving to its slot. */ - private static void play(GameIO io, SaveService saves, GameSession session) { + private static void play(GameIO io, SaveService saves, GameSession session, boolean withTutorial) { GameContext ctx = new GameContext(session, io); ctx.setSaveCallback(() -> { try { @@ -128,6 +131,13 @@ public final class App { registry.register(menu, "menu", "quit", "exit"); registry.register(new HelpCommand(registry), "help", "?"); + if (withTutorial) { + Tutorial tutorial = new TutorialLoader().load(); + if (!tutorial.isEmpty()) { + game.setTutorialGuide(new TutorialGuide(tutorial, registry)); + } + } + io.print(Banner.welcome(session.getWorld().getTitle())); io.write(session.getWorld().getWelcomeMessage()); new LookCommand().execute(ctx, List.of()); diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Game.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Game.java index 68f5bb1..a56ee9a 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Game.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Game.java @@ -32,12 +32,23 @@ public class Game { /** Loop flag. Flipped to false by {@link #stop()}. */ private boolean running = true; + /** 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; + } + /** * Runs the loop until {@link #stop()} is called or input is exhausted. */ public void run() { publishHud(); maybeEnd(); + if (tutorialGuide != null) { + tutorialGuide.begin(ctx); + } while (running) { String input = ctx.getIo().readLine(); if (input == null) { @@ -47,6 +58,12 @@ public class Game { 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'."); @@ -56,6 +73,9 @@ public class Game { if (ctx.getSession().getTurn() % 10 == 0) { ctx.save(); } + if (tutorialGuide != null) { + tutorialGuide.onCommand(ctx, parsed); + } } publishHud(); maybeEnd(); diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java new file mode 100644 index 0000000..43a10ea --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTutorialTest.java @@ -0,0 +1,47 @@ +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 + } +}