diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Pathfinder.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Pathfinder.java new file mode 100644 index 0000000..981a7a0 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Pathfinder.java @@ -0,0 +1,76 @@ +package thb.jeanluc.adventure.game; + +import thb.jeanluc.adventure.model.Direction; +import thb.jeanluc.adventure.model.ExitLock; +import thb.jeanluc.adventure.model.Room; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +/** + * Breadth-first shortest-path search over the rooms the player has already + * visited, honouring the same gates as a manual move: locked exits and + * darkness. Stateless utility, alongside {@link Conditions}/{@link Effects}/ + * {@link Light}. + */ +public final class Pathfinder { + + private Pathfinder() { + } + + /** + * Finds the shortest path from the player's current room to {@code target}. + * + * @param ctx active game context + * @param target destination room (expected to be a visited room) + * @return the rooms to step through, excluding the start and ending at + * {@code target}; empty if there is no currently-passable path + * (or {@code target} is the current room) + */ + public static List findPath(GameContext ctx, Room target) { + Room start = ctx.getPlayer().getCurrentRoom(); + Set visited = ctx.getPlayer().getVisitedRoomIds(); + + Queue queue = new ArrayDeque<>(); + Map cameFrom = new HashMap<>(); + queue.add(start); + cameFrom.put(start, null); + + while (!queue.isEmpty()) { + Room current = queue.poll(); + if (current == target) { + return reconstruct(cameFrom, target); + } + for (Map.Entry exit : current.getExits().entrySet()) { + Room next = exit.getValue(); + if (cameFrom.containsKey(next) || !visited.contains(next.getId())) { + continue; + } + ExitLock lock = current.getExitLocks().get(exit.getKey()); + if (lock != null && !Conditions.all(lock.requires(), ctx)) { + continue; + } + if (!Light.isLit(ctx, next)) { + continue; + } + cameFrom.put(next, current); + queue.add(next); + } + } + return List.of(); + } + + /** Walks predecessors back from target to start; returns steps after start. */ + private static List reconstruct(Map cameFrom, Room target) { + LinkedList path = new LinkedList<>(); + for (Room r = target; r != null && cameFrom.get(r) != null; r = cameFrom.get(r)) { + path.addFirst(r); + } + return path; + } +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/PathfinderTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/PathfinderTest.java new file mode 100644 index 0000000..2eaa376 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/PathfinderTest.java @@ -0,0 +1,88 @@ +package thb.jeanluc.adventure.game; + +import org.junit.jupiter.api.Test; +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 thb.jeanluc.adventure.model.item.Item; +import thb.jeanluc.adventure.model.item.PlainItem; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class PathfinderTest { + + private Room room(String id) { + return new Room(id, id, "d"); + } + + private GameContext ctx(Room start, List all, Set visited) { + Map rooms = new LinkedHashMap<>(); + for (Room r : all) { + rooms.put(r.getId(), r); + } + World w = new World(rooms, Map.of(), Map.of(), "t", "w"); + Player p = new Player(start, 0); + p.getVisitedRoomIds().addAll(visited); + return new GameContext(w, p, new TestIO()); + } + + @Test + void findsShortestPathOverVisitedRooms() { + Room a = room("a"), b = room("b"), c = room("c"); + a.addExit(Direction.NORTH, b); + b.addExit(Direction.NORTH, c); + GameContext ctx = ctx(a, List.of(a, b, c), Set.of("a", "b", "c")); + assertThat(Pathfinder.findPath(ctx, c)).containsExactly(b, c); + } + + @Test + void willNotRouteThroughUnvisitedRoom() { + Room a = room("a"), b = room("b"), c = room("c"); + a.addExit(Direction.NORTH, b); + b.addExit(Direction.NORTH, c); + GameContext ctx = ctx(a, List.of(a, b, c), Set.of("a", "c")); // b not visited + assertThat(Pathfinder.findPath(ctx, c)).isEmpty(); + } + + @Test + void respectsLockedExit() { + Room a = room("a"), b = room("b"); + a.addExit(Direction.NORTH, b); + a.addExitLock(Direction.NORTH, + new ExitLock(List.of(new Condition(Condition.Type.FLAG, "open")), "locked")); + GameContext ctx = ctx(a, List.of(a, b), Set.of("a", "b")); + assertThat(Pathfinder.findPath(ctx, b)).isEmpty(); + ctx.getState().set("open"); + assertThat(Pathfinder.findPath(ctx, b)).containsExactly(b); + } + + @Test + void respectsDarkness() { + Room a = room("a"); + Room dark = room("dark"); + dark.setDark(true); + a.addExit(Direction.NORTH, dark); + GameContext ctx = ctx(a, List.of(a, dark), Set.of("a", "dark")); + assertThat(Pathfinder.findPath(ctx, dark)).isEmpty(); + Item torch = PlainItem.builder().id("lit").name("Lit").description("d").light(true).build(); + ctx.getPlayer().addItem(torch); + assertThat(Pathfinder.findPath(ctx, dark)).containsExactly(dark); + } + + @Test + void unreachableTargetReturnsEmpty() { + Room a = room("a"); + Room island = room("island"); // no connecting exits + GameContext ctx = ctx(a, List.of(a, island), Set.of("a", "island")); + assertThat(Pathfinder.findPath(ctx, island)).isEmpty(); + } +}