feat(game): Quest model, QuestLog, START_QUEST effect

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 22:25:58 +02:00
parent eeee609c26
commit dd16d1326b
8 changed files with 114 additions and 1 deletions

View File

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

View File

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

View File

@@ -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<String, Integer> stageIndex = new LinkedHashMap<>();
private final Set<String> 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<String> active() {
return Collections.unmodifiableSet(stageIndex.keySet());
}
public Set<String> completed() {
return Collections.unmodifiableSet(completed);
}
}

View File

@@ -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 }
}

View File

@@ -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<QuestStage> stages, List<Effect> onComplete) {
public Quest {
stages = stages == null ? List.of() : List.copyOf(stages);
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
}
}

View File

@@ -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<Condition> completion, List<Effect> onComplete) {
public QuestStage {
completion = completion == null ? List.of() : List.copyOf(completion);
onComplete = onComplete == null ? List.of() : List.copyOf(onComplete);
}
}

View File

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

View File

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