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:
2026-05-25 21:37:59 +02:00
parent 9b6528d800
commit 83643a192f
85 changed files with 4453 additions and 0 deletions

View File

@@ -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<String, Item> oneItem(String id) {
Map<String, Item> m = new HashMap<>();
m.put(id, PlainItem.builder().id(id).name(id).description("d").build());
return m;
}
@Test
void resolveRooms_wiresExitsAndItems() {
Map<String, Room> rooms = new HashMap<>();
rooms.put("a", new Room("a", "A", "d"));
rooms.put("b", new Room("b", "B", "d"));
Map<String, Item> 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<String, Room> 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<String, Room> 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<String, Item> items = oneItem("lamp");
items.put("key", PlainItem.builder().id("key").name("Key").description("d").build());
Map<String, Npc> 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<String, Item> items = oneItem("lamp");
Map<String, Npc> 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<String, Item> items = oneItem("lamp");
Map<String, Npc> 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");
}
}

View File

@@ -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"));
}
}

View File

@@ -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<String, Item> items() {
Map<String, Item> m = new HashMap<>();
m.put("lamp", PlainItem.builder().id("lamp").name("Lamp").description("d").build());
return m;
}
private Map<String, Room> rooms() {
Map<String, Room> m = new HashMap<>();
m.put("kitchen", new Room("kitchen", "K", "d"));
return m;
}
private Map<String, Npc> npcs(String greeting) {
Map<String, Npc> 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<String, Room> 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");
}
}