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,38 @@
package thb.jeanluc.adventure.command;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CommandParserTest {
private final CommandParser parser = new CommandParser();
@Test
void parse_blankInput_yieldsEmptyVerb() {
assertThat(parser.parse("").verb()).isEmpty();
assertThat(parser.parse(" ").verb()).isEmpty();
assertThat(parser.parse(null).verb()).isEmpty();
}
@Test
void parse_lowercases() {
ParsedCommand p = parser.parse("Go NORTH");
assertThat(p.verb()).isEqualTo("go");
assertThat(p.args()).containsExactly("north");
}
@Test
void parse_dropsFillers() {
ParsedCommand p = parser.parse("go to the north");
assertThat(p.verb()).isEqualTo("go");
assertThat(p.args()).containsExactly("north");
}
@Test
void parse_giveItemToNpc_keepsBoth() {
ParsedCommand p = parser.parse("give lamp to old_man");
assertThat(p.verb()).isEqualTo("give");
assertThat(p.args()).containsExactly("lamp", "old_man");
}
}

View File

@@ -0,0 +1,43 @@
package thb.jeanluc.adventure.command;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.game.GameContext;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class CommandRegistryTest {
private static class StubCommand implements Command {
@Override public void execute(GameContext ctx, List<String> args) {}
@Override public String help() { return "stub"; }
}
@Test
void register_andFindByEachAlias() {
CommandRegistry r = new CommandRegistry();
StubCommand cmd = new StubCommand();
r.register(cmd, "go", "move", "walk");
assertThat(r.find("go")).contains(cmd);
assertThat(r.find("MOVE")).contains(cmd);
assertThat(r.find("walk")).contains(cmd);
}
@Test
void find_unknownVerb_returnsEmpty() {
assertThat(new CommandRegistry().find("nope")).isEmpty();
}
@Test
void distinctCommands_deduplicatesAliases() {
CommandRegistry r = new CommandRegistry();
StubCommand a = new StubCommand();
StubCommand b = new StubCommand();
r.register(a, "x", "y");
r.register(b, "z");
assertThat(r.distinctCommands()).containsExactlyInAnyOrder(a, b);
}
}

View File

@@ -0,0 +1,60 @@
package thb.jeanluc.adventure.command.impl;
import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Direction;
import thb.jeanluc.adventure.model.Npc;
import thb.jeanluc.adventure.model.NpcReaction;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.PlainItem;
import thb.jeanluc.adventure.model.item.ReadableItem;
import thb.jeanluc.adventure.model.item.SwitchableItem;
/**
* Tiny world used by the command tests. Two rooms (kitchen → hallway),
* a few items, and one NPC with one reaction.
*/
public final class CommandTestSupport {
private CommandTestSupport() {}
public static class World {
public final Room kitchen;
public final Room hallway;
public final Item lamp;
public final Item letter;
public final Item key;
public final Item shovel;
public final Npc oldMan;
public final Player player;
public final TestIO io = new TestIO();
public final GameContext ctx;
public World() {
kitchen = new Room("kitchen", "Kitchen", "kitchen desc");
hallway = new Room("hallway", "Hallway", "hallway desc");
kitchen.addExit(Direction.NORTH, hallway);
hallway.addExit(Direction.SOUTH, kitchen);
lamp = SwitchableItem.builder().id("lamp").name("Lamp").description("a lamp")
.state(false).onText("on").offText("off").build();
letter = ReadableItem.builder().id("letter").name("Letter").description("a letter")
.readText("dear reader").build();
key = PlainItem.builder().id("key").name("Key").description("brass key").build();
shovel = PlainItem.builder().id("shovel").name("Shovel").description("rusty").build();
kitchen.addItem(lamp);
kitchen.addItem(letter);
oldMan = Npc.shell("old_man", "Old Man", "stooped", "greetings");
oldMan.putReaction("lamp", NpcReaction.builder()
.consumes(lamp).gives(key).response("Take the key.").build());
kitchen.addNpc(oldMan);
player = new Player(kitchen, 0);
ctx = new GameContext(null, player, io);
}
}
}

View File

@@ -0,0 +1,48 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class GoCommandTest {
@Test
void execute_validDirection_movesPlayer() {
CommandTestSupport.World w = new CommandTestSupport.World();
new GoCommand().execute(w.ctx, List.of("north"));
assertThat(w.player.getCurrentRoom()).isEqualTo(w.hallway);
}
@Test
void execute_noExit_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new GoCommand().execute(w.ctx, List.of("south"));
assertThat(w.player.getCurrentRoom()).isEqualTo(w.kitchen);
assertThat(w.io.allOutput()).contains("can't go south");
}
@Test
void execute_unknownDirection_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new GoCommand().execute(w.ctx, List.of("upwards"));
assertThat(w.player.getCurrentRoom()).isEqualTo(w.kitchen);
assertThat(w.io.allOutput()).contains("upwards");
}
@Test
void execute_noArg_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new GoCommand().execute(w.ctx, List.of());
assertThat(w.io.allOutput()).contains("Go where");
}
}

View File

@@ -0,0 +1,28 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class InventoryCommandTest {
@Test
void empty_writesEmptyMessage() {
CommandTestSupport.World w = new CommandTestSupport.World();
new InventoryCommand().execute(w.ctx, List.of());
assertThat(w.io.lastOutput()).contains("not carrying");
}
@Test
void withItems_listsByName() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("letter"));
new TakeCommand().execute(w.ctx, List.of("lamp"));
new InventoryCommand().execute(w.ctx, List.of());
assertThat(w.io.lastOutput()).contains("Letter").contains("Lamp");
}
}

View File

@@ -0,0 +1,37 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class LookCommandTest {
@Test
void look_writesNameDescriptionItemsNpcsAndExits() {
CommandTestSupport.World w = new CommandTestSupport.World();
new LookCommand().execute(w.ctx, List.of());
String text = w.io.lastOutput();
assertThat(text).contains("Kitchen");
assertThat(text).contains("kitchen desc");
assertThat(text).contains("Lamp").contains("Letter");
assertThat(text).contains("Old Man");
assertThat(text).contains("north");
}
@Test
void look_noItemsNoNpcs_stillWorks() {
CommandTestSupport.World w = new CommandTestSupport.World();
w.player.setCurrentRoom(w.hallway);
new LookCommand().execute(w.ctx, List.of());
String text = w.io.lastOutput();
assertThat(text).contains("Hallway");
assertThat(text).contains("south");
assertThat(text).doesNotContain("You see");
}
}

View File

@@ -0,0 +1,45 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class TakeDropCommandTest {
@Test
void take_movesItemFromRoomToInventory() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("lamp"));
assertThat(w.player.hasItem("lamp")).isTrue();
assertThat(w.kitchen.findItem("lamp")).isEmpty();
}
@Test
void take_unknownItem_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("dragon"));
assertThat(w.io.allOutput()).contains("no 'dragon'");
}
@Test
void drop_movesItemFromInventoryToRoom() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("lamp"));
new DropCommand().execute(w.ctx, List.of("lamp"));
assertThat(w.player.hasItem("lamp")).isFalse();
assertThat(w.kitchen.findItem("lamp")).isPresent();
}
@Test
void drop_notInInventory_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new DropCommand().execute(w.ctx, List.of("lamp"));
assertThat(w.io.allOutput()).contains("not carrying");
}
}

View File

@@ -0,0 +1,64 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class TalkGiveCommandTest {
@Test
void talk_existingNpc_writesGreeting() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TalkCommand().execute(w.ctx, List.of("old_man"));
assertThat(w.io.lastOutput()).contains("Old Man").contains("greetings");
}
@Test
void talk_missingNpc_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TalkCommand().execute(w.ctx, List.of("ghost"));
assertThat(w.io.allOutput()).contains("no 'ghost'");
}
@Test
void give_matchingReaction_swapsItems() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("lamp"));
new GiveCommand().execute(w.ctx, List.of("lamp", "old_man"));
assertThat(w.player.hasItem("lamp")).isFalse();
assertThat(w.player.hasItem("key")).isTrue();
assertThat(w.io.lastOutput()).contains("Take the key");
}
@Test
void give_noReaction_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("letter"));
new GiveCommand().execute(w.ctx, List.of("letter", "old_man"));
assertThat(w.player.hasItem("letter")).isTrue();
assertThat(w.io.lastOutput()).contains("does not react");
}
@Test
void give_unknownNpc_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("lamp"));
new GiveCommand().execute(w.ctx, List.of("lamp", "ghost"));
assertThat(w.io.allOutput()).contains("no 'ghost'");
}
@Test
void give_itemNotHeld_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new GiveCommand().execute(w.ctx, List.of("lamp", "old_man"));
assertThat(w.io.allOutput()).contains("not carrying");
}
}

View File

@@ -0,0 +1,51 @@
package thb.jeanluc.adventure.command.impl;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class UseReadExamineCommandTest {
@Test
void use_readableInInventory_writesText() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("letter"));
new UseCommand().execute(w.ctx, List.of("letter"));
assertThat(w.io.lastOutput()).isEqualTo("dear reader");
}
@Test
void use_unknownItem_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new UseCommand().execute(w.ctx, List.of("dragon"));
assertThat(w.io.allOutput()).contains("no 'dragon'");
}
@Test
void read_nonReadable_complains() {
CommandTestSupport.World w = new CommandTestSupport.World();
new TakeCommand().execute(w.ctx, List.of("lamp"));
new ReadCommand().execute(w.ctx, List.of("lamp"));
assertThat(w.io.lastOutput()).contains("nothing to read");
}
@Test
void examine_item_writesDescription() {
CommandTestSupport.World w = new CommandTestSupport.World();
new ExamineCommand().execute(w.ctx, List.of("letter"));
assertThat(w.io.lastOutput()).isEqualTo("a letter");
}
@Test
void examine_npc_writesDescription() {
CommandTestSupport.World w = new CommandTestSupport.World();
new ExamineCommand().execute(w.ctx, List.of("old_man"));
assertThat(w.io.lastOutput()).isEqualTo("stooped");
}
}

View File

@@ -0,0 +1,58 @@
package thb.jeanluc.adventure.game;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.command.CommandParser;
import thb.jeanluc.adventure.command.CommandRegistry;
import thb.jeanluc.adventure.command.impl.GoCommand;
import thb.jeanluc.adventure.command.impl.LookCommand;
import thb.jeanluc.adventure.command.impl.QuitCommand;
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 static org.assertj.core.api.Assertions.assertThat;
class GameTest {
@Test
void run_dispatchesCommands_andQuitsCleanly() {
Room kitchen = new Room("kitchen", "Kitchen", "kd");
Room hallway = new Room("hallway", "Hallway", "hd");
kitchen.addExit(Direction.NORTH, hallway);
Player player = new Player(kitchen, 0);
TestIO io = new TestIO()
.enqueue("go north")
.enqueue("quit");
GameContext ctx = new GameContext(null, player, io);
CommandRegistry registry = new CommandRegistry();
registry.register(new GoCommand(), "go");
registry.register(new LookCommand(), "look");
QuitCommand quit = new QuitCommand();
registry.register(quit, "quit");
Game game = new Game(ctx, registry, new CommandParser());
quit.bind(game);
game.run();
assertThat(player.getCurrentRoom()).isEqualTo(hallway);
assertThat(io.allOutput()).contains("Goodbye");
}
@Test
void run_unknownVerb_writesHint() {
Player player = new Player(new Room("r", "R", "d"), 0);
TestIO io = new TestIO().enqueue("dance").enqueue("quit");
GameContext ctx = new GameContext(null, player, io);
CommandRegistry registry = new CommandRegistry();
QuitCommand quit = new QuitCommand();
registry.register(quit, "quit");
Game game = new Game(ctx, registry, new CommandParser());
quit.bind(game);
game.run();
assertThat(io.allOutput()).contains("don't understand 'dance'");
}
}

View File

@@ -0,0 +1,42 @@
package thb.jeanluc.adventure.io;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* Simple in-memory {@link GameIO} for tests: queued inputs, captured outputs.
*/
public class TestIO implements GameIO {
private final Deque<String> inputs = new ArrayDeque<>();
private final List<String> outputs = new ArrayList<>();
public TestIO enqueue(String line) {
inputs.add(line);
return this;
}
public List<String> outputs() {
return outputs;
}
public String lastOutput() {
return outputs.isEmpty() ? null : outputs.getLast();
}
public String allOutput() {
return String.join("\n", outputs);
}
@Override
public String readLine() {
return inputs.pollFirst();
}
@Override
public void write(String s) {
outputs.add(s);
}
}

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

View File

@@ -0,0 +1,41 @@
package thb.jeanluc.adventure.model;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class DirectionTest {
@Test
void getOpposite_pairsAreSymmetric() {
for (Direction d : Direction.values()) {
assertThat(d.getOpposite().getOpposite()).isEqualTo(d);
}
}
@Test
void fromString_caseInsensitive_returnsEnum() {
assertThat(Direction.fromString("north")).isEqualTo(Direction.NORTH);
assertThat(Direction.fromString("EAST")).isEqualTo(Direction.EAST);
assertThat(Direction.fromString(" west ")).isEqualTo(Direction.WEST);
}
@Test
void fromString_unknown_throws() {
assertThatThrownBy(() -> Direction.fromString("up"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("up");
}
@Test
void fromString_null_throws() {
assertThatThrownBy(() -> Direction.fromString(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void getLabel_isLowercase() {
assertThat(Direction.NORTH.getLabel()).isEqualTo("north");
}
}

View File

@@ -0,0 +1,22 @@
package thb.jeanluc.adventure.model;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.PlainItem;
import static org.assertj.core.api.Assertions.assertThat;
class NpcTest {
@Test
void shell_isMutableForReactions() {
Npc man = Npc.shell("man", "Man", "d", "hi");
Item lamp = PlainItem.builder().id("lamp").name("Lamp").description("d").build();
man.putReaction("lamp", NpcReaction.builder()
.consumes(lamp).response("thanks").build());
assertThat(man.reactionFor("lamp")).isPresent();
assertThat(man.reactionFor("nope")).isEmpty();
}
}

View File

@@ -0,0 +1,57 @@
package thb.jeanluc.adventure.model;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.PlainItem;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class PlayerTest {
@Test
void constructor_nullRoom_throws() {
assertThatThrownBy(() -> new Player(null, 0))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void constructor_negativeGold_throws() {
Room r = new Room("r", "R", "d");
assertThatThrownBy(() -> new Player(r, -1))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void addItem_andRemoveItem_areConsistent() {
Player p = new Player(new Room("r", "R", "d"), 0);
Item it = PlainItem.builder().id("key").name("Key").description("d").build();
p.addItem(it);
assertThat(p.hasItem("key")).isTrue();
assertThat(p.removeItem("key")).contains(it);
assertThat(p.hasItem("key")).isFalse();
}
@Test
void inventory_preservesInsertionOrder() {
Player p = new Player(new Room("r", "R", "d"), 0);
p.addItem(PlainItem.builder().id("a").name("A").description("d").build());
p.addItem(PlainItem.builder().id("b").name("B").description("d").build());
p.addItem(PlainItem.builder().id("c").name("C").description("d").build());
assertThat(p.getInventory().keySet()).containsExactly("a", "b", "c");
}
@Test
void setCurrentRoom_updatesPosition() {
Room a = new Room("a", "A", "d");
Room b = new Room("b", "B", "d");
Player p = new Player(a, 0);
p.setCurrentRoom(b);
assertThat(p.getCurrentRoom()).isEqualTo(b);
}
}

View File

@@ -0,0 +1,69 @@
package thb.jeanluc.adventure.model;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.PlainItem;
import static org.assertj.core.api.Assertions.assertThat;
class RoomTest {
@Test
void addExit_withNewDirection_storesNeighbour() {
Room a = new Room("a", "A", "first");
Room b = new Room("b", "B", "second");
a.addExit(Direction.NORTH, b);
assertThat(a.getExit(Direction.NORTH)).contains(b);
assertThat(a.getExit(Direction.SOUTH)).isEmpty();
}
@Test
void addItem_storesUnderItemId() {
Room room = new Room("r", "R", "d");
Item lamp = PlainItem.builder().id("lamp").name("Lamp").description("d").build();
room.addItem(lamp);
assertThat(room.findItem("lamp")).contains(lamp);
}
@Test
void removeItem_returnsAndDetachesItem() {
Room room = new Room("r", "R", "d");
Item lamp = PlainItem.builder().id("lamp").name("Lamp").description("d").build();
room.addItem(lamp);
var removed = room.removeItem("lamp");
assertThat(removed).contains(lamp);
assertThat(room.findItem("lamp")).isEmpty();
}
@Test
void removeItem_unknownId_returnsEmpty() {
Room room = new Room("r", "R", "d");
assertThat(room.removeItem("nope")).isEmpty();
}
@Test
void addNpc_andFindById() {
Room room = new Room("r", "R", "d");
Npc npc = Npc.shell("man", "Man", "d", "hi");
room.addNpc(npc);
assertThat(room.findNpc("man")).contains(npc);
}
@Test
void items_preserveInsertionOrder() {
Room room = new Room("r", "R", "d");
room.addItem(PlainItem.builder().id("a").name("A").description("d").build());
room.addItem(PlainItem.builder().id("b").name("B").description("d").build());
room.addItem(PlainItem.builder().id("c").name("C").description("d").build());
assertThat(room.getItems().keySet()).containsExactly("a", "b", "c");
}
}

View File

@@ -0,0 +1,66 @@
package thb.jeanluc.adventure.model.item;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.game.GameContext;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import static org.assertj.core.api.Assertions.assertThat;
class ItemTest {
private GameContext newCtx(TestIO io) {
Player p = new Player(new Room("r", "R", "d"), 0);
return new GameContext(null, p, io);
}
@Test
void readableItem_use_writesReadText() {
TestIO io = new TestIO();
ReadableItem letter = ReadableItem.builder()
.id("letter").name("Letter").description("d")
.readText("hello world").build();
letter.use(newCtx(io));
assertThat(io.lastOutput()).isEqualTo("hello world");
}
@Test
void switchableItem_use_togglesAndPrintsAppropriateText() {
TestIO io = new TestIO();
SwitchableItem lamp = SwitchableItem.builder()
.id("lamp").name("Lamp").description("d")
.state(false).onText("on").offText("off").build();
lamp.use(newCtx(io));
assertThat(lamp.isOn()).isTrue();
assertThat(io.outputs()).containsExactly("on");
lamp.use(newCtx(io));
assertThat(lamp.isOn()).isFalse();
assertThat(io.outputs()).endsWith("off");
}
@Test
void switchableItem_initialStateTrue_startsOn() {
SwitchableItem lamp = SwitchableItem.builder()
.id("lamp").name("Lamp").description("d")
.state(true).onText("on").offText("off").build();
assertThat(lamp.isOn()).isTrue();
}
@Test
void plainItem_use_writesGenericMessage() {
TestIO io = new TestIO();
PlainItem shovel = PlainItem.builder()
.id("shovel").name("Shovel").description("d").build();
shovel.use(newCtx(io));
assertThat(io.lastOutput()).contains("Shovel");
assertThat(io.lastOutput()).contains("can't use");
}
}