diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/TutorialGuide.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/TutorialGuide.java new file mode 100644 index 0000000..2ac91aa --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/TutorialGuide.java @@ -0,0 +1,119 @@ +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(); + } +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/TutorialGuideTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/TutorialGuideTest.java new file mode 100644 index 0000000..84b06f5 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/TutorialGuideTest.java @@ -0,0 +1,143 @@ +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(); + } +}