From 3ec540e41baa7c436aeae1d25f57cd6a02c36e58 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 22:47:38 +0200 Subject: [PATCH] feat: Ending model + endings.yaml loading (ordered, optional) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../adventure/loader/EndingFactory.java | 21 ++++++++++++++++++ .../jeanluc/adventure/loader/WorldLoader.java | 11 +++++++++- .../adventure/loader/dto/EndingDto.java | 7 ++++++ .../thb/jeanluc/adventure/model/Ending.java | 10 +++++++++ .../thb/jeanluc/adventure/model/World.java | 14 ++++++++++-- .../adventure/loader/EndingLoadingTest.java | 22 +++++++++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/EndingFactory.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EndingDto.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Ending.java create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/EndingLoadingTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/EndingFactory.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/EndingFactory.java new file mode 100644 index 0000000..9ac368d --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/EndingFactory.java @@ -0,0 +1,21 @@ +package thb.jeanluc.adventure.loader; + +import thb.jeanluc.adventure.loader.dto.ConditionDto; +import thb.jeanluc.adventure.loader.dto.EndingDto; +import thb.jeanluc.adventure.model.Ending; + +/** Builds {@link Ending} objects from {@link EndingDto}s. */ +public final class EndingFactory { + + private EndingFactory() { + } + + public static Ending fromDto(EndingDto dto) { + return new Ending( + dto.id(), + dto.title(), + Boolean.TRUE.equals(dto.victory()), + ConditionDto.toModelList(dto.when()), + dto.text()); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java index b783a63..b5c0eb1 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/WorldLoader.java @@ -5,11 +5,13 @@ import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import thb.jeanluc.adventure.loader.dto.EndingDto; import thb.jeanluc.adventure.loader.dto.GameDto; import thb.jeanluc.adventure.loader.dto.ItemDto; import thb.jeanluc.adventure.loader.dto.NpcDto; import thb.jeanluc.adventure.loader.dto.QuestDto; import thb.jeanluc.adventure.loader.dto.RoomDto; +import thb.jeanluc.adventure.model.Ending; import thb.jeanluc.adventure.model.Npc; import thb.jeanluc.adventure.model.Player; import thb.jeanluc.adventure.model.Quest; @@ -19,6 +21,7 @@ import thb.jeanluc.adventure.model.item.Item; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -71,12 +74,14 @@ public class WorldLoader { List npcDtos = readList(basePath + "/npcs.yaml", NpcDto.class); List roomDtos = readList(basePath + "/rooms.yaml", RoomDto.class); List questDtos = readListOptional(basePath + "/quests.yaml", QuestDto.class); + List endingDtos = readListOptional(basePath + "/endings.yaml", EndingDto.class); GameDto gameDto = readSingle(basePath + "/game.yaml", GameDto.class); requireUniqueIds("item", itemDtos.stream().map(ItemDto::id).toList()); requireUniqueIds("npc", npcDtos.stream().map(NpcDto::id).toList()); requireUniqueIds("room", roomDtos.stream().map(RoomDto::id).toList()); requireUniqueIds("quest", questDtos.stream().map(QuestDto::id).toList()); + requireUniqueIds("ending", endingDtos.stream().map(EndingDto::id).toList()); Map items = new HashMap<>(); for (ItemDto dto : itemDtos) { @@ -94,6 +99,10 @@ public class WorldLoader { for (QuestDto dto : questDtos) { quests.put(dto.id(), QuestFactory.fromDto(dto)); } + List endings = new ArrayList<>(); + for (EndingDto dto : endingDtos) { + endings.add(EndingFactory.fromDto(dto)); + } ReferenceResolver resolver = new ReferenceResolver(items, npcs, rooms); resolver.resolveRooms(roomDtos); @@ -106,7 +115,7 @@ public class WorldLoader { Player player = new Player(start, gold); World world = new World(rooms, items, npcs, - gameDto.title(), gameDto.welcomeMessage(), quests); + gameDto.title(), gameDto.welcomeMessage(), quests, endings); log.info("World '{}' loaded: {} rooms, {} items, {} npcs", gameDto.title(), rooms.size(), items.size(), npcs.size()); return new LoadResult(world, player); diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EndingDto.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EndingDto.java new file mode 100644 index 0000000..0434956 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/dto/EndingDto.java @@ -0,0 +1,7 @@ +package thb.jeanluc.adventure.loader.dto; + +import java.util.List; + +/** YAML representation of a game ending. */ +public record EndingDto(String id, String title, Boolean victory, List when, String text) { +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Ending.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Ending.java new file mode 100644 index 0000000..83458d9 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Ending.java @@ -0,0 +1,10 @@ +package thb.jeanluc.adventure.model; + +import java.util.List; + +/** A game ending: shown (and ends the game) when its conditions hold. */ +public record Ending(String id, String title, boolean victory, List when, String text) { + public Ending { + when = when == null ? List.of() : List.copyOf(when); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java index b478fa8..56dffb1 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/World.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import thb.jeanluc.adventure.model.item.Item; +import java.util.List; import java.util.Map; /** @@ -33,9 +34,18 @@ public class World { /** Global lookup of quests by id. */ private final Map quests; - /** Backward-compatible constructor for worlds without quests. */ + /** Ordered list of endings (first matching one wins). */ + private final List endings; + + /** Backward-compatible constructor for worlds without quests or endings. */ public World(Map rooms, Map items, Map npcs, String title, String welcomeMessage) { - this(rooms, items, npcs, title, welcomeMessage, Map.of()); + this(rooms, items, npcs, title, welcomeMessage, Map.of(), List.of()); + } + + /** Backward-compatible constructor for worlds with quests but no endings. */ + public World(Map rooms, Map items, Map npcs, + String title, String welcomeMessage, Map quests) { + this(rooms, items, npcs, title, welcomeMessage, quests, List.of()); } } diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/EndingLoadingTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/EndingLoadingTest.java new file mode 100644 index 0000000..257d519 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/EndingLoadingTest.java @@ -0,0 +1,22 @@ +package thb.jeanluc.adventure.loader; + +import org.junit.jupiter.api.Test; +import thb.jeanluc.adventure.loader.dto.ConditionDto; +import thb.jeanluc.adventure.loader.dto.EndingDto; +import thb.jeanluc.adventure.model.Ending; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class EndingLoadingTest { + @Test + void factoryMapsFields() { + Ending e = EndingFactory.fromDto(new EndingDto("victory", "Won", true, + List.of(new ConditionDto("manor_secured", null, null)), "You win.")); + assertThat(e.id()).isEqualTo("victory"); + assertThat(e.victory()).isTrue(); + assertThat(e.when()).hasSize(1); + assertThat(e.text()).isEqualTo("You win."); + } +}