4 TDD tasks: GameIO.travelStep hook (+SwingIO animation), Pathfinder BFS over visited rooms (locks+light), GoCommand routing/resolve/walk/summary, docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
610 lines
22 KiB
Markdown
610 lines
22 KiB
Markdown
# `go to <room>` Pathfinding 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 `go to <room>` — BFS auto-walk over visited rooms (respecting locked exits + darkness), routed through `GoCommand`, with a silent console summary and an animated GUI minimap.
|
|
|
|
**Architecture:** A new stateless `Pathfinder` (game package, next to `Conditions`/`Effects`/`Light`) does BFS over visited rooms using the same passability gates `GoCommand` enforces. `GoCommand` gains a branch: a directional arg moves one step (unchanged); a non-direction arg resolves a visited room by id/name and auto-walks the path. Per step it pushes a map frame via a new `GameIO.travelStep(MapView)` hook — a console no-op, overridden by `SwingIO` to repaint + briefly sleep so the minimap animates.
|
|
|
|
**Tech Stack:** Java 25, Maven, JUnit 5 + AssertJ, Lombok.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
**Create:**
|
|
- `src/main/java/thb/jeanluc/adventure/game/Pathfinder.java` — BFS over visited rooms.
|
|
|
|
**Modify:**
|
|
- `io/GameIO.java` — add `travelStep(MapView)` default no-op.
|
|
- `io/SwingIO.java` — override `travelStep` (setMap + ~300ms sleep).
|
|
- `command/impl/GoCommand.java` — extract `moveDirection`; add `goToRoom` (resolve + path + walk + summary).
|
|
- `docs/enhancement-ideas.md` — mark pathfinding implemented.
|
|
|
|
---
|
|
|
|
## Task 1: `GameIO.travelStep` hook + `SwingIO` animation
|
|
|
|
**Files:**
|
|
- Modify: `src/main/java/thb/jeanluc/adventure/io/GameIO.java`
|
|
- Modify: `src/main/java/thb/jeanluc/adventure/io/SwingIO.java`
|
|
- Test: `src/test/java/thb/jeanluc/adventure/io/TravelStepDefaultTest.java`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
`src/test/java/thb/jeanluc/adventure/io/TravelStepDefaultTest.java`:
|
|
```java
|
|
package thb.jeanluc.adventure.io;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import thb.jeanluc.adventure.io.text.MapView;
|
|
|
|
import java.util.List;
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
|
class TravelStepDefaultTest {
|
|
|
|
@Test
|
|
void defaultTravelStepIsSilentNoOp() {
|
|
TestIO io = new TestIO();
|
|
// An empty map view is fine; the default must not print or throw.
|
|
io.travelStep(new MapView(List.of(), List.of()));
|
|
assertThat(io.outputs()).isEmpty();
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `mvn -q test -Dtest=TravelStepDefaultTest`
|
|
Expected: FAIL — `travelStep` is not defined on `GameIO`.
|
|
|
|
- [ ] **Step 3: Add the default to `GameIO`**
|
|
|
|
In `src/main/java/thb/jeanluc/adventure/io/GameIO.java`, add the import (if missing it is already there — `MapView` is used by `setMap`) and the method:
|
|
```java
|
|
/**
|
|
* One frame of an auto-walk (the {@code go to} command), as the player moves
|
|
* through a room en route. Default is a no-op: text mode travels silently.
|
|
* The GUI overrides this to repaint the map and pace the animation.
|
|
*
|
|
* @param view the map snapshot at this step
|
|
*/
|
|
default void travelStep(MapView view) {
|
|
// no animation in text mode
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Override in `SwingIO`**
|
|
|
|
In `src/main/java/thb/jeanluc/adventure/io/SwingIO.java`:
|
|
1. Add a constant near the other size constants (e.g. below `SMALL_SIZE`):
|
|
```java
|
|
/** Delay between rooms while animating a go-to auto-walk, in milliseconds. */
|
|
private static final long TRAVEL_STEP_MS = 300L;
|
|
```
|
|
2. Add the override (place it near `setMap`):
|
|
```java
|
|
@Override
|
|
public void travelStep(MapView view) {
|
|
setMap(view);
|
|
try {
|
|
Thread.sleep(TRAVEL_STEP_MS);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
}
|
|
```
|
|
(`setMap` already calls `map.show(view)` which repaints on the EDT; the sleep on the game-loop worker thread lets each frame render. `MapView` is already imported in `SwingIO`.)
|
|
|
|
- [ ] **Step 5: Run tests**
|
|
|
|
Run: `mvn -q test -Dtest=TravelStepDefaultTest`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add src/main/java/thb/jeanluc/adventure/io/GameIO.java \
|
|
src/main/java/thb/jeanluc/adventure/io/SwingIO.java \
|
|
src/test/java/thb/jeanluc/adventure/io/TravelStepDefaultTest.java
|
|
git commit -m "feat(io): travelStep hook for animated auto-walk (console no-op, Swing override)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: `Pathfinder` BFS
|
|
|
|
**Files:**
|
|
- Create: `src/main/java/thb/jeanluc/adventure/game/Pathfinder.java`
|
|
- Test: `src/test/java/thb/jeanluc/adventure/game/PathfinderTest.java`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
`src/test/java/thb/jeanluc/adventure/game/PathfinderTest.java`:
|
|
```java
|
|
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<Room> all, Set<String> visited) {
|
|
Map<String, Room> 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();
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `mvn -q test -Dtest=PathfinderTest`
|
|
Expected: FAIL — `Pathfinder` does not exist.
|
|
|
|
- [ ] **Step 3: Create `Pathfinder`**
|
|
|
|
`src/main/java/thb/jeanluc/adventure/game/Pathfinder.java`:
|
|
```java
|
|
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<Room> findPath(GameContext ctx, Room target) {
|
|
Room start = ctx.getPlayer().getCurrentRoom();
|
|
Set<String> visited = ctx.getPlayer().getVisitedRoomIds();
|
|
|
|
Queue<Room> queue = new ArrayDeque<>();
|
|
Map<Room, Room> 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<Direction, Room> 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<Room> reconstruct(Map<Room, Room> cameFrom, Room target) {
|
|
LinkedList<Room> path = new LinkedList<>();
|
|
for (Room r = target; r != null && cameFrom.get(r) != null; r = cameFrom.get(r)) {
|
|
path.addFirst(r);
|
|
}
|
|
return path;
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `mvn -q test -Dtest=PathfinderTest`
|
|
Expected: PASS (5 tests).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/main/java/thb/jeanluc/adventure/game/Pathfinder.java \
|
|
src/test/java/thb/jeanluc/adventure/game/PathfinderTest.java
|
|
git commit -m "feat(game): Pathfinder BFS over visited rooms (locks + light aware)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: `GoCommand` — route, resolve, walk, summarize
|
|
|
|
**Files:**
|
|
- Modify: `src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java`
|
|
- Test: `src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java`
|
|
|
|
- [ ] **Step 1: Write the failing test**
|
|
|
|
`src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java`:
|
|
```java
|
|
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<String, Room> 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");
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run test to verify it fails**
|
|
|
|
Run: `mvn -q test -Dtest=GoCommandPathTest`
|
|
Expected: FAIL — `go to` routing not implemented (e.g. `goToWalksToVisitedRoomById` fails: "library" treated as an unknown direction).
|
|
|
|
- [ ] **Step 3: Rewrite `GoCommand`**
|
|
|
|
Replace `src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java`:
|
|
```java
|
|
package thb.jeanluc.adventure.command.impl;
|
|
|
|
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. {@code go <direction>} steps once to a connected room.
|
|
* {@code go to <room>} auto-walks the shortest path (BFS) over already-visited
|
|
* rooms, honouring locked exits and darkness. Usage: {@code go <direction>} or
|
|
* {@code go to <room>}.
|
|
*/
|
|
public class GoCommand implements Command {
|
|
|
|
@Override
|
|
public void execute(GameContext ctx, List<String> args) {
|
|
if (args.isEmpty()) {
|
|
ctx.getIo().write("Go where? Try 'go north' or 'go to <room>'.");
|
|
return;
|
|
}
|
|
try {
|
|
Direction dir = Direction.fromString(args.getFirst());
|
|
moveDirection(ctx, dir);
|
|
} catch (IllegalArgumentException notADirection) {
|
|
goToRoom(ctx, args);
|
|
}
|
|
}
|
|
|
|
/** Single-step move in a compass direction (the original behaviour). */
|
|
private void moveDirection(GameContext ctx, Direction dir) {
|
|
Room here = ctx.getPlayer().getCurrentRoom();
|
|
Optional<Room> next = here.getExit(dir);
|
|
if (next.isEmpty()) {
|
|
ctx.getIo().write("You can't go " + dir.getLabel() + " from here.");
|
|
return;
|
|
}
|
|
ExitLock lock = here.getExitLocks().get(dir);
|
|
if (lock != null && !Conditions.all(lock.requires(), ctx)) {
|
|
ctx.getIo().write(lock.blocked());
|
|
return;
|
|
}
|
|
if (!Light.isLit(ctx, next.get())) {
|
|
ctx.getIo().write("It's pitch black beyond the doorway — you need a lit light source "
|
|
+ "(try lighting your lamp with 'use lamp').");
|
|
return;
|
|
}
|
|
ctx.getPlayer().setCurrentRoom(next.get());
|
|
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<String> 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<Room> path = Pathfinder.findPath(ctx, target);
|
|
if (path.isEmpty()) {
|
|
ctx.getIo().write("You can't find a way there right now.");
|
|
return;
|
|
}
|
|
List<Room> 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) {
|
|
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<Room> 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 <direction> - move one room (north/south/east/west); "
|
|
+ "or go to <room> to travel to a known room";
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Run tests to verify they pass**
|
|
|
|
Run: `mvn -q test -Dtest=GoCommandPathTest`
|
|
Expected: PASS (6 tests).
|
|
|
|
- [ ] **Step 5: Full suite (no regressions to existing go/movement tests)**
|
|
|
|
Run: `mvn -q clean test`
|
|
Expected: BUILD SUCCESS, all green (existing `GameTest`/command tests that use `go north` still pass).
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java \
|
|
src/test/java/thb/jeanluc/adventure/command/impl/GoCommandPathTest.java
|
|
git commit -m "feat(command): go to <room> auto-walk (BFS path, animated map, summary)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Docs
|
|
|
|
**Files:**
|
|
- Modify: `docs/enhancement-ideas.md`
|
|
|
|
- [ ] **Step 1: Mark pathfinding implemented**
|
|
|
|
In `docs/enhancement-ideas.md`, find the "Festgelegte Erweiterungs-Entscheidungen"
|
|
area where the blockquote notes Pathfinding is *in scope for a later round* (added
|
|
when main-menu/save-load shipped). Update that note to mark it done, in the existing
|
|
`> ✅` style:
|
|
|
|
> ✅ umgesetzt (Branch `feature/pathfinding-go-to`): `go to <raum>` per BFS über
|
|
> besuchte Räume (Locks + Licht beachtet), via `GoCommand` (Richtung vs. Raumziel).
|
|
> Konsole still + Zusammenfassung (Start/Mitte/Ziel), GUI-Minikarte animiert pro
|
|
> Schritt (`GameIO.travelStep`, ~300 ms). Trie-Autocomplete weiter zurückgestellt.
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add docs/enhancement-ideas.md
|
|
git commit -m "docs: mark go to <room> pathfinding implemented"
|
|
```
|
|
|
|
---
|
|
|
|
## Final Verification
|
|
|
|
- [ ] `mvn -q clean test` — all tests pass (184 prior + new Pathfinder/GoCommand/travelStep suites).
|
|
- [ ] Manual GUI smoke (optional): start a game, walk kitchen→hallway→library manually (so they're visited), return to kitchen, then `go to library` — the minimap dot should step through the hallway to the library (~300ms each), then the library room renders with a summary line.
|
|
- [ ] Manual console smoke (optional): same, confirming silent traversal + the "You make your way ..." line + destination look.
|
|
- [ ] `git status` clean.
|