- findItem(String itemId) {
+ return Optional.ofNullable(inventory.get(itemId));
+ }
+
+ /**
+ * Convenience predicate for inventory containment.
+ *
+ * @param itemId id of the item to test
+ * @return {@code true} if the player holds the item
+ */
+ public boolean hasItem(String itemId) {
+ return inventory.containsKey(itemId);
+ }
+}
diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java
new file mode 100644
index 0000000..f104fed
--- /dev/null
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Room.java
@@ -0,0 +1,114 @@
+package thb.jeanluc.adventure.model;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import thb.jeanluc.adventure.model.item.Item;
+
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.Optional;
+
+/**
+ * A location in the game world. Rooms are connected to neighbouring rooms
+ * through {@link Direction}-keyed exits, and may contain items and NPCs
+ * that the player can interact with.
+ *
+ * Identity, name, and description are fixed after construction; the
+ * exits, items, and NPCs collections are mutable to support the player
+ * picking up items, dropping them, and the world otherwise evolving.
+ *
+ * Mutation should go through the convenience methods on this class so
+ * that ids stay consistent with the contained objects.
+ */
+@Getter
+@RequiredArgsConstructor
+public class Room {
+
+ /** Unique identifier of this room within the world. */
+ private final String id;
+
+ /** Human-readable display name shown to the player. */
+ private final String name;
+
+ /** Description shown when the player enters the room or types {@code look}. */
+ private final String description;
+
+ /** Outgoing exits from this room, keyed by direction. */
+ private final EnumMap exits = new EnumMap<>(Direction.class);
+
+ /** Items currently in this room, keyed by item id. Insertion-ordered for stable display. */
+ private final LinkedHashMap items = new LinkedHashMap<>();
+
+ /** NPCs currently in this room, keyed by npc id. Insertion-ordered for stable display. */
+ private final LinkedHashMap npcs = new LinkedHashMap<>();
+
+ /**
+ * Connects this room to another in the given direction. Does not
+ * create the reverse connection — callers must set that up
+ * explicitly if a bidirectional path is desired.
+ *
+ * @param direction the direction in which the neighbour lies
+ * @param neighbour the room reachable from here in that direction
+ */
+ public void addExit(Direction direction, Room neighbour) {
+ exits.put(direction, neighbour);
+ }
+
+ /**
+ * Looks up the room reachable in the given direction.
+ *
+ * @param direction the direction to query
+ * @return the connected room, or empty if there is no exit that way
+ */
+ public Optional getExit(Direction direction) {
+ return Optional.ofNullable(exits.get(direction));
+ }
+
+ /**
+ * Places an item in this room.
+ *
+ * @param item the item to add; its id is used as the map key
+ */
+ public void addItem(Item item) {
+ items.put(item.getId(), item);
+ }
+
+ /**
+ * Removes an item from this room by id.
+ *
+ * @param itemId id of the item to remove
+ * @return the removed item, or empty if no such item was present
+ */
+ public Optional- removeItem(String itemId) {
+ return Optional.ofNullable(items.remove(itemId));
+ }
+
+ /**
+ * Looks up an item in this room without removing it.
+ *
+ * @param itemId id of the item to find
+ * @return the item, or empty if no such item is present
+ */
+ public Optional
- findItem(String itemId) {
+ return Optional.ofNullable(items.get(itemId));
+ }
+
+ /**
+ * Places an NPC in this room.
+ *
+ * @param npc the NPC to add; its id is used as the map key
+ */
+ public void addNpc(Npc npc) {
+ npcs.put(npc.getId(), npc);
+ }
+
+ /**
+ * Looks up an NPC in this room.
+ *
+ * @param npcId id of the NPC to find
+ * @return the NPC, or empty if no such NPC is present
+ */
+ public Optional findNpc(String npcId) {
+ return Optional.ofNullable(npcs.get(npcId));
+ }
+}
diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java
new file mode 100644
index 0000000..d2fb395
--- /dev/null
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java
@@ -0,0 +1,32 @@
+package thb.jeanluc.adventure.model;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import thb.jeanluc.adventure.model.item.Item;
+
+import java.util.Map;
+
+/**
+ * Aggregate root holding every entity in the game world plus global
+ * metadata read from {@code game.yaml}. Produced by the
+ * {@code WorldLoader} and handed to the game loop.
+ */
+@Getter
+@RequiredArgsConstructor
+public class World {
+
+ /** Global lookup of rooms by id. */
+ private final Map rooms;
+
+ /** Global lookup of item prototypes by id. */
+ private final Map items;
+
+ /** Global lookup of NPCs by id. */
+ private final Map npcs;
+
+ /** Display title shown to the player at startup. */
+ private final String title;
+
+ /** Welcome message printed once when the game starts. */
+ private final String welcomeMessage;
+}
diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java
new file mode 100644
index 0000000..504d805
--- /dev/null
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/Item.java
@@ -0,0 +1,43 @@
+package thb.jeanluc.adventure.model.item;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import thb.jeanluc.adventure.game.GameContext;
+
+/**
+ * Abstract base class for every interactive object in the world.
+ * Concrete subclasses implement {@link #use(GameContext)} to provide
+ * type-specific behaviour. Items are intentionally "dumb": they do not
+ * know which room or inventory currently holds them.
+ */
+@Getter
+@SuperBuilder
+@RequiredArgsConstructor
+@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 {
+
+ /** Unique identifier of this item, used for lookup and player input matching. */
+ protected final String id;
+
+ /** Human-readable display name shown to the player. */
+ protected final String name;
+
+ /** Long description shown by the {@code examine} command. */
+ protected final String description;
+
+ /**
+ * Executes the item's primary action. Side effects (text output,
+ * mutation of player state) flow through the given context.
+ *
+ * @param ctx active game context providing player, world, and IO
+ */
+ public abstract void use(GameContext ctx);
+}
diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/PlainItem.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/PlainItem.java
new file mode 100644
index 0000000..ee15e9f
--- /dev/null
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/PlainItem.java
@@ -0,0 +1,23 @@
+package thb.jeanluc.adventure.model.item;
+
+import lombok.experimental.SuperBuilder;
+import thb.jeanluc.adventure.game.GameContext;
+
+/**
+ * Item with no inherent behaviour. Exists so that every item in the
+ * world can polymorphically answer {@link #use(GameContext)} and so the
+ * YAML schema can rely on a uniform {@code type} discriminator.
+ */
+@SuperBuilder
+public class PlainItem extends Item {
+
+ /**
+ * Writes a generic message that this item cannot be used on its own.
+ *
+ * @param ctx active game context
+ */
+ @Override
+ public void use(GameContext ctx) {
+ ctx.getIo().write("You can't use the " + name + " by itself.");
+ }
+}
diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/ReadableItem.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/ReadableItem.java
new file mode 100644
index 0000000..84fcf5b
--- /dev/null
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/ReadableItem.java
@@ -0,0 +1,27 @@
+package thb.jeanluc.adventure.model.item;
+
+import lombok.Getter;
+import lombok.experimental.SuperBuilder;
+import thb.jeanluc.adventure.game.GameContext;
+
+/**
+ * Item that displays a piece of text when used or read. Typical examples
+ * include letters, signs, and books.
+ */
+@Getter
+@SuperBuilder
+public class ReadableItem extends Item {
+
+ /** Text written on the item; printed to the player on {@code use}/{@code read}. */
+ private final String readText;
+
+ /**
+ * Writes the item's text to the player's output.
+ *
+ * @param ctx active game context
+ */
+ @Override
+ public void use(GameContext ctx) {
+ ctx.getIo().write(readText);
+ }
+}
diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/SwitchableItem.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/SwitchableItem.java
new file mode 100644
index 0000000..e3b7684
--- /dev/null
+++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/item/SwitchableItem.java
@@ -0,0 +1,46 @@
+package thb.jeanluc.adventure.model.item;
+
+import lombok.Getter;
+import lombok.experimental.SuperBuilder;
+import thb.jeanluc.adventure.game.GameContext;
+
+/**
+ * Item with two boolean states (on / off). Each {@code use} toggles the
+ * state and prints the corresponding transition message.
+ */
+@Getter
+@SuperBuilder
+public class SwitchableItem extends Item {
+
+ /** Text written when the item transitions to on. */
+ private final String onText;
+
+ /** Text written when the item transitions to off. */
+ private final String offText;
+
+ /**
+ * Current on/off state. Seeded by the builder from the YAML
+ * {@code initialState} field via the item factory.
+ */
+ private boolean state;
+
+ /**
+ * Toggles the state and writes the corresponding transition text.
+ *
+ * @param ctx active game context
+ */
+ @Override
+ public void use(GameContext ctx) {
+ state = !state;
+ ctx.getIo().write(state ? onText : offText);
+ }
+
+ /**
+ * Convenience predicate for the current state.
+ *
+ * @return {@code true} if the item is currently on
+ */
+ public boolean isOn() {
+ return state;
+ }
+}
diff --git a/Semesterprojekt/src/main/resources/logback.xml b/Semesterprojekt/src/main/resources/logback.xml
new file mode 100644
index 0000000..1d635ce
--- /dev/null
+++ b/Semesterprojekt/src/main/resources/logback.xml
@@ -0,0 +1,12 @@
+
+
+ System.err
+
+ %d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n
+
+
+
+
+
+
+
diff --git a/Semesterprojekt/src/main/resources/world/game.yaml b/Semesterprojekt/src/main/resources/world/game.yaml
new file mode 100644
index 0000000..f38d152
--- /dev/null
+++ b/Semesterprojekt/src/main/resources/world/game.yaml
@@ -0,0 +1,7 @@
+title: Haunted Manor
+version: "1.0"
+startRoom: kitchen
+startGold: 0
+welcomeMessage: |
+ Welcome to the Haunted Manor.
+ Type 'help' to see available commands.
diff --git a/Semesterprojekt/src/main/resources/world/items.yaml b/Semesterprojekt/src/main/resources/world/items.yaml
new file mode 100644
index 0000000..06d0650
--- /dev/null
+++ b/Semesterprojekt/src/main/resources/world/items.yaml
@@ -0,0 +1,24 @@
+- type: readable
+ id: letter
+ name: Letter
+ description: A crumpled piece of paper.
+ readText: |
+ "Meet me at midnight in the cellar. - A."
+
+- type: switchable
+ id: lamp
+ name: Oil Lamp
+ description: An old oil lamp, heavy with fuel.
+ initialState: false
+ onText: The lamp flares to life, casting a warm glow.
+ offText: You snuff out the lamp.
+
+- type: plain
+ id: shovel
+ name: Shovel
+ description: A rusty shovel. Sturdy enough to dig.
+
+- type: plain
+ id: key
+ name: Brass Key
+ description: A small brass key, polished from use.
diff --git a/Semesterprojekt/src/main/resources/world/npcs.yaml b/Semesterprojekt/src/main/resources/world/npcs.yaml
new file mode 100644
index 0000000..5d9b04c
--- /dev/null
+++ b/Semesterprojekt/src/main/resources/world/npcs.yaml
@@ -0,0 +1,12 @@
+- id: old_man
+ name: Old Man
+ description: A stooped old man with a grey beard.
+ greeting: |
+ "Hello, traveller. If you bring me the oil lamp,
+ I will give you the key to the cellar."
+ reactions:
+ - onReceive: lamp
+ response: |
+ "Thank you! Here, take this key."
+ gives: key
+ consumes: lamp
diff --git a/Semesterprojekt/src/main/resources/world/rooms.yaml b/Semesterprojekt/src/main/resources/world/rooms.yaml
new file mode 100644
index 0000000..8dccb7d
--- /dev/null
+++ b/Semesterprojekt/src/main/resources/world/rooms.yaml
@@ -0,0 +1,41 @@
+- id: kitchen
+ name: Old Kitchen
+ description: |
+ A dusty kitchen. Cobwebs hang from the rafters
+ and a letter lies on the table.
+ 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.
+ Faint footsteps echo somewhere far away.
+ exits:
+ south: kitchen
+ west: library
+ items: []
+ npcs: []
+
+- id: library
+ name: Library
+ description: |
+ Tall shelves crammed with mouldering books.
+ An old chest stands in the corner.
+ exits:
+ east: hallway
+ items: [shovel]
+ npcs: []
+
+- id: cellar
+ name: Damp Cellar
+ description: |
+ Cold, damp, and very dark. You can barely
+ make out shapes against the far wall.
+ exits:
+ west: kitchen
+ items: []
+ npcs: []
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/CommandParserTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/CommandParserTest.java
new file mode 100644
index 0000000..1d7bd59
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/CommandParserTest.java
@@ -0,0 +1,38 @@
+package thb.jeanluc.adventure.command;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandParserTest {
+
+ private final CommandParser parser = new CommandParser();
+
+ @Test
+ void parse_blankInput_yieldsEmptyVerb() {
+ assertThat(parser.parse("").verb()).isEmpty();
+ assertThat(parser.parse(" ").verb()).isEmpty();
+ assertThat(parser.parse(null).verb()).isEmpty();
+ }
+
+ @Test
+ void parse_lowercases() {
+ ParsedCommand p = parser.parse("Go NORTH");
+ assertThat(p.verb()).isEqualTo("go");
+ assertThat(p.args()).containsExactly("north");
+ }
+
+ @Test
+ void parse_dropsFillers() {
+ ParsedCommand p = parser.parse("go to the north");
+ assertThat(p.verb()).isEqualTo("go");
+ assertThat(p.args()).containsExactly("north");
+ }
+
+ @Test
+ void parse_giveItemToNpc_keepsBoth() {
+ ParsedCommand p = parser.parse("give lamp to old_man");
+ assertThat(p.verb()).isEqualTo("give");
+ assertThat(p.args()).containsExactly("lamp", "old_man");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/CommandRegistryTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/CommandRegistryTest.java
new file mode 100644
index 0000000..9c09530
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/CommandRegistryTest.java
@@ -0,0 +1,43 @@
+package thb.jeanluc.adventure.command;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.game.GameContext;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CommandRegistryTest {
+
+ private static class StubCommand implements Command {
+ @Override public void execute(GameContext ctx, List args) {}
+ @Override public String help() { return "stub"; }
+ }
+
+ @Test
+ void register_andFindByEachAlias() {
+ CommandRegistry r = new CommandRegistry();
+ StubCommand cmd = new StubCommand();
+ r.register(cmd, "go", "move", "walk");
+
+ assertThat(r.find("go")).contains(cmd);
+ assertThat(r.find("MOVE")).contains(cmd);
+ assertThat(r.find("walk")).contains(cmd);
+ }
+
+ @Test
+ void find_unknownVerb_returnsEmpty() {
+ assertThat(new CommandRegistry().find("nope")).isEmpty();
+ }
+
+ @Test
+ void distinctCommands_deduplicatesAliases() {
+ CommandRegistry r = new CommandRegistry();
+ StubCommand a = new StubCommand();
+ StubCommand b = new StubCommand();
+ r.register(a, "x", "y");
+ r.register(b, "z");
+
+ assertThat(r.distinctCommands()).containsExactlyInAnyOrder(a, b);
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/CommandTestSupport.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/CommandTestSupport.java
new file mode 100644
index 0000000..51c644c
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/CommandTestSupport.java
@@ -0,0 +1,60 @@
+package thb.jeanluc.adventure.command.impl;
+
+import thb.jeanluc.adventure.game.GameContext;
+import thb.jeanluc.adventure.io.TestIO;
+import thb.jeanluc.adventure.model.Direction;
+import thb.jeanluc.adventure.model.Npc;
+import thb.jeanluc.adventure.model.NpcReaction;
+import thb.jeanluc.adventure.model.Player;
+import thb.jeanluc.adventure.model.Room;
+import thb.jeanluc.adventure.model.item.Item;
+import thb.jeanluc.adventure.model.item.PlainItem;
+import thb.jeanluc.adventure.model.item.ReadableItem;
+import thb.jeanluc.adventure.model.item.SwitchableItem;
+
+/**
+ * Tiny world used by the command tests. Two rooms (kitchen → hallway),
+ * a few items, and one NPC with one reaction.
+ */
+public final class CommandTestSupport {
+
+ private CommandTestSupport() {}
+
+ public static class World {
+ public final Room kitchen;
+ public final Room hallway;
+ public final Item lamp;
+ public final Item letter;
+ public final Item key;
+ public final Item shovel;
+ public final Npc oldMan;
+ public final Player player;
+ public final TestIO io = new TestIO();
+ public final GameContext ctx;
+
+ public World() {
+ kitchen = new Room("kitchen", "Kitchen", "kitchen desc");
+ hallway = new Room("hallway", "Hallway", "hallway desc");
+ kitchen.addExit(Direction.NORTH, hallway);
+ hallway.addExit(Direction.SOUTH, kitchen);
+
+ lamp = SwitchableItem.builder().id("lamp").name("Lamp").description("a lamp")
+ .state(false).onText("on").offText("off").build();
+ letter = ReadableItem.builder().id("letter").name("Letter").description("a letter")
+ .readText("dear reader").build();
+ key = PlainItem.builder().id("key").name("Key").description("brass key").build();
+ shovel = PlainItem.builder().id("shovel").name("Shovel").description("rusty").build();
+
+ kitchen.addItem(lamp);
+ kitchen.addItem(letter);
+
+ oldMan = Npc.shell("old_man", "Old Man", "stooped", "greetings");
+ oldMan.putReaction("lamp", NpcReaction.builder()
+ .consumes(lamp).gives(key).response("Take the key.").build());
+ kitchen.addNpc(oldMan);
+
+ player = new Player(kitchen, 0);
+ ctx = new GameContext(null, player, io);
+ }
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandTest.java
new file mode 100644
index 0000000..fde1059
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/GoCommandTest.java
@@ -0,0 +1,48 @@
+package thb.jeanluc.adventure.command.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GoCommandTest {
+
+ @Test
+ void execute_validDirection_movesPlayer() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+
+ new GoCommand().execute(w.ctx, List.of("north"));
+
+ assertThat(w.player.getCurrentRoom()).isEqualTo(w.hallway);
+ }
+
+ @Test
+ void execute_noExit_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+
+ new GoCommand().execute(w.ctx, List.of("south"));
+
+ assertThat(w.player.getCurrentRoom()).isEqualTo(w.kitchen);
+ assertThat(w.io.allOutput()).contains("can't go south");
+ }
+
+ @Test
+ void execute_unknownDirection_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+
+ new GoCommand().execute(w.ctx, List.of("upwards"));
+
+ assertThat(w.player.getCurrentRoom()).isEqualTo(w.kitchen);
+ assertThat(w.io.allOutput()).contains("upwards");
+ }
+
+ @Test
+ void execute_noArg_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+
+ new GoCommand().execute(w.ctx, List.of());
+
+ assertThat(w.io.allOutput()).contains("Go where");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/InventoryCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/InventoryCommandTest.java
new file mode 100644
index 0000000..599af6c
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/InventoryCommandTest.java
@@ -0,0 +1,28 @@
+package thb.jeanluc.adventure.command.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class InventoryCommandTest {
+
+ @Test
+ void empty_writesEmptyMessage() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new InventoryCommand().execute(w.ctx, List.of());
+ assertThat(w.io.lastOutput()).contains("not carrying");
+ }
+
+ @Test
+ void withItems_listsByName() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("letter"));
+ new TakeCommand().execute(w.ctx, List.of("lamp"));
+
+ new InventoryCommand().execute(w.ctx, List.of());
+
+ assertThat(w.io.lastOutput()).contains("Letter").contains("Lamp");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/LookCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/LookCommandTest.java
new file mode 100644
index 0000000..b7b7150
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/LookCommandTest.java
@@ -0,0 +1,37 @@
+package thb.jeanluc.adventure.command.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class LookCommandTest {
+
+ @Test
+ void look_writesNameDescriptionItemsNpcsAndExits() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+
+ new LookCommand().execute(w.ctx, List.of());
+
+ String text = w.io.lastOutput();
+ assertThat(text).contains("Kitchen");
+ assertThat(text).contains("kitchen desc");
+ assertThat(text).contains("Lamp").contains("Letter");
+ assertThat(text).contains("Old Man");
+ assertThat(text).contains("north");
+ }
+
+ @Test
+ void look_noItemsNoNpcs_stillWorks() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ w.player.setCurrentRoom(w.hallway);
+
+ new LookCommand().execute(w.ctx, List.of());
+
+ String text = w.io.lastOutput();
+ assertThat(text).contains("Hallway");
+ assertThat(text).contains("south");
+ assertThat(text).doesNotContain("You see");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/TakeDropCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/TakeDropCommandTest.java
new file mode 100644
index 0000000..f7df093
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/TakeDropCommandTest.java
@@ -0,0 +1,45 @@
+package thb.jeanluc.adventure.command.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TakeDropCommandTest {
+
+ @Test
+ void take_movesItemFromRoomToInventory() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+
+ new TakeCommand().execute(w.ctx, List.of("lamp"));
+
+ assertThat(w.player.hasItem("lamp")).isTrue();
+ assertThat(w.kitchen.findItem("lamp")).isEmpty();
+ }
+
+ @Test
+ void take_unknownItem_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("dragon"));
+ assertThat(w.io.allOutput()).contains("no 'dragon'");
+ }
+
+ @Test
+ void drop_movesItemFromInventoryToRoom() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("lamp"));
+
+ new DropCommand().execute(w.ctx, List.of("lamp"));
+
+ assertThat(w.player.hasItem("lamp")).isFalse();
+ assertThat(w.kitchen.findItem("lamp")).isPresent();
+ }
+
+ @Test
+ void drop_notInInventory_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new DropCommand().execute(w.ctx, List.of("lamp"));
+ assertThat(w.io.allOutput()).contains("not carrying");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/TalkGiveCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/TalkGiveCommandTest.java
new file mode 100644
index 0000000..4029042
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/TalkGiveCommandTest.java
@@ -0,0 +1,64 @@
+package thb.jeanluc.adventure.command.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TalkGiveCommandTest {
+
+ @Test
+ void talk_existingNpc_writesGreeting() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TalkCommand().execute(w.ctx, List.of("old_man"));
+ assertThat(w.io.lastOutput()).contains("Old Man").contains("greetings");
+ }
+
+ @Test
+ void talk_missingNpc_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TalkCommand().execute(w.ctx, List.of("ghost"));
+ assertThat(w.io.allOutput()).contains("no 'ghost'");
+ }
+
+ @Test
+ void give_matchingReaction_swapsItems() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("lamp"));
+
+ new GiveCommand().execute(w.ctx, List.of("lamp", "old_man"));
+
+ assertThat(w.player.hasItem("lamp")).isFalse();
+ assertThat(w.player.hasItem("key")).isTrue();
+ assertThat(w.io.lastOutput()).contains("Take the key");
+ }
+
+ @Test
+ void give_noReaction_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("letter"));
+
+ new GiveCommand().execute(w.ctx, List.of("letter", "old_man"));
+
+ assertThat(w.player.hasItem("letter")).isTrue();
+ assertThat(w.io.lastOutput()).contains("does not react");
+ }
+
+ @Test
+ void give_unknownNpc_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("lamp"));
+
+ new GiveCommand().execute(w.ctx, List.of("lamp", "ghost"));
+
+ assertThat(w.io.allOutput()).contains("no 'ghost'");
+ }
+
+ @Test
+ void give_itemNotHeld_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new GiveCommand().execute(w.ctx, List.of("lamp", "old_man"));
+ assertThat(w.io.allOutput()).contains("not carrying");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/UseReadExamineCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/UseReadExamineCommandTest.java
new file mode 100644
index 0000000..d6d62b2
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/impl/UseReadExamineCommandTest.java
@@ -0,0 +1,51 @@
+package thb.jeanluc.adventure.command.impl;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UseReadExamineCommandTest {
+
+ @Test
+ void use_readableInInventory_writesText() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("letter"));
+
+ new UseCommand().execute(w.ctx, List.of("letter"));
+
+ assertThat(w.io.lastOutput()).isEqualTo("dear reader");
+ }
+
+ @Test
+ void use_unknownItem_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new UseCommand().execute(w.ctx, List.of("dragon"));
+ assertThat(w.io.allOutput()).contains("no 'dragon'");
+ }
+
+ @Test
+ void read_nonReadable_complains() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new TakeCommand().execute(w.ctx, List.of("lamp"));
+
+ new ReadCommand().execute(w.ctx, List.of("lamp"));
+
+ assertThat(w.io.lastOutput()).contains("nothing to read");
+ }
+
+ @Test
+ void examine_item_writesDescription() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new ExamineCommand().execute(w.ctx, List.of("letter"));
+ assertThat(w.io.lastOutput()).isEqualTo("a letter");
+ }
+
+ @Test
+ void examine_npc_writesDescription() {
+ CommandTestSupport.World w = new CommandTestSupport.World();
+ new ExamineCommand().execute(w.ctx, List.of("old_man"));
+ assertThat(w.io.lastOutput()).isEqualTo("stooped");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java
new file mode 100644
index 0000000..2cd53a5
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java
@@ -0,0 +1,58 @@
+package thb.jeanluc.adventure.game;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.command.CommandParser;
+import thb.jeanluc.adventure.command.CommandRegistry;
+import thb.jeanluc.adventure.command.impl.GoCommand;
+import thb.jeanluc.adventure.command.impl.LookCommand;
+import thb.jeanluc.adventure.command.impl.QuitCommand;
+import thb.jeanluc.adventure.io.TestIO;
+import thb.jeanluc.adventure.model.Direction;
+import thb.jeanluc.adventure.model.Player;
+import thb.jeanluc.adventure.model.Room;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GameTest {
+
+ @Test
+ void run_dispatchesCommands_andQuitsCleanly() {
+ Room kitchen = new Room("kitchen", "Kitchen", "kd");
+ Room hallway = new Room("hallway", "Hallway", "hd");
+ kitchen.addExit(Direction.NORTH, hallway);
+ Player player = new Player(kitchen, 0);
+ TestIO io = new TestIO()
+ .enqueue("go north")
+ .enqueue("quit");
+ GameContext ctx = new GameContext(null, player, io);
+
+ CommandRegistry registry = new CommandRegistry();
+ registry.register(new GoCommand(), "go");
+ registry.register(new LookCommand(), "look");
+ QuitCommand quit = new QuitCommand();
+ registry.register(quit, "quit");
+
+ Game game = new Game(ctx, registry, new CommandParser());
+ quit.bind(game);
+ game.run();
+
+ assertThat(player.getCurrentRoom()).isEqualTo(hallway);
+ assertThat(io.allOutput()).contains("Goodbye");
+ }
+
+ @Test
+ void run_unknownVerb_writesHint() {
+ Player player = new Player(new Room("r", "R", "d"), 0);
+ TestIO io = new TestIO().enqueue("dance").enqueue("quit");
+ GameContext ctx = new GameContext(null, player, io);
+
+ CommandRegistry registry = new CommandRegistry();
+ QuitCommand quit = new QuitCommand();
+ registry.register(quit, "quit");
+ Game game = new Game(ctx, registry, new CommandParser());
+ quit.bind(game);
+ game.run();
+
+ assertThat(io.allOutput()).contains("don't understand 'dance'");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/TestIO.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/TestIO.java
new file mode 100644
index 0000000..7a8bacd
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/TestIO.java
@@ -0,0 +1,42 @@
+package thb.jeanluc.adventure.io;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Simple in-memory {@link GameIO} for tests: queued inputs, captured outputs.
+ */
+public class TestIO implements GameIO {
+
+ private final Deque inputs = new ArrayDeque<>();
+ private final List outputs = new ArrayList<>();
+
+ public TestIO enqueue(String line) {
+ inputs.add(line);
+ return this;
+ }
+
+ public List outputs() {
+ return outputs;
+ }
+
+ public String lastOutput() {
+ return outputs.isEmpty() ? null : outputs.getLast();
+ }
+
+ public String allOutput() {
+ return String.join("\n", outputs);
+ }
+
+ @Override
+ public String readLine() {
+ return inputs.pollFirst();
+ }
+
+ @Override
+ public void write(String s) {
+ outputs.add(s);
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/ReferenceResolverTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/ReferenceResolverTest.java
new file mode 100644
index 0000000..21a7163
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/ReferenceResolverTest.java
@@ -0,0 +1,120 @@
+package thb.jeanluc.adventure.loader;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.loader.dto.NpcDto;
+import thb.jeanluc.adventure.loader.dto.ReactionDto;
+import thb.jeanluc.adventure.loader.dto.RoomDto;
+import thb.jeanluc.adventure.model.Direction;
+import thb.jeanluc.adventure.model.Npc;
+import thb.jeanluc.adventure.model.Room;
+import thb.jeanluc.adventure.model.item.Item;
+import thb.jeanluc.adventure.model.item.PlainItem;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class ReferenceResolverTest {
+
+ private static Map oneItem(String id) {
+ Map m = new HashMap<>();
+ m.put(id, PlainItem.builder().id(id).name(id).description("d").build());
+ return m;
+ }
+
+ @Test
+ void resolveRooms_wiresExitsAndItems() {
+ Map rooms = new HashMap<>();
+ rooms.put("a", new Room("a", "A", "d"));
+ rooms.put("b", new Room("b", "B", "d"));
+ Map items = oneItem("key");
+
+ new ReferenceResolver(items, new HashMap<>(), rooms).resolveRooms(List.of(
+ new RoomDto("a", "A", "d", Map.of("north", "b"), List.of("key"), List.of()),
+ new RoomDto("b", "B", "d", Map.of("south", "a"), List.of(), List.of())
+ ));
+
+ assertThat(rooms.get("a").getExit(Direction.NORTH)).contains(rooms.get("b"));
+ assertThat(rooms.get("a").findItem("key")).isPresent();
+ }
+
+ @Test
+ void resolveRooms_unknownExitTarget_throws() {
+ Map rooms = new HashMap<>();
+ rooms.put("a", new Room("a", "A", "d"));
+
+ assertThatThrownBy(() ->
+ new ReferenceResolver(Map.of(), Map.of(), rooms).resolveRooms(List.of(
+ new RoomDto("a", "A", "d", Map.of("north", "ghost"), List.of(), List.of())
+ )))
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("ghost");
+ }
+
+ @Test
+ void resolveRooms_unknownDirection_throws() {
+ Map rooms = new HashMap<>();
+ rooms.put("a", new Room("a", "A", "d"));
+
+ assertThatThrownBy(() ->
+ new ReferenceResolver(Map.of(), Map.of(), rooms).resolveRooms(List.of(
+ new RoomDto("a", "A", "d", Map.of("up", "a"), List.of(), List.of())
+ )))
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("up");
+ }
+
+ @Test
+ void resolveNpcs_wiresReactionItems() {
+ Map items = oneItem("lamp");
+ items.put("key", PlainItem.builder().id("key").name("Key").description("d").build());
+ Map npcs = new HashMap<>();
+ npcs.put("man", Npc.shell("man", "Man", "d", "hi"));
+
+ new ReferenceResolver(items, npcs, Map.of()).resolveNpcs(List.of(
+ new NpcDto("man", "Man", "d", "hi", List.of(
+ new ReactionDto("lamp", "thx", "key", "lamp")
+ ))
+ ));
+
+ var reaction = npcs.get("man").reactionFor("lamp").orElseThrow();
+ assertThat(reaction.getGives()).isEqualTo(items.get("key"));
+ assertThat(reaction.getConsumes()).isEqualTo(items.get("lamp"));
+ }
+
+ @Test
+ void resolveNpcs_duplicateTrigger_throws() {
+ Map items = oneItem("lamp");
+ Map npcs = new HashMap<>();
+ npcs.put("man", Npc.shell("man", "Man", "d", "hi"));
+
+ assertThatThrownBy(() ->
+ new ReferenceResolver(items, npcs, Map.of()).resolveNpcs(List.of(
+ new NpcDto("man", "Man", "d", "hi", List.of(
+ new ReactionDto("lamp", "a", null, null),
+ new ReactionDto("lamp", "b", null, null)
+ ))
+ )))
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("duplicate");
+ }
+
+ @Test
+ void resolveNpcs_unknownGivesItem_throws() {
+ Map items = oneItem("lamp");
+ Map npcs = new HashMap<>();
+ npcs.put("man", Npc.shell("man", "Man", "d", "hi"));
+
+ assertThatThrownBy(() ->
+ new ReferenceResolver(items, npcs, Map.of()).resolveNpcs(List.of(
+ new NpcDto("man", "Man", "d", "hi", List.of(
+ new ReactionDto("lamp", "a", "ghost", null)
+ ))
+ )))
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("ghost");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/WorldLoaderTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/WorldLoaderTest.java
new file mode 100644
index 0000000..dc7055d
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/WorldLoaderTest.java
@@ -0,0 +1,44 @@
+package thb.jeanluc.adventure.loader;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.model.Direction;
+import thb.jeanluc.adventure.model.NpcReaction;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class WorldLoaderTest {
+
+ @Test
+ void load_happyPath_buildsWorldFromTestFixtures() {
+ WorldLoader.LoadResult result = new WorldLoader().load();
+
+ assertThat(result.world().getTitle()).isEqualTo("Test Manor");
+ assertThat(result.world().getRooms().keySet()).containsExactlyInAnyOrder("kitchen", "hallway");
+ assertThat(result.world().getItems().keySet()).containsExactlyInAnyOrder("letter", "lamp", "key");
+ assertThat(result.world().getNpcs().keySet()).containsExactly("old_man");
+
+ assertThat(result.player().getCurrentRoom().getId()).isEqualTo("kitchen");
+ assertThat(result.player().getGold()).isEqualTo(5);
+ }
+
+ @Test
+ void load_resolvesExitsBidirectionally_whenYamlDeclaresThem() {
+ var loaded = new WorldLoader().load().world();
+ var kitchen = loaded.getRooms().get("kitchen");
+ var hallway = loaded.getRooms().get("hallway");
+
+ assertThat(kitchen.getExit(Direction.NORTH)).contains(hallway);
+ assertThat(hallway.getExit(Direction.SOUTH)).contains(kitchen);
+ }
+
+ @Test
+ void load_resolvesNpcReactionsToItemReferences() {
+ var loaded = new WorldLoader().load().world();
+ var oldMan = loaded.getNpcs().get("old_man");
+
+ assertThat(oldMan.getReactions()).containsKey("lamp");
+ NpcReaction r = oldMan.reactionFor("lamp").orElseThrow();
+ assertThat(r.getConsumes()).isEqualTo(loaded.getItems().get("lamp"));
+ assertThat(r.getGives()).isEqualTo(loaded.getItems().get("key"));
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/WorldValidatorTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/WorldValidatorTest.java
new file mode 100644
index 0000000..c7c4b33
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/WorldValidatorTest.java
@@ -0,0 +1,81 @@
+package thb.jeanluc.adventure.loader;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.loader.dto.GameDto;
+import thb.jeanluc.adventure.model.Npc;
+import thb.jeanluc.adventure.model.Room;
+import thb.jeanluc.adventure.model.item.Item;
+import thb.jeanluc.adventure.model.item.PlainItem;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class WorldValidatorTest {
+
+ private Map items() {
+ Map m = new HashMap<>();
+ m.put("lamp", PlainItem.builder().id("lamp").name("Lamp").description("d").build());
+ return m;
+ }
+
+ private Map rooms() {
+ Map m = new HashMap<>();
+ m.put("kitchen", new Room("kitchen", "K", "d"));
+ return m;
+ }
+
+ private Map npcs(String greeting) {
+ Map m = new HashMap<>();
+ m.put("man", Npc.shell("man", "Man", "d", greeting));
+ return m;
+ }
+
+ @Test
+ void happyPath_doesNotThrow() {
+ var v = new WorldValidator(items(), npcs("hi"), rooms(),
+ new GameDto("t", "1", "kitchen", 0, "w"));
+ assertThat(v).isNotNull();
+ v.validate();
+ }
+
+ @Test
+ void invalidId_throws() {
+ Map r = new HashMap<>();
+ r.put("Kitchen-1", new Room("Kitchen-1", "K", "d"));
+ var v = new WorldValidator(items(), npcs("hi"), r,
+ new GameDto("t", "1", "kitchen", 0, "w"));
+ assertThatThrownBy(v::validate)
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("Invalid id");
+ }
+
+ @Test
+ void unknownStartRoom_throws() {
+ var v = new WorldValidator(items(), npcs("hi"), rooms(),
+ new GameDto("t", "1", "ghost_room", 0, "w"));
+ assertThatThrownBy(v::validate)
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("ghost_room");
+ }
+
+ @Test
+ void emptyGreeting_throws() {
+ var v = new WorldValidator(items(), npcs(""), rooms(),
+ new GameDto("t", "1", "kitchen", 0, "w"));
+ assertThatThrownBy(v::validate)
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("greeting");
+ }
+
+ @Test
+ void missingStartRoomField_throws() {
+ var v = new WorldValidator(items(), npcs("hi"), rooms(),
+ new GameDto("t", "1", null, 0, "w"));
+ assertThatThrownBy(v::validate)
+ .isInstanceOf(WorldLoadException.class)
+ .hasMessageContaining("startRoom");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/DirectionTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/DirectionTest.java
new file mode 100644
index 0000000..b342f7a
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/DirectionTest.java
@@ -0,0 +1,41 @@
+package thb.jeanluc.adventure.model;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class DirectionTest {
+
+ @Test
+ void getOpposite_pairsAreSymmetric() {
+ for (Direction d : Direction.values()) {
+ assertThat(d.getOpposite().getOpposite()).isEqualTo(d);
+ }
+ }
+
+ @Test
+ void fromString_caseInsensitive_returnsEnum() {
+ assertThat(Direction.fromString("north")).isEqualTo(Direction.NORTH);
+ assertThat(Direction.fromString("EAST")).isEqualTo(Direction.EAST);
+ assertThat(Direction.fromString(" west ")).isEqualTo(Direction.WEST);
+ }
+
+ @Test
+ void fromString_unknown_throws() {
+ assertThatThrownBy(() -> Direction.fromString("up"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("up");
+ }
+
+ @Test
+ void fromString_null_throws() {
+ assertThatThrownBy(() -> Direction.fromString(null))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void getLabel_isLowercase() {
+ assertThat(Direction.NORTH.getLabel()).isEqualTo("north");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/NpcTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/NpcTest.java
new file mode 100644
index 0000000..7fd5d69
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/NpcTest.java
@@ -0,0 +1,22 @@
+package thb.jeanluc.adventure.model;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.model.item.Item;
+import thb.jeanluc.adventure.model.item.PlainItem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class NpcTest {
+
+ @Test
+ void shell_isMutableForReactions() {
+ Npc man = Npc.shell("man", "Man", "d", "hi");
+ Item lamp = PlainItem.builder().id("lamp").name("Lamp").description("d").build();
+
+ man.putReaction("lamp", NpcReaction.builder()
+ .consumes(lamp).response("thanks").build());
+
+ assertThat(man.reactionFor("lamp")).isPresent();
+ assertThat(man.reactionFor("nope")).isEmpty();
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/PlayerTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/PlayerTest.java
new file mode 100644
index 0000000..0b4fc6c
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/PlayerTest.java
@@ -0,0 +1,57 @@
+package thb.jeanluc.adventure.model;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.model.item.Item;
+import thb.jeanluc.adventure.model.item.PlainItem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class PlayerTest {
+
+ @Test
+ void constructor_nullRoom_throws() {
+ assertThatThrownBy(() -> new Player(null, 0))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void constructor_negativeGold_throws() {
+ Room r = new Room("r", "R", "d");
+ assertThatThrownBy(() -> new Player(r, -1))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void addItem_andRemoveItem_areConsistent() {
+ Player p = new Player(new Room("r", "R", "d"), 0);
+ Item it = PlainItem.builder().id("key").name("Key").description("d").build();
+
+ p.addItem(it);
+ assertThat(p.hasItem("key")).isTrue();
+
+ assertThat(p.removeItem("key")).contains(it);
+ assertThat(p.hasItem("key")).isFalse();
+ }
+
+ @Test
+ void inventory_preservesInsertionOrder() {
+ Player p = new Player(new Room("r", "R", "d"), 0);
+ p.addItem(PlainItem.builder().id("a").name("A").description("d").build());
+ p.addItem(PlainItem.builder().id("b").name("B").description("d").build());
+ p.addItem(PlainItem.builder().id("c").name("C").description("d").build());
+
+ assertThat(p.getInventory().keySet()).containsExactly("a", "b", "c");
+ }
+
+ @Test
+ void setCurrentRoom_updatesPosition() {
+ Room a = new Room("a", "A", "d");
+ Room b = new Room("b", "B", "d");
+ Player p = new Player(a, 0);
+
+ p.setCurrentRoom(b);
+
+ assertThat(p.getCurrentRoom()).isEqualTo(b);
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/RoomTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/RoomTest.java
new file mode 100644
index 0000000..c29267c
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/RoomTest.java
@@ -0,0 +1,69 @@
+package thb.jeanluc.adventure.model;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.model.item.Item;
+import thb.jeanluc.adventure.model.item.PlainItem;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RoomTest {
+
+ @Test
+ void addExit_withNewDirection_storesNeighbour() {
+ Room a = new Room("a", "A", "first");
+ Room b = new Room("b", "B", "second");
+
+ a.addExit(Direction.NORTH, b);
+
+ assertThat(a.getExit(Direction.NORTH)).contains(b);
+ assertThat(a.getExit(Direction.SOUTH)).isEmpty();
+ }
+
+ @Test
+ void addItem_storesUnderItemId() {
+ Room room = new Room("r", "R", "d");
+ Item lamp = PlainItem.builder().id("lamp").name("Lamp").description("d").build();
+
+ room.addItem(lamp);
+
+ assertThat(room.findItem("lamp")).contains(lamp);
+ }
+
+ @Test
+ void removeItem_returnsAndDetachesItem() {
+ Room room = new Room("r", "R", "d");
+ Item lamp = PlainItem.builder().id("lamp").name("Lamp").description("d").build();
+ room.addItem(lamp);
+
+ var removed = room.removeItem("lamp");
+
+ assertThat(removed).contains(lamp);
+ assertThat(room.findItem("lamp")).isEmpty();
+ }
+
+ @Test
+ void removeItem_unknownId_returnsEmpty() {
+ Room room = new Room("r", "R", "d");
+ assertThat(room.removeItem("nope")).isEmpty();
+ }
+
+ @Test
+ void addNpc_andFindById() {
+ Room room = new Room("r", "R", "d");
+ Npc npc = Npc.shell("man", "Man", "d", "hi");
+
+ room.addNpc(npc);
+
+ assertThat(room.findNpc("man")).contains(npc);
+ }
+
+ @Test
+ void items_preserveInsertionOrder() {
+ Room room = new Room("r", "R", "d");
+ room.addItem(PlainItem.builder().id("a").name("A").description("d").build());
+ room.addItem(PlainItem.builder().id("b").name("B").description("d").build());
+ room.addItem(PlainItem.builder().id("c").name("C").description("d").build());
+
+ assertThat(room.getItems().keySet()).containsExactly("a", "b", "c");
+ }
+}
diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/item/ItemTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/item/ItemTest.java
new file mode 100644
index 0000000..257bdaa
--- /dev/null
+++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/model/item/ItemTest.java
@@ -0,0 +1,66 @@
+package thb.jeanluc.adventure.model.item;
+
+import org.junit.jupiter.api.Test;
+import thb.jeanluc.adventure.game.GameContext;
+import thb.jeanluc.adventure.io.TestIO;
+import thb.jeanluc.adventure.model.Player;
+import thb.jeanluc.adventure.model.Room;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ItemTest {
+
+ private GameContext newCtx(TestIO io) {
+ Player p = new Player(new Room("r", "R", "d"), 0);
+ return new GameContext(null, p, io);
+ }
+
+ @Test
+ void readableItem_use_writesReadText() {
+ TestIO io = new TestIO();
+ ReadableItem letter = ReadableItem.builder()
+ .id("letter").name("Letter").description("d")
+ .readText("hello world").build();
+
+ letter.use(newCtx(io));
+
+ assertThat(io.lastOutput()).isEqualTo("hello world");
+ }
+
+ @Test
+ void switchableItem_use_togglesAndPrintsAppropriateText() {
+ TestIO io = new TestIO();
+ SwitchableItem lamp = SwitchableItem.builder()
+ .id("lamp").name("Lamp").description("d")
+ .state(false).onText("on").offText("off").build();
+
+ lamp.use(newCtx(io));
+ assertThat(lamp.isOn()).isTrue();
+ assertThat(io.outputs()).containsExactly("on");
+
+ lamp.use(newCtx(io));
+ assertThat(lamp.isOn()).isFalse();
+ assertThat(io.outputs()).endsWith("off");
+ }
+
+ @Test
+ void switchableItem_initialStateTrue_startsOn() {
+ SwitchableItem lamp = SwitchableItem.builder()
+ .id("lamp").name("Lamp").description("d")
+ .state(true).onText("on").offText("off").build();
+
+ assertThat(lamp.isOn()).isTrue();
+ }
+
+ @Test
+ void plainItem_use_writesGenericMessage() {
+ TestIO io = new TestIO();
+ PlainItem shovel = PlainItem.builder()
+ .id("shovel").name("Shovel").description("d").build();
+
+ shovel.use(newCtx(io));
+
+ assertThat(io.lastOutput()).contains("Shovel");
+ assertThat(io.lastOutput()).contains("can't use");
+ }
+}
diff --git a/Semesterprojekt/src/test/resources/world/game.yaml b/Semesterprojekt/src/test/resources/world/game.yaml
new file mode 100644
index 0000000..0be3b8a
--- /dev/null
+++ b/Semesterprojekt/src/test/resources/world/game.yaml
@@ -0,0 +1,6 @@
+title: Test Manor
+version: "test"
+startRoom: kitchen
+startGold: 5
+welcomeMessage: |
+ Welcome to the test.
diff --git a/Semesterprojekt/src/test/resources/world/items.yaml b/Semesterprojekt/src/test/resources/world/items.yaml
new file mode 100644
index 0000000..eae04c9
--- /dev/null
+++ b/Semesterprojekt/src/test/resources/world/items.yaml
@@ -0,0 +1,19 @@
+- type: readable
+ id: letter
+ name: Letter
+ description: A note.
+ readText: |
+ hello
+
+- type: switchable
+ id: lamp
+ name: Lamp
+ description: A lamp.
+ initialState: false
+ onText: on
+ offText: off
+
+- type: plain
+ id: key
+ name: Key
+ description: A key.
diff --git a/Semesterprojekt/src/test/resources/world/npcs.yaml b/Semesterprojekt/src/test/resources/world/npcs.yaml
new file mode 100644
index 0000000..c70f171
--- /dev/null
+++ b/Semesterprojekt/src/test/resources/world/npcs.yaml
@@ -0,0 +1,10 @@
+- id: old_man
+ name: Old Man
+ description: stooped
+ greeting: |
+ greetings
+ reactions:
+ - onReceive: lamp
+ response: thanks
+ gives: key
+ consumes: lamp
diff --git a/Semesterprojekt/src/test/resources/world/rooms.yaml b/Semesterprojekt/src/test/resources/world/rooms.yaml
new file mode 100644
index 0000000..559da36
--- /dev/null
+++ b/Semesterprojekt/src/test/resources/world/rooms.yaml
@@ -0,0 +1,15 @@
+- id: kitchen
+ name: Kitchen
+ description: kitchen desc
+ exits:
+ north: hallway
+ items: [letter, lamp]
+ npcs: [old_man]
+
+- id: hallway
+ name: Hallway
+ description: hallway desc
+ exits:
+ south: kitchen
+ items: []
+ npcs: []