docs: spec for win-condition & endings (sub-project 3.3)

Priority-ordered condition-driven endings + an end-of-game summary (turns,
quests X/Y, rank). EndingEngine mirrors the quest engine; Game ends the loop
when an ending fires. endings.yaml optional; World gains 5/6/7-arg back-compat
constructors. Two reachable demo endings (victory, flee) with no new mechanics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:44:01 +02:00
parent eb78c7840e
commit 0f40ee637d

View File

@@ -0,0 +1,116 @@
# Spec: Win-Condition & Enden (Teilprojekt 3.3)
Stand: 2026-05-31. Capstone des Quest-Systems. Baut auf Fundament (3.1) + Quests
(3.2) auf: Conditions/Effects/Flags + QuestLog sind vorhanden.
## 1. Kontext & Ziel
Das Spiel endet bisher nur per `quit`. Ziel: **mehrere, priorisierte Enden**,
condition-driven (Ansatz wie Quests), plus ein **End-Screen mit Zusammenfassung**
(Züge, abgeschlossene Quests, Rang). Bestätigt: **A mit Summary**.
## 2. Scope
**In Scope:**
- `Ending`-Datenmodell + optionale `endings.yaml` (Reihenfolge = Priorität).
- `EndingEngine` (erstes passendes Ende ermitteln + End-Screen rendern).
- `Game`-Anbindung: pro Zug nach dem Quest-Tick prüfen; bei Treffer Ende +
Summary ausgeben und `stop()`.
- End-Summary: Züge, Quests X/Y, Rang.
- Loader: `EndingDto`, `EndingFactory`, `World.endings`.
- Demo: zwei spielbar erreichbare Enden ohne neue Mechanik.
**Out of Scope:** Scoreboard/Persistenz; zugbasierte Conditions; Spielstand-Reset.
## 3. Datenmodell (model)
```java
public record Ending(String id, String title, boolean victory,
List<Condition> when, String text) { /* when: List.copyOf */ }
```
`endings.yaml` (geordnete Liste, erstes passendes gewinnt):
```yaml
- id: victory
title: "The Manor Reclaimed"
victory: true
when: [{ flag: manor_secured }]
text: |
The lights hold steady, the whispers fade. You've made the manor yours.
- id: fled
title: "Into the Night"
victory: false
when: [{ flag: fled }]
text: |
You bolt through the front door. Safe — but the manor keeps its secrets.
```
## 4. Engine (game)
- `EndingEngine.triggered(GameContext ctx)` → erstes `Ending`, dessen `when` via
`Conditions.all` gilt, sonst `null`. Rein, testbar (Priorität/erster Treffer).
- `EndingEngine.render(Ending e, GameContext ctx, int turns)``StyledText`:
Titel-Banner + `text` + Summary.
- **Summary**: `Turns: N`, `Quests completed: X / Y`
(`X = questLog.completed().size()`, `Y = world.getQuests().size()`), plus
**Rang**:
- victory & alle Quests → „Master of the Manor"
- victory → „Manor Reclaimed"
- sonst → „Escaped — the manor keeps its secrets"
## 5. Game-Anbindung
In `Game.run` nach `publishHud()` (Initial **und** pro Schleifendurchlauf) ein
`maybeEnd()`:
```java
private void maybeEnd() {
Ending e = EndingEngine.triggered(ctx);
if (e != null) {
ctx.getIo().print(EndingEngine.render(e, ctx, turn));
stop();
}
}
```
`stop()` setzt `running=false`; die Schleife endet sauber, kein zusätzlicher Prompt.
Konsole: Programm endet. GUI: Worker-Thread endet, Fenster bleibt mit End-Screen.
## 6. Loading
- `EndingDto(id, title, Boolean victory, List<ConditionDto> when, String text)`.
- `EndingFactory.fromDto` baut `Ending` (`when` via `ConditionDto.toModelList`).
- `WorldLoader` liest `endings.yaml` **optional** (`readListOptional`), baut eine
**geordnete** `List<Ending>` (Reihenfolge der YAML-Liste), `requireUniqueIds("ending", …)`.
- `World` bekommt Feld `endings` (List) + **rückwärtskompatible 5- und
6-Arg-Konstruktoren** (leere Quests/Endings), damit bestehende `new World(...)`-
Aufrufe (Tests, bisheriger Loader-Pfad) unverändert kompilieren. Der Loader nutzt
den vollen 7-Arg-Konstruktor.
## 7. Fehlerbehandlung
| Fall | Verhalten |
|---|---|
| `endings.yaml` fehlt | leere Liste → Spiel endet nur per `quit` (wie bisher) |
| mehrere Conditions treffen | erstes Ende in Listenreihenfolge gewinnt |
| Ending ohne `when` | trifft sofort (leere Condition-Liste = true) Autoren sollten Reihenfolge beachten |
| keine Quests definiert | Summary zeigt `0 / 0`, Rang nach victory-Flag |
## 8. Demonstration (zwei erreichbare Enden, keine neue Mechanik)
- **victory**: `manor_secured` wird gesetzt, wenn die `restore_power`-Quest endet
(bereits vorhanden) → Sieg-Ende.
- **fled**: neuer schaltbarer Gegenstand **Front Door** (`effects: setFlag fled`)
in der Küche → Flucht-Ende. `victory` steht vor `fled`, also schlägt das
Sichern der Villa die Flucht.
## 9. Testing
- `EndingEngine.triggered`: Priorität/erster Treffer; kein Treffer → `null`.
- `EndingEngine.render`: enthält Titel, Text, Züge, Quests-Zähler, Rang.
- `EndingFactory.fromDto`: Mapping inkl. victory/when/text.
- End-to-End (Konsole): Villa sichern → Sieg-Ende + Summary, Spiel endet;
alternativ Front Door benutzen → Flucht-Ende.
- `Game`-Integration: nicht separat unit-getestet (Loop/IO); über Konsolen-Smoke.
## 10. Offene Detailfragen (in Implementierung)
- Genaue Banner-Optik des End-Screens (HEADING-Rahmen).
- Rang-Schwellen (zunächst nur victory + alle-Quests-Abfrage).