# Light & Darkness Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Dark rooms you can't enter, see, or interact with unless you carry a lit light source (the lamp). **Architecture:** A `Room.dark` flag and an `Item.light` flag, plus one `Light` helper (`isLit`/`carryingLight`). Four commands gate on it (`go` entry, `look`, `take`, `examine`); the HUD's `light` field is wired to it. Reuses the switchable lamp and is fully backward-compatible (optional flags). **Tech Stack:** Java 25, Maven, JUnit 5 + AssertJ, Lombok. Spec: [docs/superpowers/specs/2026-05-31-light-darkness-design.md](../specs/2026-05-31-light-darkness-design.md) --- ## Task 1: Data flags + `Light` helper **Files:** - Modify: `model/item/Item.java`, `loader/dto/ItemDto.java`, `loader/ItemFactory.java`, `model/Room.java`, `loader/dto/RoomDto.java`, `loader/RoomFactory.java` - Create: `game/Light.java` - Test: `game/LightTest.java` - [ ] **Step 1: Write failing test** `src/test/java/thb/jeanluc/adventure/game/LightTest.java`: ```java package thb.jeanluc.adventure.game; import org.junit.jupiter.api.Test; import thb.jeanluc.adventure.io.TestIO; import thb.jeanluc.adventure.model.Player; import thb.jeanluc.adventure.model.Room; import thb.jeanluc.adventure.model.item.PlainItem; import thb.jeanluc.adventure.model.item.SwitchableItem; import static org.assertj.core.api.Assertions.assertThat; class LightTest { private GameContext ctx(Player p) { return new GameContext(null, p, new TestIO()); } private SwitchableItem lamp(boolean on) { return SwitchableItem.builder().id("lamp").name("Lamp").description("d") .state(on).onText("on").offText("off").light(true).build(); } @Test void litRoomIsAlwaysLit() { Room bright = new Room("r", "R", "d"); // dark defaults to false assertThat(Light.isLit(ctx(new Player(bright, 0)), bright)).isTrue(); } @Test void darkRoomNeedsActiveLight() { Room dark = new Room("r", "R", "d"); dark.setDark(true); Player p = new Player(dark, 0); GameContext ctx = ctx(p); assertThat(Light.isLit(ctx, dark)).isFalse(); p.addItem(lamp(false)); assertThat(Light.isLit(ctx, dark)).isFalse(); // lamp off p.removeItem("lamp"); p.addItem(lamp(true)); assertThat(Light.isLit(ctx, dark)).isTrue(); // lamp on, carried assertThat(Light.carryingLight(ctx)).isTrue(); } @Test void litLampLyingInRoomLightsIt() { Room dark = new Room("r", "R", "d"); dark.setDark(true); dark.addItem(lamp(true)); Player p = new Player(dark, 0); assertThat(Light.isLit(ctx(p), dark)).isTrue(); assertThat(Light.carryingLight(ctx(p))).isFalse(); // not carried } @Test void nonLightItemDoesNotLight() { Room dark = new Room("r", "R", "d"); dark.setDark(true); Player p = new Player(dark, 0); p.addItem(PlainItem.builder().id("rock").name("Rock").description("d").build()); assertThat(Light.isLit(ctx(p), dark)).isFalse(); } } ``` - [ ] **Step 2: Run to verify failure** Run: `mvn -q test -Dtest=LightTest` Expected: FAIL — `Item.light` / `Room.setDark` / `Light` missing. - [ ] **Step 3: Add `light` to `Item.java`** Add the field after `description`: ```java /** Whether this item can serve as a light source (when on, for switchables). */ protected final boolean light; ``` (`@SuperBuilder` exposes `.light(...)` on every subclass builder; `@Getter` generates `isLight()`. Unset builders default to `false`.) - [ ] **Step 4: Add `light` to `ItemDto.java` and wire `ItemFactory.java`** In `ItemDto.java` append a component (after `effects`) and update the back-compat constructor: ```java List effects, Boolean light ``` ```java /** Backward-compatible constructor without effects/light. */ public ItemDto(String type, String id, String name, String description, String readText, Boolean initialState, String onText, String offText) { this(type, id, name, description, readText, initialState, onText, offText, null, null); } ``` In `ItemFactory.java`, add `.light(Boolean.TRUE.equals(dto.light()))` to **each** of the three builder cases (plain, readable, switchable), e.g.: ```java case "plain" -> PlainItem.builder() .id(dto.id()) .name(dto.name()) .description(dto.description()) .light(Boolean.TRUE.equals(dto.light())) .build(); ``` (and the same `.light(...)` line before `.build()` in the `readable` and `switchable` cases.) - [ ] **Step 5: Add `dark` to `Room.java`** Add the import if missing: ```java import lombok.Setter; ``` Add the field after `descriptionStates`: ```java /** Whether this room is dark (needs a light source to enter/see). */ @Setter private boolean dark; ``` (`@Getter` generates `isDark()`; not a constructor arg, so existing `new Room(id,name,desc)` calls are unaffected.) - [ ] **Step 6: Add `dark` to `RoomDto.java` and wire `RoomFactory.java`** In `RoomDto.java` append a component (after `descriptionStates`) and update the back-compat constructor: ```java List descriptionStates, Boolean dark ``` ```java /** Backward-compatible constructor without the optional state fields. */ public RoomDto(String id, String name, String description, Map exits, List items, List npcs) { this(id, name, description, exits, items, npcs, null, null, null); } ``` In `RoomFactory.java`, set `dark` after building the shell: ```java public static Room shellFromDto(RoomDto dto) { Room room = new Room(dto.id(), dto.name(), dto.description()); room.setDark(Boolean.TRUE.equals(dto.dark())); return room; } ``` - [ ] **Step 7: Create `game/Light.java`** ```java package thb.jeanluc.adventure.game; import thb.jeanluc.adventure.model.Room; import thb.jeanluc.adventure.model.item.Item; import thb.jeanluc.adventure.model.item.SwitchableItem; import java.util.Collection; /** Decides whether a room is lit, given dark rooms and active light sources. */ public final class Light { private Light() { } /** A room is lit if it is not dark, or an active light source is carried or present. */ public static boolean isLit(GameContext ctx, Room room) { if (!room.isDark()) { return true; } return hasActiveLight(ctx.getPlayer().getInventory().values()) || hasActiveLight(room.getItems().values()); } /** True if the player carries an active light source (for the HUD). */ public static boolean carryingLight(GameContext ctx) { return hasActiveLight(ctx.getPlayer().getInventory().values()); } private static boolean hasActiveLight(Collection items) { for (Item it : items) { if (isActiveLight(it)) { return true; } } return false; } private static boolean isActiveLight(Item it) { if (!it.isLight()) { return false; } return !(it instanceof SwitchableItem s) || s.isOn(); } } ``` - [ ] **Step 8: Run tests** Run: `mvn -q test -Dtest=LightTest` Expected: PASS. Run: `mvn -q test` Expected: PASS — flags default off; existing item/room construction unchanged via back-compat constructors. - [ ] **Step 9: Commit** ```bash git add src/main/java/thb/jeanluc/adventure/model/item/Item.java \ src/main/java/thb/jeanluc/adventure/loader/dto/ItemDto.java \ src/main/java/thb/jeanluc/adventure/loader/ItemFactory.java \ src/main/java/thb/jeanluc/adventure/model/Room.java \ src/main/java/thb/jeanluc/adventure/loader/dto/RoomDto.java \ src/main/java/thb/jeanluc/adventure/loader/RoomFactory.java \ src/main/java/thb/jeanluc/adventure/game/Light.java \ src/test/java/thb/jeanluc/adventure/game/LightTest.java git commit -m "feat: Room.dark + Item.light flags and Light helper" ``` --- ## Task 2: Command gating + HUD **Files:** - Modify: `command/impl/GoCommand.java`, `command/impl/LookCommand.java`, `command/impl/TakeCommand.java`, `command/impl/ExamineCommand.java`, `game/Game.java` - Test: `command/impl/DarknessTest.java` - [ ] **Step 1: Write failing test** `src/test/java/thb/jeanluc/adventure/command/impl/DarknessTest.java`: ```java package thb.jeanluc.adventure.command.impl; import org.junit.jupiter.api.Test; import thb.jeanluc.adventure.game.GameContext; 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 thb.jeanluc.adventure.model.item.SwitchableItem; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class DarknessTest { private SwitchableItem lamp(boolean on) { return SwitchableItem.builder().id("lamp").name("Lamp").description("d") .state(on).onText("on").offText("off").light(true).build(); } @Test void cannotEnterDarkRoomWithoutLight() { Room hall = new Room("hall", "Hall", "d"); Room cave = new Room("cave", "Cave", "d"); cave.setDark(true); hall.addExit(Direction.NORTH, cave); Player p = new Player(hall, 0); GameContext ctx = new GameContext(null, p, new TestIO()); new GoCommand().execute(ctx, List.of("north")); assertThat(p.getCurrentRoom()).isEqualTo(hall); assertThat(((TestIO) ctx.getIo()).allOutput()).containsIgnoringCase("pitch black"); } @Test void canEnterDarkRoomWithLitLamp() { Room hall = new Room("hall", "Hall", "d"); Room cave = new Room("cave", "Cave", "d"); cave.setDark(true); hall.addExit(Direction.NORTH, cave); Player p = new Player(hall, 0); p.addItem(lamp(true)); GameContext ctx = new GameContext(null, p, new TestIO()); new GoCommand().execute(ctx, List.of("north")); assertThat(p.getCurrentRoom()).isEqualTo(cave); } @Test void lookAndTakeAreBlockedInTheDark() { Room cave = new Room("cave", "Cave", "d"); cave.setDark(true); Player p = new Player(cave, 0); TestIO io = new TestIO(); GameContext ctx = new GameContext(null, p, io); new LookCommand().execute(ctx, List.of()); assertThat(io.allOutput()).containsIgnoringCase("pitch black"); io.outputs().clear(); new TakeCommand().execute(ctx, List.of("anything")); assertThat(io.allOutput()).containsIgnoringCase("too dark"); } } ``` - [ ] **Step 2: Run to verify failure** Run: `mvn -q test -Dtest=DarknessTest` Expected: FAIL — no darkness gating yet (entering succeeds, look shows room). - [ ] **Step 3: Gate `GoCommand`** Add the import: ```java import thb.jeanluc.adventure.game.Light; ``` After the exit-lock check and before `ctx.getPlayer().setCurrentRoom(next.get())`, insert: ```java if (!Light.isLit(ctx, next.get())) { ctx.getIo().write("It's pitch black beyond the doorway — you need a lit light source " + "(try lighting your lamp with 'use lamp')."); return; } ``` - [ ] **Step 4: Gate `LookCommand`** Add the import: ```java import thb.jeanluc.adventure.game.Light; ``` At the very start of `execute`, after fetching the room: ```java Room room = ctx.getPlayer().getCurrentRoom(); if (!Light.isLit(ctx, room)) { ctx.getIo().write("It's pitch black; you can't make anything out. You need a light source."); return; } ``` (The existing `Room room = ...` line is already there — replace it with this guarded version, keeping the rest below unchanged.) - [ ] **Step 5: Gate `TakeCommand`** Add the import: ```java import thb.jeanluc.adventure.game.Light; ``` After the `args.isEmpty()` check: ```java if (!Light.isLit(ctx, ctx.getPlayer().getCurrentRoom())) { ctx.getIo().write("It's too dark to see that."); return; } ``` - [ ] **Step 6: Gate `ExamineCommand`** Add the import: ```java import thb.jeanluc.adventure.game.Light; ``` After the `args.isEmpty()` check: ```java if (!Light.isLit(ctx, ctx.getPlayer().getCurrentRoom())) { ctx.getIo().write("It's too dark to see that."); return; } ``` - [ ] **Step 7: Wire the HUD `light` field in `Game.java`** Add the import: ```java import thb.jeanluc.adventure.game.Light; // same package 'game' — drop if redundant ``` (`Light` is in package `game`, same as `Game` — no import needed.) In `publishHud()`, change the `Hud` construction's last argument from `false` to `Light.carryingLight(ctx)`: ```java ctx.getIo().setHud(new Hud( ctx.getPlayer().getCurrentRoom().getName(), ctx.getPlayer().getGold(), turn, Light.carryingLight(ctx))); ``` - [ ] **Step 8: Run tests** Run: `mvn -q test -Dtest=DarknessTest` Expected: PASS. Run: `mvn -q test` Expected: PASS (existing command tests use non-dark rooms → `isLit` true → no behaviour change). - [ ] **Step 9: Commit** ```bash git add src/main/java/thb/jeanluc/adventure/command/impl/GoCommand.java \ src/main/java/thb/jeanluc/adventure/command/impl/LookCommand.java \ src/main/java/thb/jeanluc/adventure/command/impl/TakeCommand.java \ src/main/java/thb/jeanluc/adventure/command/impl/ExamineCommand.java \ src/main/java/thb/jeanluc/adventure/game/Game.java \ src/test/java/thb/jeanluc/adventure/command/impl/DarknessTest.java git commit -m "feat: darkness gating for go/look/take/examine + HUD light" ``` --- ## Task 3: Demo (dark dungeon) + docs + verification **Files:** - Modify: `src/main/resources/world/items.yaml`, `rooms.yaml`, `docs/enhancement-ideas.md` - [ ] **Step 1: Make the lamp a light source in `items.yaml`** On the `lamp` item, add `light: true`: ```yaml - type: switchable id: lamp name: Oil Lamp description: An old oil lamp, heavy with fuel. initialState: false light: true onText: The lamp flares to life, casting a warm glow. offText: You snuff out the lamp. ``` - [ ] **Step 2: Make the dungeon dark in `rooms.yaml`** On the `dungeon` room, add `dark: true` and tweak the description: ```yaml - id: dungeon name: Dungeon description: | A cramped stone room, black as pitch. A rusty generator squats in the corner. dark: true exits: north: kitchen items: [generator] npcs: [] ``` - [ ] **Step 3: Mark the mechanic done in `docs/enhancement-ideas.md`** Under the mechanic-spine section (point 3, "Licht & Dunkelheit"), append a status note: ```markdown > ✅ Umgesetzt (Branch `feature/light-darkness`): `Room.dark` + `Item.light`, > `Light`-Helper, Gating in go/look/take/examine, HUD-Licht. Demo: dunkler > Dungeon braucht die brennende Lampe für den Generator. ``` - [ ] **Step 4: Full suite + end-to-end console** Run: `mvn -q test` Expected: PASS. Run: ```bash printf 'go south\ntake lamp\nuse lamp\ngo south\nuse generator\nquit\n' | mvn -q -DskipTests exec:java@run ``` Expected: the first `go south` is blocked ("pitch black beyond the doorway"); after `take lamp` and `use lamp` (HUD flips to `light: on`), `go south` enters the dungeon and `use generator` restores the power. No exceptions. - [ ] **Step 5: Commit** ```bash git add src/main/resources/world/items.yaml \ src/main/resources/world/rooms.yaml \ docs/enhancement-ideas.md git commit -m "feat(content): dark dungeon needs the lit lamp" ``` --- ## Self-Review notes - **Spec coverage:** flags + `Light` (T1), command gating + HUD (T2), demo + docs (T3). Fuel/timed light, cross-room radius deferred. - **Backward compatibility:** `Item.light` and `Room.dark` default off; `ItemDto`/`RoomDto` keep back-compat constructors; `Room.dark` is not a constructor arg. Existing tests and YAML unaffected (non-dark rooms are always `isLit`). - **Type consistency:** `Light.isLit(ctx, Room)` / `carryingLight(ctx)`, `Item.isLight()`, `Room.isDark()/setDark()`, `ItemDto.light`, `RoomDto.dark` used consistently. - **Soft-lock safety:** entry-gate model means dark rooms are entered only with light; lit rooms always re-enterable.