From dd16d1326bc6446ee0ec5cfe0518a9c6803406b9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 22:25:58 +0200 Subject: [PATCH] feat(game): Quest model, QuestLog, START_QUEST effect Co-Authored-By: Claude Opus 4.8 (1M context) --- .../thb/jeanluc/adventure/game/Effects.java | 1 + .../jeanluc/adventure/game/GameContext.java | 3 ++ .../thb/jeanluc/adventure/game/QuestLog.java | 49 +++++++++++++++++++ .../thb/jeanluc/adventure/model/Effect.java | 2 +- .../thb/jeanluc/adventure/model/Quest.java | 12 +++++ .../jeanluc/adventure/model/QuestStage.java | 11 +++++ .../jeanluc/adventure/game/EffectsTest.java | 7 +++ .../jeanluc/adventure/game/QuestLogTest.java | 30 ++++++++++++ 8 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/QuestLog.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Quest.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/QuestStage.java create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/QuestLogTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Effects.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Effects.java index 317faa3..4f2d738 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Effects.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Effects.java @@ -27,6 +27,7 @@ public final class Effects { } case REMOVE_ITEM -> ctx.getPlayer().removeItem(e.arg()); case SAY -> ctx.getIo().write(e.arg()); + case START_QUEST -> ctx.getQuestLog().start(e.arg()); } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/GameContext.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/GameContext.java index 48617fe..a552b92 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/GameContext.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/GameContext.java @@ -26,4 +26,7 @@ public class GameContext { /** Mutable world flags. Created fresh per context; not a constructor arg. */ private final GameState state = new GameState(); + + /** Runtime quest progress. Created fresh per context; not a constructor arg. */ + private final QuestLog questLog = new QuestLog(); } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/QuestLog.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/QuestLog.java new file mode 100644 index 0000000..d0d9bb0 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/QuestLog.java @@ -0,0 +1,49 @@ +package thb.jeanluc.adventure.game; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** Runtime quest progress: current stage per active quest, plus completed ids. */ +public class QuestLog { + + private final Map stageIndex = new LinkedHashMap<>(); + private final Set completed = new LinkedHashSet<>(); + + public void start(String id) { + if (!completed.contains(id)) { + stageIndex.putIfAbsent(id, 0); + } + } + + public boolean isActive(String id) { + return stageIndex.containsKey(id); + } + + public boolean isCompleted(String id) { + return completed.contains(id); + } + + public int stageIndex(String id) { + return stageIndex.getOrDefault(id, 0); + } + + public void advance(String id) { + stageIndex.merge(id, 1, Integer::sum); + } + + public void complete(String id) { + stageIndex.remove(id); + completed.add(id); + } + + public Set active() { + return Collections.unmodifiableSet(stageIndex.keySet()); + } + + public Set completed() { + return Collections.unmodifiableSet(completed); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Effect.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Effect.java index c876efa..ae36755 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Effect.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Effect.java @@ -2,5 +2,5 @@ package thb.jeanluc.adventure.model; /** A single state mutation or message. */ public record Effect(Type type, String arg) { - public enum Type { SET_FLAG, CLEAR_FLAG, GIVE_ITEM, REMOVE_ITEM, SAY } + public enum Type { SET_FLAG, CLEAR_FLAG, GIVE_ITEM, REMOVE_ITEM, SAY, START_QUEST } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Quest.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Quest.java new file mode 100644 index 0000000..ff0d58d --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/Quest.java @@ -0,0 +1,12 @@ +package thb.jeanluc.adventure.model; + +import java.util.List; + +/** A multi-stage quest. Stage completion is condition-driven; rewards are effects. */ +public record Quest(String id, String title, boolean autoStart, + List stages, List onComplete) { + public Quest { + stages = stages == null ? List.of() : List.copyOf(stages); + onComplete = onComplete == null ? List.of() : List.copyOf(onComplete); + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/QuestStage.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/QuestStage.java new file mode 100644 index 0000000..87ed93a --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/model/QuestStage.java @@ -0,0 +1,11 @@ +package thb.jeanluc.adventure.model; + +import java.util.List; + +/** One stage of a {@link Quest}: an objective, a completion condition, optional rewards. */ +public record QuestStage(String objective, List completion, List onComplete) { + public QuestStage { + completion = completion == null ? List.of() : List.copyOf(completion); + onComplete = onComplete == null ? List.of() : List.copyOf(onComplete); + } +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/EffectsTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/EffectsTest.java index 44d1d2c..8728491 100644 --- a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/EffectsTest.java +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/EffectsTest.java @@ -53,4 +53,11 @@ class EffectsTest { Effects.apply(new Effect(Effect.Type.SAY, "boo"), ctx); assertThat(((TestIO) ctx.getIo()).allOutput()).contains("boo"); } + + @Test + void startQuestStartsQuestInLog() { + GameContext ctx = ctx(Map.of()); + Effects.apply(new Effect(Effect.Type.START_QUEST, "restore_power"), ctx); + assertThat(ctx.getQuestLog().isActive("restore_power")).isTrue(); + } } diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/QuestLogTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/QuestLogTest.java new file mode 100644 index 0000000..1b65415 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/QuestLogTest.java @@ -0,0 +1,30 @@ +package thb.jeanluc.adventure.game; + +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + +class QuestLogTest { + @Test + void startAdvanceComplete() { + QuestLog log = new QuestLog(); + assertThat(log.isActive("q")).isFalse(); + log.start("q"); + assertThat(log.isActive("q")).isTrue(); + assertThat(log.stageIndex("q")).isEqualTo(0); + log.advance("q"); + assertThat(log.stageIndex("q")).isEqualTo(1); + log.complete("q"); + assertThat(log.isActive("q")).isFalse(); + assertThat(log.isCompleted("q")).isTrue(); + } + + @Test + void startIgnoredOnceCompleted() { + QuestLog log = new QuestLog(); + log.start("q"); + log.complete("q"); + log.start("q"); + assertThat(log.isActive("q")).isFalse(); + assertThat(log.isCompleted("q")).isTrue(); + } +}