feat(game): TutorialGuide interactive walkthrough engine
Alias-aware, stateful engine with verb/minArgs matching, go_direction/go_to_room room-change detection, skip, and closing-tips on completion. 7 tests, all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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.ParsedCommand;
|
||||
import thb.jeanluc.adventure.command.impl.ExamineCommand;
|
||||
import thb.jeanluc.adventure.command.impl.GoCommand;
|
||||
import thb.jeanluc.adventure.command.impl.InventoryCommand;
|
||||
import thb.jeanluc.adventure.command.impl.LookCommand;
|
||||
import thb.jeanluc.adventure.command.impl.TakeCommand;
|
||||
import thb.jeanluc.adventure.command.impl.UseCommand;
|
||||
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.Tutorial;
|
||||
import thb.jeanluc.adventure.model.TutorialStep;
|
||||
import thb.jeanluc.adventure.model.World;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TutorialGuideTest {
|
||||
|
||||
private CommandRegistry registry() {
|
||||
CommandRegistry r = new CommandRegistry();
|
||||
r.register(new LookCommand(), "look", "l");
|
||||
r.register(new ExamineCommand(), "examine", "x");
|
||||
r.register(new TakeCommand(), "take", "get");
|
||||
r.register(new InventoryCommand(), "inventory", "i");
|
||||
r.register(new UseCommand(), "use");
|
||||
r.register(new GoCommand(), "go", "move", "walk");
|
||||
return r;
|
||||
}
|
||||
|
||||
private final Room kitchen = new Room("kitchen", "Kitchen", "d");
|
||||
private final Room hallway = new Room("hallway", "Hallway", "d");
|
||||
|
||||
private GameContext ctx() {
|
||||
kitchen.addExit(Direction.NORTH, hallway);
|
||||
Map<String, Room> rooms = new LinkedHashMap<>();
|
||||
rooms.put("kitchen", kitchen);
|
||||
rooms.put("hallway", hallway);
|
||||
World w = new World(rooms, Map.of(), Map.of(), "t", "w");
|
||||
Player p = new Player(kitchen, 0);
|
||||
return new GameContext(w, p, new TestIO());
|
||||
}
|
||||
|
||||
private ParsedCommand parse(String s) {
|
||||
return new CommandParser().parse(s);
|
||||
}
|
||||
|
||||
private String out(GameContext ctx) {
|
||||
return ((TestIO) ctx.getIo()).allOutput();
|
||||
}
|
||||
|
||||
private TutorialStep step(String instr, String expect, int minArgs) {
|
||||
return new TutorialStep(instr, expect, minArgs, "OK:" + expect, "HINT:" + expect);
|
||||
}
|
||||
|
||||
@Test
|
||||
void beginPrintsIntroAndFirstInstruction() {
|
||||
GameContext ctx = ctx();
|
||||
Tutorial t = new Tutorial("INTRO", List.of(step("do look", "look", 0)), "TIPS");
|
||||
TutorialGuide g = new TutorialGuide(t, registry());
|
||||
g.begin(ctx);
|
||||
assertThat(out(ctx)).contains("INTRO").contains("do look");
|
||||
assertThat(g.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasSatisfiesVerbStepAndAdvancesToTips() {
|
||||
GameContext ctx = ctx();
|
||||
Tutorial t = new Tutorial("INTRO", List.of(step("do look", "look", 0)), "TIPS");
|
||||
TutorialGuide g = new TutorialGuide(t, registry());
|
||||
g.begin(ctx);
|
||||
g.onCommand(ctx, parse("l")); // alias of look
|
||||
assertThat(out(ctx)).contains("OK:look").contains("TIPS");
|
||||
assertThat(g.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minArgsGatesTakeStep() {
|
||||
GameContext ctx = ctx();
|
||||
Tutorial t = new Tutorial("I", List.of(step("take it", "take", 1)), "TIPS");
|
||||
TutorialGuide g = new TutorialGuide(t, registry());
|
||||
g.begin(ctx);
|
||||
g.onCommand(ctx, parse("take")); // no arg -> hint, not satisfied
|
||||
assertThat(out(ctx)).contains("HINT:take");
|
||||
assertThat(g.isActive()).isTrue();
|
||||
g.onCommand(ctx, parse("take lamp")); // satisfied
|
||||
assertThat(out(ctx)).contains("OK:take");
|
||||
}
|
||||
|
||||
@Test
|
||||
void goDirectionRequiresAnActualMove() {
|
||||
GameContext ctx = ctx();
|
||||
Tutorial t = new Tutorial("I", List.of(step("move", "go_direction", 0)), "TIPS");
|
||||
TutorialGuide g = new TutorialGuide(t, registry());
|
||||
g.begin(ctx);
|
||||
// typed a direction but did NOT move -> hint
|
||||
g.onCommand(ctx, parse("go north"));
|
||||
assertThat(out(ctx)).contains("HINT:go_direction");
|
||||
assertThat(g.isActive()).isTrue();
|
||||
// now actually move, then the same command satisfies
|
||||
ctx.getPlayer().setCurrentRoom(hallway);
|
||||
g.onCommand(ctx, parse("go north"));
|
||||
assertThat(out(ctx)).contains("OK:go_direction");
|
||||
assertThat(g.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void goToRoomNeedsNonDirectionArgAndMove() {
|
||||
GameContext ctx = ctx();
|
||||
Tutorial t = new Tutorial("I", List.of(step("travel", "go_to_room", 0)), "TIPS");
|
||||
TutorialGuide g = new TutorialGuide(t, registry());
|
||||
g.begin(ctx);
|
||||
ctx.getPlayer().setCurrentRoom(hallway); // moved
|
||||
g.onCommand(ctx, parse("go kitchen")); // "kitchen" is not a direction
|
||||
assertThat(out(ctx)).contains("OK:go_to_room");
|
||||
}
|
||||
|
||||
@Test
|
||||
void skipDeactivates() {
|
||||
GameContext ctx = ctx();
|
||||
Tutorial t = new Tutorial("I", List.of(step("do look", "look", 0)), "TIPS");
|
||||
TutorialGuide g = new TutorialGuide(t, registry());
|
||||
g.begin(ctx);
|
||||
g.skip(ctx);
|
||||
assertThat(out(ctx)).contains("Tutorial skipped");
|
||||
assertThat(g.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyTutorialIsInactive() {
|
||||
TutorialGuide g = new TutorialGuide(Tutorial.none(), registry());
|
||||
assertThat(g.isActive()).isFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user