BFS over visited rooms (respecting locks + light), routed via GoCommand (go to <room> vs go <direction>); silent console traversal + summary naming start/middle/destination; GUI minimap animates per step via a new GameIO.travelStep hook (SwingIO override, ~300ms). Trie autocomplete deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
195 lines
9.1 KiB
Markdown
195 lines
9.1 KiB
Markdown
# Spec: Pathfinding `go to <room>`
|
||
|
||
Stand: 2026-06-01. Zweite der verbleibenden Mechaniken (nach `use X on Y`).
|
||
Setzt den in `enhancement-ideas.md` als Algorithmus-Showcase notierten Punkt um:
|
||
`go to <raum>` per BFS. Baut auf `GoCommand`, `MapLayout` (BFS-Gitter + Fog of
|
||
War), `Light`, `Conditions`/`ExitLock` und der `GameIO`-Abstraktion auf.
|
||
|
||
## 1. Kontext & Ziel
|
||
|
||
Bewegung erfolgt bisher Schritt für Schritt per `go <richtung>`. Ziel: **`go to
|
||
<raum>`** läuft automatisch per **BFS** über bereits **besuchte** Räume zum Ziel,
|
||
unter Beachtung derselben Gates wie ein manueller Zug (verschlossene Exits, Licht).
|
||
In der **GUI** soll die Minikarte die Bewegung **Raum für Raum animiert** zeigen;
|
||
in der Konsole läuft die Reise still mit einer knappen Zusammenfassung.
|
||
|
||
Bestätigte Entscheidungen (Brainstorming 2026-06-01):
|
||
- **Erreichbarkeit**: nur **besuchte** Räume sind Wegpunkte/Ziele (Fog of War bleibt
|
||
erhalten; kein Teleport in Unentdecktes).
|
||
- **Darstellung**: Konsole still + Zusammenfassung + Ziel-`look`; GUI animiert die
|
||
Minikarte (Map-Push pro Schritt).
|
||
- **Zusammenfassung**: nennt **Start**, **Ziel** und **höchstens einen** mittleren
|
||
Wegpunkt (Mittelknoten). Bei 9 Knoten: 1., 5., 9.
|
||
- **Schrittverzögerung GUI**: ~**300 ms** pro Raum.
|
||
- **Trie-Autocomplete**: **zurückgestellt** (eigenes späteres Teilprojekt).
|
||
|
||
## 2. Scope
|
||
|
||
**In Scope:**
|
||
- `GoCommand`-Erweiterung: Argument ist Richtung → bestehender Einzelzug; sonst →
|
||
Raum-Ziel-Pathfinding. Keine neue Verb-Registrierung (Parser entfernt `to`).
|
||
- `Pathfinder` (`game`): BFS über besuchte Räume mit Passierbarkeits-Prädikat
|
||
(Exit-Lock + Licht), liefert den Pfad oder leer.
|
||
- Auto-Walk: Schritt für Schritt `setCurrentRoom` + `GameIO.travelStep(MapView)`;
|
||
danach Zusammenfassung + `LookCommand`.
|
||
- Neue `GameIO.travelStep(MapView)`-Default-Methode (No-Op Konsole) + `SwingIO`-
|
||
Override (Map-Push + ~300 ms Sleep).
|
||
- Ziel-Auflösung nur unter besuchten Räumen (id, dann name; case-insensitive).
|
||
- Zusammenfassung mit Start/Mitte/Ziel-Logik.
|
||
- Tests: `Pathfinder` (BFS, Locks, Licht, besucht-nur, unerreichbar), `GoCommand`
|
||
(Raum-Ziel, Meldungen, Richtung weiterhin).
|
||
|
||
**Out of Scope:**
|
||
- Trie-/Tab-Autocomplete (eigenes Teilprojekt).
|
||
- Routing durch **unbesuchte** Räume.
|
||
- Pfad-Neuplanung mitten im Lauf (Zustand ändert sich während eines Zuges nicht).
|
||
- Gewichtete Kanten / Kosten (alle Kanten gleich; BFS = kürzeste Raumzahl).
|
||
- Mehr-Wort-Item-/Richtungs-Sonderfälle über die beschriebene Auflösung hinaus.
|
||
|
||
## 3. Routing in `GoCommand`
|
||
|
||
Der Parser entfernt `to`/`with`/… als Filler, daher liefern `go to library` und
|
||
`go library` beide `args=[library]`. `execute` verzweigt:
|
||
|
||
```java
|
||
if (args.isEmpty()) { io.write("Go where? Try 'go north' or 'go to <room>'."); return; }
|
||
String first = args.getFirst();
|
||
try {
|
||
Direction dir = Direction.fromString(first);
|
||
moveDirection(ctx, dir); // bestehender Einzelzug (unverändert ausgelagert)
|
||
return;
|
||
} catch (IllegalArgumentException notADirection) {
|
||
goToRoom(ctx, args); // neuer Raum-Ziel-Pfad
|
||
}
|
||
```
|
||
|
||
`moveDirection` = der bisherige Rumpf (Exit prüfen, Lock, Licht, setCurrentRoom,
|
||
look). `move`/`walk`-Aliase erben beides.
|
||
|
||
## 4. Ziel-Auflösung (`goToRoom`)
|
||
|
||
- Zielname = `String.join(" ", args).toLowerCase()` (`go to old kitchen` → "old kitchen").
|
||
- Suche **nur unter besuchten Räumen** (`player.getVisitedRoomIds()` → `world.getRooms().get(id)`):
|
||
Treffer, wenn `id.equals(name)` **oder** `room.getName().toLowerCase().equals(name)`.
|
||
- Kein Treffer → `"You don't know any place called '<name>'."` (verrät keine
|
||
unentdeckten Räume).
|
||
- Ziel == aktueller Raum → `"You're already in the <Name>."`
|
||
- sonst → `Pathfinder.findPath`; leer → `"You can't find a way there right now."`;
|
||
sonst Auto-Walk (§6).
|
||
|
||
## 5. `Pathfinder` (game/Pathfinder.java)
|
||
|
||
```java
|
||
/** Shortest path (BFS) over visited rooms to target, honouring locks + light.
|
||
* Returns the steps AFTER the current room, ending at target; empty if none. */
|
||
public static List<Room> findPath(GameContext ctx, Room target) { ... }
|
||
```
|
||
|
||
- BFS ab `player.getCurrentRoom()`; Queue + `Map<Room,Room> cameFrom`.
|
||
- Kante `from --dir--> to` passierbar gdw.:
|
||
1. `to` ist in `player.getVisitedRoomIds()`,
|
||
2. Lock: `from.getExitLocks().get(dir)` ist null **oder** `Conditions.all(lock.requires(), ctx)`,
|
||
3. Licht: `Light.isLit(ctx, to)`.
|
||
- Bei Erreichen von `target` Pfad via `cameFrom` rekonstruieren (ohne Startraum,
|
||
inkl. `target`). Unerreichbar → `List.of()`.
|
||
- Rein über die Domäne + `ctx`; keine IO. Testbar wie `MapLayoutTest`/`QuestEngineTest`.
|
||
|
||
Hinweis: Da nur besuchte Räume Kanten liefern und Start immer „besucht" ist, ist
|
||
die Suche auf die bekannte Karte beschränkt. `target == current` behandelt
|
||
`goToRoom` vorab (Pathfinder müsste sonst leeren Pfad liefern).
|
||
|
||
## 6. Auto-Walk + animierte Minikarte
|
||
|
||
```java
|
||
List<Room> path = Pathfinder.findPath(ctx, target);
|
||
if (path.isEmpty()) { io.write("You can't find a way there right now."); return; }
|
||
for (Room step : path) {
|
||
ctx.getPlayer().setCurrentRoom(step);
|
||
ctx.getIo().travelStep(MapLayout.compute(
|
||
ctx.getWorld(), ctx.getPlayer().getVisitedRoomIds(), step));
|
||
}
|
||
ctx.getIo().write(summary(routeIncludingStart)); // §7
|
||
new LookCommand().execute(ctx, List.of());
|
||
```
|
||
|
||
- `setCurrentRoom` markiert Räume als besucht (bereits besucht → idempotent).
|
||
- `go to` ist **ein** Zug (ein Command) — der Game-Loop erhöht `turn` einmal.
|
||
|
||
### `GameIO.travelStep(MapView)`
|
||
|
||
```java
|
||
/** Per-step travel frame during auto-walk. Console: no-op (silent). */
|
||
default void travelStep(MapView view) {
|
||
// no animation in text mode
|
||
}
|
||
```
|
||
|
||
`SwingIO`-Override:
|
||
```java
|
||
@Override
|
||
public void travelStep(MapView view) {
|
||
setMap(view); // Map-Panel neu zeichnen (invokeLater)
|
||
try { Thread.sleep(TRAVEL_STEP_MS); } // ~300 ms, läuft auf dem Worker-Thread
|
||
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||
}
|
||
```
|
||
`TRAVEL_STEP_MS = 300`. Konsole bleibt still und ohne Pause (Default No-Op).
|
||
Der reguläre `publishHud` am Zugende setzt den finalen Map-/HUD-Stand.
|
||
|
||
## 7. Zusammenfassungs-Zeile (§ Wegpunkte)
|
||
|
||
Über die volle Route `route = [current] + path` (n = `route.size()`, Index 0..n-1):
|
||
- **Start** = `route[0]`, **Ziel** = `route[n-1]` immer genannt.
|
||
- **Mitte** = `route[n/2]` nur wenn `n >= 3` und der Index weder 0 noch n-1 ist
|
||
(also höchstens **ein** Zwischenpunkt). Bei n=9 → Index 4 (der 5. Knoten).
|
||
- Rendering:
|
||
- n == 2: `"You make your way from the <Start> to the <Ziel>."`
|
||
- n >= 3: `"You make your way from the <Start>, through the <Mitte>, to the <Ziel>."`
|
||
|
||
(Artikel „the" bewusst einfach gehalten; reine Flavor-Zeile, leicht anpassbar.)
|
||
|
||
## 8. Fehler-/Randfälle (Meldungen)
|
||
|
||
| Fall | Meldung |
|
||
|---|---|
|
||
| `go` ohne Argument | "Go where? Try 'go north' or 'go to <room>'." |
|
||
| Richtung ohne Exit | (bestehend) "You can't go <dir> from here." |
|
||
| Richtung gesperrt / dunkel | (bestehend) Lock-Text / Dunkelheits-Hinweis |
|
||
| Zielname unbekannt (unter besuchten) | "You don't know any place called '<name>'." |
|
||
| Ziel == aktueller Raum | "You're already in the <Name>." |
|
||
| Ziel besucht, aber kein passierbarer Pfad | "You can't find a way there right now." |
|
||
|
||
## 9. Tests
|
||
|
||
- **`PathfinderTest`** (Welt inline, wie `MapLayoutTest`):
|
||
- kürzester Pfad über besuchte Räume (Länge + Knoten korrekt).
|
||
- unbesuchter Zwischenraum wird **nicht** als Wegpunkt genutzt (kein Pfad bzw.
|
||
Umweg nur über Besuchtes).
|
||
- kein Pfad durch einen Exit mit nicht erfüllter `ExitLock`-Bedingung; Pfad
|
||
erscheint, wenn die Flag gesetzt ist.
|
||
- kein Pfad in einen dunklen Raum ohne Licht; mit getragenem Licht erreichbar.
|
||
- unerreichbares Ziel → leere Liste.
|
||
- **`GoCommand`-Tests** (Raum-Ziel):
|
||
- `go to <name>` läuft zum Ziel (currentRoom aktualisiert, Ziel-`look` erfolgt).
|
||
- Auflösung per id **und** per name (case-insensitive).
|
||
- unbekannter Name / Ziel==aktuell / kein Pfad → jeweilige Meldung.
|
||
- `go north` (Richtung) funktioniert weiterhin (Regressionsschutz).
|
||
- **`travelStep`**: Default ist No-Op (Tests nutzen `TestIO`; keine Sleeps, keine
|
||
GUI). GUI-Animation wird manuell verifiziert (Konvention: keine GUI-Unit-Tests).
|
||
|
||
## 10. Architektur-Werte / Risiken
|
||
|
||
- **Gemeinsamer Loop bleibt**: nur eine neue `GameIO`-Default-Methode; Pacing/
|
||
Animation lebt im Renderer (`SwingIO`), Konsole No-Op. Keine Divergenz der Logik.
|
||
- **Konsistenz mit manuellem Zug**: Pathfinder nutzt dieselben Gates (Lock, Licht)
|
||
wie `GoCommand` — kein Auto-Walk durch etwas, das man manuell nicht passieren darf.
|
||
- **Fog of War**: nur besuchte Räume → kein Informationsleck über Unentdecktes.
|
||
- **Algorithmus-Showcase**: klassisches BFS (Queue + `cameFrom`) mit
|
||
Standard-Collections (entspricht der Projekt-Entscheidung „keine eigenen
|
||
`uebung`-Strukturen"). `MapLayout` macht bereits BFS — `Pathfinder` ist die
|
||
fokussierte Wegfindungs-Variante (eigene Verantwortlichkeit, eigene Tests).
|
||
- **GUI-Threading**: `travelStep` schläft auf dem Worker-Thread (wie `readLine`
|
||
blockiert), `setMap` zeichnet via EDT — kein Deadlock; EDT bleibt responsiv.
|
||
- Risiko: lange Pfade × 300 ms könnten sich zäh anfühlen; bewusst akzeptiert (kurze
|
||
Karte). Später ggf. Settings-gesteuert.
|