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