From 97e2809719f06c1f6e350efe3b805c2507f95e2e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 20:13:25 +0200 Subject: [PATCH] feat(command): go to auto-walk (BFS path, animated map, summary) Co-Authored-By: Claude Sonnet 4.6 --- .../adventure/command/impl/GoCommand.java | 92 +++++++++++++++--- .../command/impl/GoCommandPathTest.java | 93 +++++++++++++++++++ 2 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java index c98af8b..d5b589b 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java @@ -4,38 +4,47 @@ import thb.jeanluc.adventure.command.Command; import thb.jeanluc.adventure.game.Conditions; import thb.jeanluc.adventure.game.GameContext; import thb.jeanluc.adventure.game.Light; +import thb.jeanluc.adventure.game.Pathfinder; +import thb.jeanluc.adventure.map.MapLayout; import thb.jeanluc.adventure.model.Direction; import thb.jeanluc.adventure.model.ExitLock; import thb.jeanluc.adventure.model.Room; +import java.util.ArrayList; import java.util.List; import java.util.Optional; /** - * Moves the player into the room reachable in a given direction. - * Usage: {@code go }. + * Moves the player. {@code go } steps once to a connected room. + * {@code go to } auto-walks the shortest path (BFS) over already-visited + * rooms, honouring locked exits and darkness. Usage: {@code go } or + * {@code go to }. */ public class GoCommand implements Command { @Override public void execute(GameContext ctx, List args) { if (args.isEmpty()) { - ctx.getIo().write("Go where? Try 'go north'."); + ctx.getIo().write("Go where? Try 'go north' or 'go to '."); return; } - Direction dir; try { - dir = Direction.fromString(args.getFirst()); - } catch (IllegalArgumentException e) { - ctx.getIo().write("I don't know which way '" + args.getFirst() + "' is."); - return; + Direction dir = Direction.fromString(args.getFirst()); + moveDirection(ctx, dir); + } catch (IllegalArgumentException notADirection) { + goToRoom(ctx, args); } - Optional next = ctx.getPlayer().getCurrentRoom().getExit(dir); + } + + /** Single-step move in a compass direction (the original behaviour). */ + private void moveDirection(GameContext ctx, Direction dir) { + Room here = ctx.getPlayer().getCurrentRoom(); + Optional next = here.getExit(dir); if (next.isEmpty()) { ctx.getIo().write("You can't go " + dir.getLabel() + " from here."); return; } - ExitLock lock = ctx.getPlayer().getCurrentRoom().getExitLocks().get(dir); + ExitLock lock = here.getExitLocks().get(dir); if (lock != null && !Conditions.all(lock.requires(), ctx)) { ctx.getIo().write(lock.blocked()); return; @@ -49,8 +58,69 @@ public class GoCommand implements Command { new LookCommand().execute(ctx, List.of()); } + /** Resolve a visited room by id/name and auto-walk the BFS path to it. */ + private void goToRoom(GameContext ctx, List args) { + String name = String.join(" ", args).toLowerCase(); + Room target = resolveVisited(ctx, name); + if (target == null) { + ctx.getIo().write("You don't know any place called '" + name + "'."); + return; + } + Room start = ctx.getPlayer().getCurrentRoom(); + if (target == start) { + ctx.getIo().write("You're already in the " + target.getName() + "."); + return; + } + List path = Pathfinder.findPath(ctx, target); + if (path.isEmpty()) { + ctx.getIo().write("You can't find a way there right now."); + return; + } + List route = new ArrayList<>(); + route.add(start); + route.addAll(path); + for (Room step : path) { + ctx.getPlayer().setCurrentRoom(step); + ctx.getIo().travelStep(MapLayout.compute( + ctx.getWorld(), ctx.getPlayer().getVisitedRoomIds(), step)); + } + ctx.getIo().write(summary(route)); + new LookCommand().execute(ctx, List.of()); + } + + /** Finds a visited room whose id or display name matches {@code name}. */ + private Room resolveVisited(GameContext ctx, String name) { + if (ctx.getWorld() == null) { + return null; + } + for (String id : ctx.getPlayer().getVisitedRoomIds()) { + Room r = ctx.getWorld().getRooms().get(id); + if (r == null) { + continue; + } + if (id.equals(name) || r.getName().toLowerCase().equals(name)) { + return r; + } + } + return null; + } + + /** Names start, destination, and at most one middle waypoint. */ + private String summary(List route) { + int n = route.size(); + String start = route.get(0).getName(); + String dest = route.get(n - 1).getName(); + if (n >= 3) { + String middle = route.get(n / 2).getName(); + return "You make your way from the " + start + ", through the " + middle + + ", to the " + dest + "."; + } + return "You make your way from the " + start + " to the " + dest + "."; + } + @Override public String help() { - return "go - move to the connected room (north/south/east/west)"; + return "go - move one room (north/south/east/west); " + + "or go to to travel to a known room"; } } diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java new file mode 100644 index 0000000..3fafb60 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java @@ -0,0 +1,93 @@ +package thb.jeanluc.adventure.command.impl; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.game.GameContext; +import thb.jeanluc.adventure.io.TestIO; +import thb.jeanluc.adventure.model.Condition; +import thb.jeanluc.adventure.model.Direction; +import thb.jeanluc.adventure.model.ExitLock; +import thb.jeanluc.adventure.model.Player; +import thb.jeanluc.adventure.model.Room; +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 GoCommandPathTest { + + /** kitchen --N--> hallway --N--> library (all lit). Optional lock on kitchen->hallway. */ + private GameContext world(boolean lockHallway, String libraryName) { + Room kitchen = new Room("kitchen", "Kitchen", "d"); + Room hallway = new Room("hallway", "Hallway", "d"); + Room library = new Room("library", libraryName, "d"); + kitchen.addExit(Direction.NORTH, hallway); + hallway.addExit(Direction.SOUTH, kitchen); + hallway.addExit(Direction.NORTH, library); + library.addExit(Direction.SOUTH, hallway); + if (lockHallway) { + kitchen.addExitLock(Direction.NORTH, + new ExitLock(List.of(new Condition(Condition.Type.FLAG, "open")), "It's locked.")); + } + Map rooms = new LinkedHashMap<>(); + rooms.put("kitchen", kitchen); + rooms.put("hallway", hallway); + rooms.put("library", library); + World w = new World(rooms, Map.of(), Map.of(), "t", "w"); + Player p = new Player(kitchen, 0); + p.getVisitedRoomIds().addAll(List.of("kitchen", "hallway", "library")); + return new GameContext(w, p, new TestIO()); + } + + private String out(GameContext ctx) { + return ((TestIO) ctx.getIo()).allOutput(); + } + + @Test + void goToWalksToVisitedRoomById() { + GameContext ctx = world(false, "Library"); + new GoCommand().execute(ctx, List.of("library")); + assertThat(ctx.getPlayer().getCurrentRoom().getId()).isEqualTo("library"); + assertThat(out(ctx)).contains("make your way"); + } + + @Test + void resolvesByRoomName() { + // id "library" but type the display name (lower-cased) — still resolves + GameContext ctx = world(false, "Grand Library"); + new GoCommand().execute(ctx, List.of("grand", "library")); + assertThat(ctx.getPlayer().getCurrentRoom().getId()).isEqualTo("library"); + } + + @Test + void unknownPlaceReportsSo() { + GameContext ctx = world(false, "Library"); + new GoCommand().execute(ctx, List.of("dungeon")); + assertThat(ctx.getPlayer().getCurrentRoom().getId()).isEqualTo("kitchen"); + assertThat(out(ctx)).contains("don't know any place called 'dungeon'"); + } + + @Test + void alreadyThere() { + GameContext ctx = world(false, "Library"); + new GoCommand().execute(ctx, List.of("kitchen")); + assertThat(out(ctx)).contains("already in the Kitchen"); + } + + @Test + void noPassablePathReportsSo() { + GameContext ctx = world(true, "Library"); // kitchen->hallway locked, flag unset + new GoCommand().execute(ctx, List.of("library")); + assertThat(ctx.getPlayer().getCurrentRoom().getId()).isEqualTo("kitchen"); + assertThat(out(ctx)).contains("can't find a way there"); + } + + @Test + void directionalMoveStillWorks() { + GameContext ctx = world(false, "Library"); + new GoCommand().execute(ctx, List.of("north")); + assertThat(ctx.getPlayer().getCurrentRoom().getId()).isEqualTo("hallway"); + } +}