semesterprojekt: implement full text adventure (phases 1-7)
Walking skeleton through Swing GUI: YAML-driven world (4 rooms, 4 items, 1 NPC), HashMap command dispatch with parser, three-tier item hierarchy (readable / switchable / plain), and end-to-end NPC give/receive flow. 67 tests green.
This commit is contained in:
33
Semesterprojekt/docs/README.md
Normal file
33
Semesterprojekt/docs/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Semesterprojekt – Textadventure
|
||||
|
||||
Design- und Architekturdokumentation. Dient als Spec während der Implementierung.
|
||||
|
||||
## Inhalt
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| [architecture.md](architecture.md) | Package-Struktur, Schichten, DTO-vs-Domain-Trennung |
|
||||
| [conventions.md](conventions.md) | Sprache, ID-Format, Naming, Lombok-Cheatsheet |
|
||||
| [data-structures.md](data-structures.md) | Alle gewählten Collection-Typen mit Begründung |
|
||||
| [item-model.md](item-model.md) | Item-Hierarchie (abstract + 3 Subtypen), Lombok-Inheritance |
|
||||
| [yaml-schemas.md](yaml-schemas.md) | Schemas für `items.yaml`, `rooms.yaml`, `npcs.yaml`, `game.yaml` |
|
||||
| [loading-flow.md](loading-flow.md) | Lade-Reihenfolge, Referenz-Auflösung, Validierung |
|
||||
| [commands.md](commands.md) | Befehlsparser, Command-Pattern, Befehlsliste |
|
||||
| [npcs.md](npcs.md) | NPC-Modell, Talk- und Give-Interaktion |
|
||||
| [implementation-status.md](implementation-status.md) | Aktueller Stand, Phasen-Checkliste, festgelegte Entscheidungen |
|
||||
|
||||
## Pflicht vs. Optional (laut Aufgabenstellung)
|
||||
|
||||
- **Pflicht:** ≥4 Räume mit Navigation, ≥3 Gegenstände mit Inventar
|
||||
- **Optional/Bonus:** NPCs, Swing-GUI
|
||||
|
||||
Beide optionalen Teile sind hier eingeplant.
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- Java 25
|
||||
- Jackson (YAML) für Daten-Loading
|
||||
- Lombok für Boilerplate-Reduktion
|
||||
- JUnit 5 + AssertJ + Mockito für Tests
|
||||
- Logback + SLF4J für Logging
|
||||
- Swing für GUI (Bonus)
|
||||
93
Semesterprojekt/docs/architecture.md
Normal file
93
Semesterprojekt/docs/architecture.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Architektur
|
||||
|
||||
## Schichten
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
IO["io (Konsole / GUI)<br/>Interaktion mit Spieler"]
|
||||
GAME["game (Engine/Loop)<br/>Spielfluss, hält World + Player"]
|
||||
CMD["command (Commands)<br/>go, take, use, talk, ..."]
|
||||
MODEL["model (Domain)<br/>Room, Item, Player, Npc, World"]
|
||||
LOADER["loader (YAML + DTO)<br/>liest Ressourcen, validiert"]
|
||||
|
||||
IO -- "liest/schreibt via GameIO" --> GAME
|
||||
GAME -- "dispatcht via CommandRegistry" --> CMD
|
||||
CMD -- "mutiert" --> MODEL
|
||||
LOADER -- "baut auf aus DTOs" --> MODEL
|
||||
```
|
||||
|
||||
## Package-Struktur
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
root["thb.jeanluc.adventure"]
|
||||
|
||||
root --> app["App.java<br/>AppGui.java"]
|
||||
root --> model
|
||||
root --> loader
|
||||
root --> command
|
||||
root --> game
|
||||
root --> io
|
||||
|
||||
model --> model_files["World, Room,<br/>Player, Npc, Direction"]
|
||||
model --> item
|
||||
item --> item_files["Item (abstract)<br/>ReadableItem<br/>SwitchableItem<br/>PlainItem"]
|
||||
|
||||
loader --> loader_files["WorldLoader<br/>ReferenceResolver<br/>WorldValidator"]
|
||||
loader --> dto
|
||||
dto --> dto_files["RoomDto, ItemDto,<br/>NpcDto, GameDto"]
|
||||
|
||||
command --> cmd_core["Command (Interface)<br/>CommandRegistry<br/>CommandParser"]
|
||||
command --> impl
|
||||
impl --> impl_files["GoCommand, TakeCommand,<br/>DropCommand, UseCommand,<br/>InventoryCommand, LookCommand,<br/>TalkCommand, GiveCommand,<br/>HelpCommand, QuitCommand"]
|
||||
|
||||
game --> game_files["Game (Loop)<br/>GameContext"]
|
||||
|
||||
io --> io_files["GameIO (Interface)<br/>ConsoleIO<br/>SwingIO"]
|
||||
```
|
||||
|
||||
## DTO vs. Domain-Trennung
|
||||
|
||||
**Kernprinzip:** YAML wird in Records deserialisiert (DTOs), erst danach werden String-IDs zu Objekt-Referenzen aufgelöst.
|
||||
|
||||
| | DTO | Domain |
|
||||
|---|---|---|
|
||||
| Typ | `record` | Lombok-Klasse |
|
||||
| Mutabilität | immutable | mutable wo nötig |
|
||||
| Felder | nur Daten + String-IDs | Objekt-Referenzen, EnumMap, etc. |
|
||||
| Zweck | Jackson-Mapping | Spielablauf |
|
||||
| Tests | Loader-Tests | Domain-Tests, brauchen kein YAML |
|
||||
|
||||
**Warum getrennt?**
|
||||
- Domain-Klassen müssen nicht mit Jackson-Annotations verschmutzt werden
|
||||
- `Room.exits` ist `EnumMap<Direction, Room>` (typisicher, schnell) statt `Map<String, String>` (was zum YAML passt)
|
||||
- Validierung passiert beim Übergang DTO→Domain (siehe [loading-flow.md](loading-flow.md))
|
||||
- Domain-Tests können Objekte direkt im Code bauen, ohne YAML-Fixtures
|
||||
|
||||
## Game-Loop (vereinfacht)
|
||||
|
||||
```java
|
||||
while (!game.isOver()) {
|
||||
String input = io.read();
|
||||
ParsedCommand parsed = parser.parse(input);
|
||||
Command cmd = registry.get(parsed.verb());
|
||||
if (cmd == null) {
|
||||
io.write("Unbekannter Befehl.");
|
||||
} else {
|
||||
cmd.execute(context, parsed.args());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## IO-Abstraktion
|
||||
|
||||
Konsole und GUI teilen sich `GameIO`:
|
||||
|
||||
```java
|
||||
public interface GameIO {
|
||||
String read(); // blockierende Leseoperation
|
||||
void write(String text);
|
||||
}
|
||||
```
|
||||
|
||||
Damit ist der Game-Loop **identisch** für beide Modi. `SwingIO` blockiert intern mit einer `BlockingQueue<String>`, die vom JTextField-ActionListener gefüllt wird.
|
||||
127
Semesterprojekt/docs/commands.md
Normal file
127
Semesterprojekt/docs/commands.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Befehle
|
||||
|
||||
Command-Pattern mit Registry — passt zum Aufgaben-Tipp „switch, if-else, HashMap" und macht die HashMap-Variante explizit.
|
||||
|
||||
## Interface
|
||||
|
||||
```java
|
||||
public interface Command {
|
||||
/** Wird vom Parser nach Tokenisierung aufgerufen. */
|
||||
void execute(GameContext ctx, List<String> args);
|
||||
|
||||
/** Short help text for the 'help' command. */
|
||||
String help();
|
||||
}
|
||||
```
|
||||
|
||||
## Registry
|
||||
|
||||
```java
|
||||
public class CommandRegistry {
|
||||
private final Map<String, Command> commands = new HashMap<>();
|
||||
|
||||
public void register(Command cmd, String... names) {
|
||||
for (String n : names) {
|
||||
commands.put(n.toLowerCase(), cmd);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Command> find(String verb) {
|
||||
return Optional.ofNullable(commands.get(verb.toLowerCase()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Aliase per Mehrfach-Registrierung:**
|
||||
|
||||
```java
|
||||
registry.register(new GoCommand(), "go", "move");
|
||||
registry.register(new TakeCommand(), "take", "pick");
|
||||
```
|
||||
|
||||
## Parser
|
||||
|
||||
Einfacher tokenisierender Parser. Erst-Token = Verb, Rest = Argumente.
|
||||
|
||||
Spezialfall: Präpositionen / Artikel wegfiltern, damit `go to north` und `go north` beide funktionieren.
|
||||
|
||||
```java
|
||||
public record ParsedCommand(String verb, List<String> args) {}
|
||||
|
||||
public class CommandParser {
|
||||
private static final Set<String> FILLERS = Set.of("to", "with", "at", "the", "a", "an");
|
||||
|
||||
public ParsedCommand parse(String input) {
|
||||
String[] tokens = input.trim().toLowerCase().split("\\s+");
|
||||
if (tokens.length == 0 || tokens[0].isEmpty()) {
|
||||
return new ParsedCommand("", List.of());
|
||||
}
|
||||
String verb = tokens[0];
|
||||
List<String> args = Arrays.stream(tokens, 1, tokens.length)
|
||||
.filter(t -> !FILLERS.contains(t))
|
||||
.toList();
|
||||
return new ParsedCommand(verb, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Befehlsliste (Pflicht + Optional)
|
||||
|
||||
| Verb | Aliase | Wirkung | Quelle |
|
||||
|---|---|---|---|
|
||||
| `go <direction>` | `move`, `walk` | Spieler wechselt Raum | Pflicht |
|
||||
| `take <item>` | `pick`, `get` | Item aus Raum ins Inventar | Pflicht |
|
||||
| `drop <item>` | `put` | Item aus Inventar in Raum | sinnvoll |
|
||||
| `use <item>` | — | Item-spezifische Aktion | Pflicht |
|
||||
| `read <item>` | — | Spezialfall von use für `readable` | Pflicht |
|
||||
| `inventory` | `inv`, `i` | Inventar anzeigen | sinnvoll |
|
||||
| `look` | `l` | Raumbeschreibung wiederholen | sinnvoll |
|
||||
| `examine <item>` | `x` | Item-Beschreibung anzeigen | sinnvoll |
|
||||
| `talk <npc>` | `speak` | NPC-Greeting ausgeben | NPC-Bonus |
|
||||
| `give <item> <npc>` | — | Item übergeben, Reaktion auslösen | NPC-Bonus |
|
||||
| `help` | `?` | Befehlsübersicht | sinnvoll |
|
||||
| `quit` | `exit` | Spiel beenden | sinnvoll |
|
||||
|
||||
## Behandlung unbekannter Befehle
|
||||
|
||||
```java
|
||||
parser.parse(input);
|
||||
registry.find(parsed.verb())
|
||||
.ifPresentOrElse(
|
||||
cmd -> cmd.execute(ctx, parsed.args()),
|
||||
() -> ctx.io().write("I don't understand '" + parsed.verb() + "'. Type 'help'.")
|
||||
);
|
||||
```
|
||||
|
||||
## GameContext
|
||||
|
||||
Wird allen Commands gereicht, kapselt was sie ändern dürfen:
|
||||
|
||||
```java
|
||||
public class GameContext {
|
||||
private final World world;
|
||||
private final Player player;
|
||||
private final GameIO io;
|
||||
// Lombok @Getter, kein Setter
|
||||
}
|
||||
```
|
||||
|
||||
So vermeidest du, dass jeder Command 5 Konstruktor-Parameter braucht.
|
||||
|
||||
## Tests
|
||||
|
||||
Pro Command ein Testfall, der `GameContext` mit Mockito mockt (oder als Fake-IO baut):
|
||||
|
||||
```java
|
||||
@Test
|
||||
void goCommand_movesPlayerToConnectedRoom() {
|
||||
Room kitchen = ...; Room hallway = ...;
|
||||
kitchen.getExits().put(Direction.NORTH, hallway);
|
||||
Player p = new Player(kitchen, 0);
|
||||
GameContext ctx = new GameContext(world, p, new TestIO());
|
||||
|
||||
new GoCommand().execute(ctx, List.of("north"));
|
||||
|
||||
assertThat(p.getCurrentRoom()).isEqualTo(hallway);
|
||||
}
|
||||
```
|
||||
88
Semesterprojekt/docs/conventions.md
Normal file
88
Semesterprojekt/docs/conventions.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Konventionen
|
||||
|
||||
Sprache, IDs, Package-Struktur, Naming.
|
||||
|
||||
## Sprache
|
||||
|
||||
- **Code:** Englisch — Klassen, Methoden, Felder, Enums, Package-Namen, Javadoc.
|
||||
- **YAML:** Englisch — Keys, IDs, alle Texte (descriptions, dialogue, readText, etc.).
|
||||
- **Doku-Prose** (`docs/*.md`): Deutsch — Diskussion mit dem Entwickler, keine Spec für Korrektoren.
|
||||
|
||||
## IDs
|
||||
|
||||
Slugs, keine UUIDs. Begründung in [item-model.md](item-model.md) und [yaml-schemas.md](yaml-schemas.md).
|
||||
|
||||
| Regel | Beispiel | Gegenbeispiel |
|
||||
|---|---|---|
|
||||
| nur kleinbuchstaben | `kitchen`, `letter` | `Kitchen`, `LETTER` |
|
||||
| ASCII, keine Umlaute | `cellar`, `kueche` schlecht | `keller` gut, `küche` schlecht |
|
||||
| Mehrwortig: snake_case | `oil_lamp`, `old_man` | `oilLamp`, `old-man`, `oil lamp` |
|
||||
| keine führende Ziffer | `key1` | `1key` |
|
||||
| nur `[a-z0-9_]` | `key2` | `key#2`, `key@2` |
|
||||
|
||||
Regex zur Validierung im `WorldValidator`:
|
||||
|
||||
```
|
||||
^[a-z][a-z0-9_]*$
|
||||
```
|
||||
|
||||
Geltungsbereich: id ist eindeutig **innerhalb seiner Entitätsart**. Ein Item und ein NPC dürfen `lamp` heißen (nicht empfohlen, aber technisch erlaubt).
|
||||
|
||||
## Package-Struktur
|
||||
|
||||
```
|
||||
thb.jeanluc.adventure
|
||||
├── model
|
||||
│ ├── Direction
|
||||
│ ├── Room
|
||||
│ ├── Player
|
||||
│ ├── Npc
|
||||
│ └── item <- eigener Subpackage wegen Hierarchie
|
||||
│ ├── Item (abstract)
|
||||
│ ├── ReadableItem
|
||||
│ ├── SwitchableItem
|
||||
│ └── PlainItem
|
||||
├── io
|
||||
├── command
|
||||
├── game
|
||||
└── loader
|
||||
└── dto
|
||||
```
|
||||
|
||||
**Wann ein Subpackage:** wenn eine Klassenfamilie ≥ 3 Klassen umfasst und eine eigene Abstraktion bildet (`item`, evtl. später `command.impl`). Sonst flach lassen.
|
||||
|
||||
## Naming
|
||||
|
||||
Java-Standard:
|
||||
- Klassen: `UpperCamelCase`
|
||||
- Methoden, Felder, Parameter: `lowerCamelCase`
|
||||
- Konstanten: `UPPER_SNAKE_CASE`
|
||||
- Packages: `lowercase`, ohne Underscores wenn möglich
|
||||
|
||||
YAML:
|
||||
- Keys: `lowerCamelCase` (`startRoom`, `readText`, `initialState`) — passt zu Jackson-Default-Mapping auf Java-Felder
|
||||
- IDs: siehe oben
|
||||
|
||||
## Javadoc
|
||||
|
||||
Pflicht laut Aufgabenstellung **an Klassen, Methoden, und Instanzvariablen**. Auch bei Lombok-Feldern wenn sie `@Getter`-exposed sind.
|
||||
|
||||
Keep it sachlich — was tut die Klasse/Methode, nicht wie. Keine Geschichten erzählen.
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit-Tests pro Domain-Klasse: `RoomTest`, `PlayerTest`, `ItemTest` etc.
|
||||
- Integration-Tests für Loader: `WorldLoaderTest` mit `src/test/resources/world/`-Fixtures
|
||||
- Naming: `methodName_condition_expectedResult` — z.B. `addExit_withNewDirection_storesNeighbour`
|
||||
|
||||
## Lombok-Cheatsheet
|
||||
|
||||
| Annotation | Wozu |
|
||||
|---|---|
|
||||
| `@Getter` | Getter für alle Felder |
|
||||
| `@RequiredArgsConstructor` | Konstruktor nur für finals ohne Initializer |
|
||||
| `@SuperBuilder` | Builder über Vererbung (statt `@Builder`) |
|
||||
| `@Slf4j` | `log`-Field für SLF4J |
|
||||
| `@ToString` | Nur wenn aussagekräftig — sonst weglassen |
|
||||
|
||||
**Vermeiden:** `@Data` (zu viel auf einmal, generiert `equals/hashCode` was bei Room/Player unerwünscht ist).
|
||||
73
Semesterprojekt/docs/data-structures.md
Normal file
73
Semesterprojekt/docs/data-structures.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Datenstrukturen
|
||||
|
||||
Bewusste Wahl jeder Collection — der Dozent bewertet das laut Aufgabenstellung explizit.
|
||||
|
||||
## Übersicht
|
||||
|
||||
| Verwendung | Struktur | Komplexität Lookup |
|
||||
|---|---|---|
|
||||
| Ausgänge eines Raums | `EnumMap<Direction, Room>` | O(1) |
|
||||
| Alle Räume der Welt | `HashMap<String, Room>` | O(1) |
|
||||
| Item-Registry (global) | `HashMap<String, Item>` | O(1) |
|
||||
| NPC-Registry (global) | `HashMap<String, Npc>` | O(1) |
|
||||
| Items in einem Raum | `LinkedHashMap<String, Item>` | O(1) |
|
||||
| NPCs in einem Raum | `LinkedHashMap<String, Npc>` | O(1) |
|
||||
| Spieler-Inventar | `LinkedHashMap<String, Item>` | O(1) |
|
||||
| Befehlsregistry | `HashMap<String, Command>` | O(1) |
|
||||
| NPC-Reaktionen | `HashMap<String, NpcReaction>` | O(1) |
|
||||
| Eingabehistorie (optional) | `ArrayDeque<String>` | O(1) Front/Back |
|
||||
|
||||
## Begründungen im Detail
|
||||
|
||||
### `EnumMap<Direction, Room>` für Raum-Ausgänge
|
||||
|
||||
- **Direction** ist Enum (`NORTH`, `SOUTH`, `EAST`, `WEST`, evtl. `UP`, `DOWN`)
|
||||
- `EnumMap` ist array-backed, kein Hashing nötig → schneller und kompakter als `HashMap`
|
||||
- Iteration in Enum-Deklarationsreihenfolge (stabil)
|
||||
|
||||
### `HashMap<String, Room>` für die Welt
|
||||
|
||||
- Lookup über `id` beim Auflösen von Exits
|
||||
- Reihenfolge irrelevant (keine Anzeige der gesamten Welt)
|
||||
- Standardwahl wenn nur Lookup gebraucht wird
|
||||
|
||||
### `LinkedHashMap<String, Item>` für Inventar & Raum-Items
|
||||
|
||||
Zwei Anforderungen gleichzeitig:
|
||||
|
||||
1. **O(1) Lookup** beim `take letter` / `read letter`
|
||||
2. **Stabile Anzeigereihenfolge** beim `inventory`
|
||||
|
||||
Eine plain `HashMap` würde Punkt 2 verletzen (Items springen scheinbar zufällig zwischen Ausgaben). Eine `ArrayList<Item>` würde Punkt 1 auf O(n) drücken und Duplikat-Prüfung verlangen.
|
||||
|
||||
Entscheidung "keine Stapel" (1 Item pro id) macht das Map-basierte Modell sauber.
|
||||
|
||||
### `HashMap<String, Command>` für Befehle
|
||||
|
||||
- O(1)-Dispatch
|
||||
- Aliase durch Mehrfach-Registrierung (`put("go", goCmd); put("move", goCmd);`)
|
||||
- Entspricht dem expliziten Tipp aus der Aufgabenstellung
|
||||
- Vermeidet wachsendes `switch`-Statement
|
||||
|
||||
### `ArrayDeque<String>` für Historie (optional)
|
||||
|
||||
- Falls Up-Arrow in der GUI gewünscht oder Befehlsverlauf
|
||||
- `ArrayDeque` ist `LinkedList` praktisch immer überlegen (bessere Cache-Lokalität, weniger Overhead)
|
||||
- Beidseitige O(1)-Operationen
|
||||
|
||||
## Bewusst NICHT gewählt
|
||||
|
||||
| Struktur | Warum nicht |
|
||||
|---|---|
|
||||
| `ArrayList<Item>` für Inventar | O(n)-Lookup, Duplikat-Handling nötig |
|
||||
| `HashMap<String, Item>` für Inventar | Anzeige-Reihenfolge instabil |
|
||||
| `TreeMap` irgendwo | Keine sortierte Iteration nötig, O(log n) ohne Nutzen |
|
||||
| `LinkedList` | `ArrayDeque` ist fast immer besser |
|
||||
| `Vector` / `Hashtable` | Legacy, synchronisiert (nicht gebraucht), langsamer |
|
||||
| `Map<String, String>` für Exits in Domain | Direction sollte Enum sein, nicht String |
|
||||
|
||||
## Threading
|
||||
|
||||
Single-threaded: Game-Loop liest, dispatcht, schreibt — keine parallelen Mutationen.
|
||||
|
||||
**Ausnahme:** Bei Swing-GUI läuft Input über den Event-Dispatch-Thread, der Game-Loop in einem Worker-Thread. Hier kommt `BlockingQueue<String>` (`ArrayBlockingQueue` reicht) als Brücke ins Spiel — siehe [architecture.md](architecture.md).
|
||||
135
Semesterprojekt/docs/implementation-status.md
Normal file
135
Semesterprojekt/docs/implementation-status.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Implementierungsstand & Reihenfolge
|
||||
|
||||
Stand: alle Phasen 1–7 implementiert, 67 Tests grün, End-to-End-Smoke-Test des Walking-Skeletons + YAML-Load erfolgreich.
|
||||
|
||||
## Phasen-Überblick
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P1["Phase 1<br/>Domain-Fundament"]
|
||||
P2["Phase 2<br/>Command-Schicht"]
|
||||
P3["Phase 3<br/>Walking Skeleton<br/>(Konsole + handgebaute Welt)"]
|
||||
P4["Phase 4<br/>YAML-Loading + Validator"]
|
||||
P5["Phase 5<br/>Restliche Commands"]
|
||||
P6["Phase 6<br/>NPCs end-to-end"]
|
||||
P7["Phase 7<br/>Swing-GUI (Bonus)"]
|
||||
|
||||
P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
|
||||
```
|
||||
|
||||
## Checkliste Phase 1: Domain
|
||||
|
||||
- [x] `Direction` (`model.Direction`)
|
||||
- [x] `Room` (`model.Room`)
|
||||
- [x] `Item` abstract (`model.item.Item`) mit `abstract use(GameContext)`
|
||||
- [x] `Npc` (`model.Npc`) inklusive `shell()`-Factory und `putReaction()`
|
||||
- [x] `NpcReaction` (`model.NpcReaction`)
|
||||
- [x] `GameIO` interface (`io.GameIO`)
|
||||
- [x] `Player` (`model.Player`)
|
||||
- [x] `World` (`model.World`)
|
||||
- [x] `GameContext` (`game.GameContext`)
|
||||
- [x] `ReadableItem` (`model.item.ReadableItem`)
|
||||
- [x] `SwitchableItem` (`model.item.SwitchableItem`)
|
||||
- [x] `PlainItem` (`model.item.PlainItem`)
|
||||
|
||||
## Checkliste Phase 2: Commands
|
||||
|
||||
- [x] `Command` interface (`command.Command`)
|
||||
- [x] `ParsedCommand` (record)
|
||||
- [x] `CommandRegistry` (`command.CommandRegistry`)
|
||||
- [x] `CommandParser` (`command.CommandParser`) mit Filler-Words
|
||||
- [x] `LookCommand`
|
||||
- [x] `GoCommand`
|
||||
- [x] `InventoryCommand`
|
||||
|
||||
## Checkliste Phase 3: Walking Skeleton
|
||||
|
||||
- [x] `ConsoleIO`
|
||||
- [x] `Game` (Loop)
|
||||
- [x] `App.main` (lädt sofort über YAML; der Walking-Skeleton-Zustand mit hartkodierter Welt wurde übersprungen, weil YAML-Load und Loop schon zusammen funktionieren)
|
||||
- [x] Probelauf: `look`, `go north`, `inventory` (siehe `GameTest`, `LookCommandTest`)
|
||||
|
||||
## Checkliste Phase 4: YAML-Loading
|
||||
|
||||
- [x] DTOs: `GameDto`, `ItemDto`, `RoomDto`, `NpcDto`, `ReactionDto`
|
||||
- [x] Test-Fixtures unter `src/test/resources/world/`
|
||||
- [x] `WorldLoader` (Happy-Path)
|
||||
- [x] `ReferenceResolver`
|
||||
- [x] `WorldValidator` (eine Validierungsregel pro Test)
|
||||
- [x] Echte Welt-YAMLs unter `src/main/resources/world/`
|
||||
- [x] `App.main` läuft direkt gegen YAML-Load
|
||||
|
||||
## Checkliste Phase 5: Restliche Commands
|
||||
|
||||
- [x] `TakeCommand`
|
||||
- [x] `DropCommand`
|
||||
- [x] `UseCommand`
|
||||
- [x] `ReadCommand`
|
||||
- [x] `ExamineCommand`
|
||||
- [x] `HelpCommand`
|
||||
- [x] `QuitCommand`
|
||||
|
||||
## Checkliste Phase 6: NPCs
|
||||
|
||||
- [x] `Npc` voll ausgebaut (greeting, reactions)
|
||||
- [x] `NpcReaction`
|
||||
- [x] `TalkCommand`
|
||||
- [x] `GiveCommand`
|
||||
- [x] NPCs in `WorldLoader` integriert
|
||||
- [x] End-to-End-Test: Lampe geben → Schlüssel bekommen (`TalkGiveCommandTest`)
|
||||
|
||||
## Checkliste Phase 7: Swing-GUI
|
||||
|
||||
- [x] `SwingIO` mit `LinkedBlockingQueue`-Brücke
|
||||
- [x] `AppGui.main`
|
||||
- [x] Game-Loop in Worker-Thread
|
||||
|
||||
## Build & Run
|
||||
|
||||
```sh
|
||||
mvn test # 67 Tests
|
||||
mvn -DskipTests exec:java -Dexec.mainClass=thb.jeanluc.adventure.App # Konsole
|
||||
mvn -DskipTests exec:java -Dexec.mainClass=thb.jeanluc.adventure.AppGui # Swing
|
||||
```
|
||||
|
||||
## Festgelegte Designentscheidungen
|
||||
|
||||
Nicht mehr offen, nicht nochmal diskutieren:
|
||||
|
||||
| Entscheidung | Wert |
|
||||
|---|---|
|
||||
| Item-Hierarchie | abstract Item + ReadableItem/SwitchableItem/PlainItem |
|
||||
| Item-Package | `model.item` (Subpackage) |
|
||||
| Switchable-State-Typ | `boolean` (kein Enum) |
|
||||
| Switchable-Felder | nur `state` (Builder mappt YAML `initialState`); kein separates `initialState`-Feld am Domain-Objekt |
|
||||
| use-Targets | argless `use X`, kein `use X on Y` |
|
||||
| Item kennt Standort | nein, „dumme" Items |
|
||||
| Hidden Items | nein |
|
||||
| State→Raum-Beschreibung | nein im MVP |
|
||||
| Room.description | `final`, immutable |
|
||||
| Room.describe() | nicht auf Room, im LookCommand |
|
||||
| Room.equals/hashCode | nicht überschreiben, Identity |
|
||||
| Room-NPCs-Feld | von Anfang an drin |
|
||||
| Bidirektionale Exits | manuell, kein Auto-Spiegeln |
|
||||
| GameContext-Inhalt | minimal: World + Player + GameIO |
|
||||
| IDs | lowercase snake_case slugs, kein UUID |
|
||||
| ID-Regex | `^[a-z][a-z0-9_]*$` |
|
||||
| YAML-Aufteilung | `game.yaml`, `items.yaml`, `rooms.yaml`, `npcs.yaml` |
|
||||
| DTO ↔ Domain | getrennt, Resolver-Phase löst String-IDs zu Referenzen auf |
|
||||
| Item-Type-Discriminator | YAML-Feld `type: plain|readable|switchable`, in `ItemFactory` als switch |
|
||||
| Codebase-Sprache | Englisch (Identifier, YAML, User-Strings) |
|
||||
| Doku-Sprache | Deutsch (Prose), Englisch (Code-Beispiele) |
|
||||
| Diagramme | Mermaid |
|
||||
| Lombok-Inheritance | `@SuperBuilder` |
|
||||
| `@Data` | vermeiden, einzelne Annotations bevorzugen |
|
||||
| Quit-Wiring | `QuitCommand.bind(Game)` nach Registry-Aufbau |
|
||||
| Help-Quelle | `HelpCommand` zieht aus `CommandRegistry.distinctCommands()` |
|
||||
| GameIO-Methodennamen | `readLine()` / `write(String)` (Java-üblich, konsistent) |
|
||||
| Lombok-Version | 1.18.42 (1.18.36 ist nicht Java-26-kompatibel) |
|
||||
|
||||
## Offen / nicht im MVP
|
||||
|
||||
- **Win-Condition**: Spiel endet nur per `quit`. Optionale Erweiterung: Bedingung in `game.yaml` (`winRoom`, `requiredItem`).
|
||||
- **Bedingte NPC-Reaktionen**, NPC-Memory, Quests — bewusst ausgelassen (siehe `npcs.md`).
|
||||
- **Item-Aliases** (z.B. `lamp` ↔ `oil_lamp`) — YAGNI bis konkreter Bedarf.
|
||||
- **Eingabehistorie** in der GUI — `ArrayDeque` ist vorgesehen, nicht umgesetzt.
|
||||
139
Semesterprojekt/docs/item-model.md
Normal file
139
Semesterprojekt/docs/item-model.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Item-Modell
|
||||
|
||||
Hierarchie aus abstraktem `Item` und drei konkreten Subtypen. Liegt im Subpackage `thb.jeanluc.adventure.model.item`.
|
||||
|
||||
## Hierarchie
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Item {
|
||||
<<abstract>>
|
||||
#String id
|
||||
#String name
|
||||
#String description
|
||||
+use(GameContext)* void
|
||||
}
|
||||
|
||||
class ReadableItem {
|
||||
-String readText
|
||||
+use(GameContext) void
|
||||
}
|
||||
|
||||
class SwitchableItem {
|
||||
-boolean state
|
||||
-boolean initialState
|
||||
-String onText
|
||||
-String offText
|
||||
+use(GameContext) void
|
||||
+isOn() boolean
|
||||
}
|
||||
|
||||
class PlainItem {
|
||||
+use(GameContext) void
|
||||
}
|
||||
|
||||
Item <|-- ReadableItem
|
||||
Item <|-- SwitchableItem
|
||||
Item <|-- PlainItem
|
||||
```
|
||||
|
||||
## Felder pro Klasse
|
||||
|
||||
### `Item` (abstract)
|
||||
|
||||
| Feld | Typ | Hinweis |
|
||||
|---|---|---|
|
||||
| `id` | `String` | unique slug, Lookup-Key |
|
||||
| `name` | `String` | Anzeigename |
|
||||
| `description` | `String` | `examine`-Output |
|
||||
|
||||
Abstrakte Methode: `public abstract void use(GameContext ctx)`
|
||||
|
||||
### `ReadableItem`
|
||||
|
||||
| Feld | Typ | Hinweis |
|
||||
|---|---|---|
|
||||
| `readText` | `String` | wird beim `read`/`use` ausgegeben |
|
||||
|
||||
`use()` schreibt `readText` über `ctx.io()`.
|
||||
|
||||
### `SwitchableItem`
|
||||
|
||||
| Feld | Typ | Hinweis |
|
||||
|---|---|---|
|
||||
| `state` | `boolean` | aktueller Zustand (mutable) |
|
||||
| `initialState` | `boolean` | aus YAML, im Konstruktor an `state` durchgereicht |
|
||||
| `onText` | `String` | Nachricht beim Einschalten |
|
||||
| `offText` | `String` | Nachricht beim Ausschalten |
|
||||
|
||||
`use()` toggelt `state` und schreibt den entsprechenden Text.
|
||||
|
||||
Bewusste Entscheidung: `boolean` statt `enum SwitchState`. Wenn später `BROKEN` o.ä. nötig wird, refactoren.
|
||||
|
||||
### `PlainItem`
|
||||
|
||||
Keine eigenen Felder. `use()` schreibt eine generische Nachricht („You can't use the X by itself."). Existiert, damit alle Items polymorph `use()` haben und das YAML einen konsistenten `type:`-Discriminator hat.
|
||||
|
||||
## Lombok-Setup
|
||||
|
||||
`@SuperBuilder` ist Pflicht, weil normales `@Builder` Vererbung nicht beherrscht.
|
||||
|
||||
```java
|
||||
@Getter
|
||||
@SuperBuilder
|
||||
@RequiredArgsConstructor
|
||||
public abstract class Item {
|
||||
protected final String id;
|
||||
protected final String name;
|
||||
protected final String description;
|
||||
|
||||
public abstract void use(GameContext ctx);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@SuperBuilder
|
||||
public class ReadableItem extends Item {
|
||||
private final String readText;
|
||||
|
||||
@Override
|
||||
public void use(GameContext ctx) {
|
||||
ctx.getIo().write(readText);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Konstruktion:
|
||||
```java
|
||||
ReadableItem.builder()
|
||||
.id("letter").name("Letter").description("A crumpled paper.")
|
||||
.readText("Meet me at midnight.")
|
||||
.build();
|
||||
```
|
||||
|
||||
## Jackson Polymorphism
|
||||
|
||||
```java
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = ReadableItem.class, name = "readable"),
|
||||
@JsonSubTypes.Type(value = SwitchableItem.class, name = "switchable"),
|
||||
@JsonSubTypes.Type(value = PlainItem.class, name = "plain")
|
||||
})
|
||||
public abstract class Item { ... }
|
||||
```
|
||||
|
||||
YAML-Pflichtfeld pro Item: `type: readable|switchable|plain`.
|
||||
|
||||
## Verworfen / bewusst nicht im MVP
|
||||
|
||||
- **`use X on Y`** (targeted use) — Items kennen ihren Kontext über `GameContext`, kein zweites Item als Parameter.
|
||||
- **Hidden Items** (sichtbar erst nach Aktion) — keine `visible`-Flag, alle Items sofort sichtbar.
|
||||
- **Item kennt seinen Standort** — Items sind „dumm", nur Room/Player wissen wer sie hält.
|
||||
- **Item-State beeinflusst Raumbeschreibung** (z.B. „Cellar dark unless lamp on") — wenn nötig, später per Conditions-System.
|
||||
- **`hasBeenRead`-Tracking** — Lesen bleibt idempotent.
|
||||
|
||||
## Player-Input-Matching
|
||||
|
||||
Player tippt `take lamp` → Match gegen `id`. Multi-Word-Items haben snake_case ids, also `take oil_lamp`.
|
||||
|
||||
Alias-Feld (`aliases: [lamp, oil]`) ist YAGNI bis ein konkreter Bedarf entsteht.
|
||||
78
Semesterprojekt/docs/loading-flow.md
Normal file
78
Semesterprojekt/docs/loading-flow.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Loading-Flow
|
||||
|
||||
Wie aus 4 YAML-Dateien eine spielbare `World` mit aufgelösten Referenzen wird.
|
||||
|
||||
## Phasen
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P1["<b>Phase 1: YAML → DTOs</b><br/>Jackson liest items.yaml, npcs.yaml,<br/>rooms.yaml, game.yaml in Records.<br/>Nur Daten, IDs als Strings."]
|
||||
P2["<b>Phase 2: Domain-Shells erzeugen</b><br/>Pro DTO ein leeres Domain-Objekt.<br/>itemRegistry, npcRegistry, roomRegistry."]
|
||||
P3["<b>Phase 3: Referenzen auflösen</b><br/>Room: items-ids → Item-Objekte<br/>Room: npcs-ids → Npc-Objekte<br/>Room: exits-Strings → Direction + Room-Ref<br/>Npc: reactions auflösen (gives, consumes)"]
|
||||
P4["<b>Phase 4: Validierung</b><br/>Validierungsregeln aus yaml-schemas.md.<br/>Wirft WorldLoadException bei Verstoss."]
|
||||
P5["<b>Phase 5: Player + World zusammenstellen</b><br/>Player mit startRoom + startGold.<br/>World für den Game-Loop bereit."]
|
||||
|
||||
P1 --> P2 --> P3 --> P4 --> P5
|
||||
```
|
||||
|
||||
## Klassen-Aufteilung
|
||||
|
||||
| Klasse | Verantwortung |
|
||||
|---|---|
|
||||
| `WorldLoader` | Orchestriert die Phasen, public API: `World load()` |
|
||||
| `ReferenceResolver` | Phase 3 isoliert (testbar) |
|
||||
| `WorldValidator` | Phase 4 isoliert (testbar) |
|
||||
| `WorldLoadException` | Eigene RuntimeException mit aussagekräftiger Message |
|
||||
|
||||
## Beispiel-Skizze
|
||||
|
||||
```java
|
||||
public class WorldLoader {
|
||||
|
||||
private final ObjectMapper yaml = new ObjectMapper(new YAMLFactory());
|
||||
|
||||
public World load() {
|
||||
// Phase 1
|
||||
List<ItemDto> itemDtos = readList("/world/items.yaml", ItemDto.class);
|
||||
List<NpcDto> npcDtos = readList("/world/npcs.yaml", NpcDto.class);
|
||||
List<RoomDto> roomDtos = readList("/world/rooms.yaml", RoomDto.class);
|
||||
GameDto gameDto = readSingle("/world/game.yaml", GameDto.class);
|
||||
|
||||
// Phase 2: Registries
|
||||
Map<String, Item> items = itemDtos.stream()
|
||||
.collect(Collectors.toMap(ItemDto::id, ItemFactory::fromDto));
|
||||
Map<String, Npc> npcs = npcDtos.stream()
|
||||
.collect(Collectors.toMap(NpcDto::id, NpcFactory::fromDto));
|
||||
Map<String, Room> rooms = roomDtos.stream()
|
||||
.collect(Collectors.toMap(RoomDto::id, RoomFactory::shellFromDto));
|
||||
|
||||
// Phase 3: Referenzen auflösen
|
||||
ReferenceResolver resolver = new ReferenceResolver(items, npcs, rooms);
|
||||
resolver.resolveRooms(roomDtos);
|
||||
resolver.resolveNpcs(npcDtos);
|
||||
|
||||
// Phase 4: validieren
|
||||
new WorldValidator(items, npcs, rooms, gameDto).validate();
|
||||
|
||||
// Phase 5
|
||||
Player player = new Player(rooms.get(gameDto.startRoom()), gameDto.startGold());
|
||||
return new World(rooms, items, npcs, player, gameDto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Warum getrennte Phasen?
|
||||
|
||||
- **Testbarkeit:** Resolver und Validator können isoliert mit fertigen DTOs gefüttert werden, ohne YAML auf der Platte
|
||||
- **Fehlerlokalisierung:** „Resolver hat versagt" vs. „Validator hat versagt" sind unterschiedliche Diagnosen
|
||||
- **Zirkuläre Referenzen** (Raum A → Raum B → Raum A) werden problemlos: in Phase 2 sind beide Shells da, Phase 3 verlinkt
|
||||
- **Fail-fast:** Spiel startet nicht mit kaputter Welt — besser als NullPointer beim ersten `go north`
|
||||
|
||||
## Tests pro Phase
|
||||
|
||||
| Test | Inhalt |
|
||||
|---|---|
|
||||
| `WorldLoaderTest` | Happy-Path: lädt Test-Fixtures aus `src/test/resources/world/` |
|
||||
| `ReferenceResolverTest` | DTOs als Eingabe, prüft Auflösung |
|
||||
| `WorldValidatorTest` | Pro Validierungsregel ein Test, der den Fehler triggert |
|
||||
| `WorldValidatorTest.happy()` | Vollständig valide Welt darf nicht werfen |
|
||||
119
Semesterprojekt/docs/npcs.md
Normal file
119
Semesterprojekt/docs/npcs.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# NPCs
|
||||
|
||||
Nicht-Spieler-Figuren — optionaler Bonusteil laut Aufgabenstellung. Hier mit Talk- und Give-Interaktion modelliert.
|
||||
|
||||
## Domain-Modell
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Npc {
|
||||
-String id
|
||||
-String name
|
||||
-String description
|
||||
-String greeting
|
||||
-Map~String, NpcReaction~ reactions
|
||||
+talk(GameContext) void
|
||||
+receive(Item, GameContext) boolean
|
||||
}
|
||||
|
||||
class NpcReaction {
|
||||
-Item consumes
|
||||
-Item gives
|
||||
-String response
|
||||
+apply(Player) void
|
||||
}
|
||||
|
||||
Npc "1" --> "*" NpcReaction : reactions
|
||||
```
|
||||
|
||||
- `reactions` ist `HashMap<String, NpcReaction>` mit dem Trigger-Item-id als Key. O(1) Nachschlagen beim `gib X an Y`.
|
||||
|
||||
## Interaktionen
|
||||
|
||||
### `talk <npc>`
|
||||
|
||||
- Sucht NPC im aktuellen Raum.
|
||||
- Wirft `greeting`-Text aus.
|
||||
- Mutiert nichts.
|
||||
|
||||
### `gib <item> an <npc>`
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Spieler
|
||||
participant Cmd as GiveCommand
|
||||
participant P as Player
|
||||
participant N as Npc
|
||||
participant IO
|
||||
|
||||
Spieler->>Cmd: give lamp old_man
|
||||
Cmd->>P: hasItem("lamp")?
|
||||
P-->>Cmd: yes
|
||||
Cmd->>N: receive(lamp)
|
||||
N->>N: reactions.get("lamp")
|
||||
alt Reaktion existiert
|
||||
N->>P: inventory.remove(lamp)
|
||||
N->>P: inventory.add(key)
|
||||
N-->>Cmd: response text
|
||||
Cmd->>IO: write(response)
|
||||
else keine Reaktion
|
||||
N-->>Cmd: false
|
||||
Cmd->>IO: write("The NPC does not react.")
|
||||
end
|
||||
```
|
||||
|
||||
## YAML-Beispiel
|
||||
|
||||
```yaml
|
||||
- id: old_man
|
||||
name: Old Man
|
||||
description: A stooped old man with a grey beard.
|
||||
greeting: |
|
||||
"Hello traveller. If you bring me the lamp,
|
||||
I will show you the way to the cellar."
|
||||
reactions:
|
||||
- onReceive: lamp
|
||||
response: |
|
||||
"Thank you! Here, take this key."
|
||||
gives: key
|
||||
consumes: lamp
|
||||
```
|
||||
|
||||
Felder einer Reaktion:
|
||||
|
||||
| Feld | Pflicht | Bedeutung |
|
||||
|---|---|---|
|
||||
| `onReceive` | ja | Item-id, das ausgelöst werden muss |
|
||||
| `response` | ja | Antworttext nach erfolgreicher Übergabe |
|
||||
| `gives` | nein | Item-id, das der NPC zurückgibt |
|
||||
| `consumes` | nein | Item-id, das aus Spielerinventar entfernt wird (oft = `onReceive`) |
|
||||
|
||||
## Speichern in Räumen
|
||||
|
||||
Räume halten ihre NPCs analog zu Items:
|
||||
|
||||
```java
|
||||
private final LinkedHashMap<String, Npc> npcs = new LinkedHashMap<>();
|
||||
```
|
||||
|
||||
- `LinkedHashMap` weil O(1)-Lookup beim `talk <npc>` *und* stabile Reihenfolge in der Raumbeschreibung („Here is: Old Man, Innkeeper").
|
||||
|
||||
## Erweiterungen (bewusst nicht im MVP)
|
||||
|
||||
- Bedingte Dialoge („wenn Quest X erledigt, sag Y")
|
||||
- Kauf/Verkauf mit Gold
|
||||
- NPC bewegt sich zwischen Räumen
|
||||
- Mehrfache Reaktionsketten (NPC-Memory)
|
||||
|
||||
Diese würden ein eigenes Quest-/Event-System rechtfertigen. Für 3 Bonuspunkte überdimensioniert.
|
||||
|
||||
## Validierungsregeln
|
||||
|
||||
Wiederholung aus [yaml-schemas.md](yaml-schemas.md), hier explizit pro NPC:
|
||||
|
||||
1. `id` eindeutig in `npcs.yaml`
|
||||
2. `greeting` nicht leer (sonst sinnloses NPC)
|
||||
3. Jede `onReceive`-id existiert in `items.yaml`
|
||||
4. Jede `gives`-id existiert in `items.yaml`
|
||||
5. Jede `consumes`-id existiert in `items.yaml`
|
||||
6. Innerhalb eines NPCs ist `onReceive` eindeutig (keine zwei Reaktionen auf dasselbe Item)
|
||||
143
Semesterprojekt/docs/yaml-schemas.md
Normal file
143
Semesterprojekt/docs/yaml-schemas.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# YAML-Schemas
|
||||
|
||||
Alle Dateien liegen unter `src/main/resources/world/` und werden via Classpath geladen.
|
||||
|
||||
## `game.yaml`
|
||||
|
||||
Spiel-Konfiguration und Startbedingungen.
|
||||
|
||||
```yaml
|
||||
title: Haunted Manor
|
||||
version: 1.0
|
||||
startRoom: kitchen
|
||||
startGold: 0
|
||||
welcomeMessage: |
|
||||
Welcome to the Haunted Manor.
|
||||
Type 'help' to see available commands.
|
||||
```
|
||||
|
||||
**Felder:**
|
||||
- `title` — Anzeigename des Spiels
|
||||
- `version` — frei
|
||||
- `startRoom` — id eines Raums aus `rooms.yaml`. Validierung: muss existieren.
|
||||
- `startGold` — Start-Goldbetrag des Spielers
|
||||
- `welcomeMessage` — wird beim Spielstart ausgegeben
|
||||
|
||||
## `items.yaml`
|
||||
|
||||
Liste aller Items. Items haben Zustand (`state`-Feld, optional), der zur Laufzeit verändert werden kann.
|
||||
|
||||
```yaml
|
||||
- id: letter
|
||||
name: Letter
|
||||
description: A crumpled piece of paper.
|
||||
readable: true
|
||||
readText: |
|
||||
"Meet me at midnight in the cellar. - A."
|
||||
|
||||
- id: lamp
|
||||
name: Oil Lamp
|
||||
description: An old oil lamp.
|
||||
switchable: true
|
||||
initialState: off # off | on
|
||||
|
||||
- id: shovel
|
||||
name: Shovel
|
||||
description: A rusty shovel.
|
||||
|
||||
- id: key
|
||||
name: Key
|
||||
description: A small brass key.
|
||||
```
|
||||
|
||||
**Felder:**
|
||||
- `id` — eindeutig, klein geschrieben, ohne Sonderzeichen
|
||||
- `name` — Anzeigename
|
||||
- `description` — Beschreibung beim `untersuche` / `lies`
|
||||
- `readable`, `switchable`, ... — feature-Flags für `Use`-Verhalten
|
||||
|
||||
**Hinweis:** Die genauen Felder hängen vom Item-Modell ab. Alternative: alle Items haben dieselben Felder, das Verhalten wird durch eine `behavior`-Discriminator-Property gesteuert (`behavior: readable` / `behavior: switchable`). Pro/Contra in der Implementierung entscheiden.
|
||||
|
||||
## `rooms.yaml`
|
||||
|
||||
Liste aller Räume. Items und NPCs werden per id referenziert, nicht eingebettet.
|
||||
|
||||
```yaml
|
||||
- id: kitchen
|
||||
name: Old Kitchen
|
||||
description: |
|
||||
A dusty kitchen. A letter lies on the table.
|
||||
A door leads north.
|
||||
exits:
|
||||
north: hallway
|
||||
east: cellar
|
||||
items: [letter, lamp]
|
||||
npcs: [old_man]
|
||||
|
||||
- id: hallway
|
||||
name: Dark Hallway
|
||||
description: |
|
||||
A long, dimly lit hallway. It smells musty.
|
||||
exits:
|
||||
south: kitchen
|
||||
west: library
|
||||
items: []
|
||||
npcs: []
|
||||
|
||||
- id: library
|
||||
name: Library
|
||||
description: |
|
||||
Tall shelves full of books. An old chest
|
||||
stands in one corner.
|
||||
exits:
|
||||
east: hallway
|
||||
items: [shovel]
|
||||
npcs: []
|
||||
|
||||
- id: cellar
|
||||
name: Damp Cellar
|
||||
description: |
|
||||
It is cold and damp. You need a light source.
|
||||
exits:
|
||||
west: kitchen
|
||||
items: [key]
|
||||
npcs: []
|
||||
```
|
||||
|
||||
**Felder:**
|
||||
- `id`, `name`, `description` — analog Items
|
||||
- `exits` — Map von Richtungs-String auf Raum-id. Richtungs-Strings müssen zu `Direction`-Enum parsbar sein.
|
||||
- `items`, `npcs` — Listen von ids aus den jeweiligen Registries
|
||||
|
||||
## `npcs.yaml`
|
||||
|
||||
Liste aller NPCs. Detaillierte Interaktion siehe [npcs.md](npcs.md).
|
||||
|
||||
```yaml
|
||||
- id: old_man
|
||||
name: Old Man
|
||||
description: A stooped old man with a grey beard.
|
||||
greeting: |
|
||||
"Hello traveller. If you bring me the lamp,
|
||||
I will show you the way to the cellar."
|
||||
reactions:
|
||||
- onReceive: lamp
|
||||
response: |
|
||||
"Thank you! Here, take this key."
|
||||
gives: key
|
||||
consumes: lamp
|
||||
```
|
||||
|
||||
## Validierungsregeln (zentral)
|
||||
|
||||
Beim Loading wird geprüft (siehe [loading-flow.md](loading-flow.md)):
|
||||
|
||||
1. Alle ids sind eindeutig innerhalb ihrer Datei.
|
||||
2. Jede in `rooms.yaml` referenzierte Item-id existiert in `items.yaml`.
|
||||
3. Jede in `rooms.yaml` referenzierte NPC-id existiert in `npcs.yaml`.
|
||||
4. Jede `exits`-Ziel-id existiert in `rooms.yaml`.
|
||||
5. Jeder `exits`-Richtungs-String ist zu `Direction` parsbar.
|
||||
6. `game.yaml.startRoom` existiert.
|
||||
7. NPC-Reaktionen: `onReceive`, `gives`, `consumes` referenzieren existierende Item-ids.
|
||||
|
||||
Bei Verstoss → Exception, Spielstart bricht ab.
|
||||
Reference in New Issue
Block a user