docs: spec for go to <room> pathfinding

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>
This commit is contained in:
2026-06-01 19:03:26 +02:00
parent c10590028e
commit d10632b800

View File

@@ -0,0 +1,194 @@
# 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.