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,119 @@
|
||||
package thb.jeanluc.adventure.game;
|
||||
|
||||
import thb.jeanluc.adventure.command.Command;
|
||||
import thb.jeanluc.adventure.command.CommandRegistry;
|
||||
import thb.jeanluc.adventure.command.ParsedCommand;
|
||||
import thb.jeanluc.adventure.model.Direction;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
import thb.jeanluc.adventure.model.Tutorial;
|
||||
import thb.jeanluc.adventure.model.TutorialStep;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Drives the interactive start-of-game walkthrough, layered onto the real game
|
||||
* loop. Prints an instruction, then on each executed command decides whether the
|
||||
* current step is satisfied (confirm + advance) or not (hint). Movement steps
|
||||
* require an actual room change. Alias-aware via the {@link CommandRegistry}.
|
||||
*/
|
||||
public final class TutorialGuide {
|
||||
|
||||
private final Tutorial tutorial;
|
||||
private final CommandRegistry registry;
|
||||
private int index;
|
||||
private boolean active;
|
||||
private Room roomAtStepStart;
|
||||
|
||||
public TutorialGuide(Tutorial tutorial, CommandRegistry registry) {
|
||||
this.tutorial = tutorial;
|
||||
this.registry = registry;
|
||||
this.active = !tutorial.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
/** Prints the intro and the first instruction (call once at loop start). */
|
||||
public void begin(GameContext ctx) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
if (tutorial.intro() != null && !tutorial.intro().isBlank()) {
|
||||
ctx.getIo().write(tutorial.intro());
|
||||
}
|
||||
enterStep(ctx);
|
||||
}
|
||||
|
||||
/** Evaluates the current step against the command that was just executed. */
|
||||
public void onCommand(GameContext ctx, ParsedCommand parsed) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
TutorialStep step = tutorial.steps().get(index);
|
||||
if (satisfies(step, ctx, parsed)) {
|
||||
ctx.getIo().write(step.confirm());
|
||||
index++;
|
||||
if (index >= tutorial.steps().size()) {
|
||||
if (tutorial.closingTips() != null && !tutorial.closingTips().isBlank()) {
|
||||
ctx.getIo().write(tutorial.closingTips());
|
||||
}
|
||||
active = false;
|
||||
} else {
|
||||
enterStep(ctx);
|
||||
}
|
||||
} else {
|
||||
ctx.getIo().write(step.hint());
|
||||
}
|
||||
}
|
||||
|
||||
/** Ends the tutorial early (the 'skip' command). */
|
||||
public void skip(GameContext ctx) {
|
||||
if (active) {
|
||||
ctx.getIo().write("Tutorial skipped.");
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void enterStep(GameContext ctx) {
|
||||
roomAtStepStart = ctx.getPlayer().getCurrentRoom();
|
||||
ctx.getIo().write(tutorial.steps().get(index).instruction());
|
||||
}
|
||||
|
||||
private boolean satisfies(TutorialStep step, GameContext ctx, ParsedCommand parsed) {
|
||||
String expect = step.expect();
|
||||
if ("go_direction".equals(expect)) {
|
||||
return isGo(parsed) && firstIsDirection(parsed) && moved(ctx);
|
||||
}
|
||||
if ("go_to_room".equals(expect)) {
|
||||
return isGo(parsed) && !firstIsDirection(parsed) && moved(ctx);
|
||||
}
|
||||
return sameCommand(parsed.verb(), expect) && parsed.args().size() >= step.minArgs();
|
||||
}
|
||||
|
||||
private boolean isGo(ParsedCommand p) {
|
||||
return sameCommand(p.verb(), "go") && !p.args().isEmpty();
|
||||
}
|
||||
|
||||
private boolean firstIsDirection(ParsedCommand p) {
|
||||
if (p.args().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Direction.fromString(p.args().get(0));
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean moved(GameContext ctx) {
|
||||
return ctx.getPlayer().getCurrentRoom() != roomAtStepStart;
|
||||
}
|
||||
|
||||
private boolean sameCommand(String a, String b) {
|
||||
Optional<Command> ca = registry.find(a);
|
||||
Optional<Command> cb = registry.find(b);
|
||||
return ca.isPresent() && cb.isPresent() && ca.get() == cb.get();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user