feat: wire TutorialGuide into the game loop; show on New Game only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,9 @@ import thb.jeanluc.adventure.command.impl.UseCommand;
|
|||||||
import thb.jeanluc.adventure.game.Game;
|
import thb.jeanluc.adventure.game.Game;
|
||||||
import thb.jeanluc.adventure.game.GameContext;
|
import thb.jeanluc.adventure.game.GameContext;
|
||||||
import thb.jeanluc.adventure.game.GameSession;
|
import thb.jeanluc.adventure.game.GameSession;
|
||||||
|
import thb.jeanluc.adventure.game.TutorialGuide;
|
||||||
|
import thb.jeanluc.adventure.loader.TutorialLoader;
|
||||||
|
import thb.jeanluc.adventure.model.Tutorial;
|
||||||
import thb.jeanluc.adventure.io.ConsoleIO;
|
import thb.jeanluc.adventure.io.ConsoleIO;
|
||||||
import thb.jeanluc.adventure.io.GameIO;
|
import thb.jeanluc.adventure.io.GameIO;
|
||||||
import thb.jeanluc.adventure.io.text.Banner;
|
import thb.jeanluc.adventure.io.text.Banner;
|
||||||
@@ -70,12 +73,12 @@ public final class App {
|
|||||||
boolean running = true;
|
boolean running = true;
|
||||||
while (running) {
|
while (running) {
|
||||||
switch (MainMenu.show(io)) {
|
switch (MainMenu.show(io)) {
|
||||||
case NEW_GAME -> play(io, saves, newSession(io));
|
case NEW_GAME -> play(io, saves, newSession(io), true);
|
||||||
case LOAD -> {
|
case LOAD -> {
|
||||||
SaveSlotInfo slot = MainMenu.pickSlot(io, saves);
|
SaveSlotInfo slot = MainMenu.pickSlot(io, saves);
|
||||||
if (slot != null) {
|
if (slot != null) {
|
||||||
try {
|
try {
|
||||||
play(io, saves, saves.load(slot.slug()));
|
play(io, saves, saves.load(slot.slug()), false);
|
||||||
} catch (SaveException e) {
|
} catch (SaveException e) {
|
||||||
io.write("Could not load: " + e.getUserMessage());
|
io.write("Could not load: " + e.getUserMessage());
|
||||||
}
|
}
|
||||||
@@ -96,7 +99,7 @@ public final class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the registry, runs one game to completion, autosaving to its slot. */
|
/** Builds the registry, runs one game to completion, autosaving to its slot. */
|
||||||
private static void play(GameIO io, SaveService saves, GameSession session) {
|
private static void play(GameIO io, SaveService saves, GameSession session, boolean withTutorial) {
|
||||||
GameContext ctx = new GameContext(session, io);
|
GameContext ctx = new GameContext(session, io);
|
||||||
ctx.setSaveCallback(() -> {
|
ctx.setSaveCallback(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -128,6 +131,13 @@ public final class App {
|
|||||||
registry.register(menu, "menu", "quit", "exit");
|
registry.register(menu, "menu", "quit", "exit");
|
||||||
registry.register(new HelpCommand(registry), "help", "?");
|
registry.register(new HelpCommand(registry), "help", "?");
|
||||||
|
|
||||||
|
if (withTutorial) {
|
||||||
|
Tutorial tutorial = new TutorialLoader().load();
|
||||||
|
if (!tutorial.isEmpty()) {
|
||||||
|
game.setTutorialGuide(new TutorialGuide(tutorial, registry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
io.print(Banner.welcome(session.getWorld().getTitle()));
|
io.print(Banner.welcome(session.getWorld().getTitle()));
|
||||||
io.write(session.getWorld().getWelcomeMessage());
|
io.write(session.getWorld().getWelcomeMessage());
|
||||||
new LookCommand().execute(ctx, List.of());
|
new LookCommand().execute(ctx, List.of());
|
||||||
|
|||||||
@@ -32,12 +32,23 @@ public class Game {
|
|||||||
/** Loop flag. Flipped to false by {@link #stop()}. */
|
/** Loop flag. Flipped to false by {@link #stop()}. */
|
||||||
private boolean running = true;
|
private boolean running = true;
|
||||||
|
|
||||||
|
/** Optional onboarding guide; null in normal/loaded play. */
|
||||||
|
private TutorialGuide tutorialGuide;
|
||||||
|
|
||||||
|
/** Wires the interactive tutorial (New Game only). */
|
||||||
|
public void setTutorialGuide(TutorialGuide tutorialGuide) {
|
||||||
|
this.tutorialGuide = tutorialGuide;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the loop until {@link #stop()} is called or input is exhausted.
|
* Runs the loop until {@link #stop()} is called or input is exhausted.
|
||||||
*/
|
*/
|
||||||
public void run() {
|
public void run() {
|
||||||
publishHud();
|
publishHud();
|
||||||
maybeEnd();
|
maybeEnd();
|
||||||
|
if (tutorialGuide != null) {
|
||||||
|
tutorialGuide.begin(ctx);
|
||||||
|
}
|
||||||
while (running) {
|
while (running) {
|
||||||
String input = ctx.getIo().readLine();
|
String input = ctx.getIo().readLine();
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
@@ -47,6 +58,12 @@ public class Game {
|
|||||||
if (parsed.verb().isEmpty()) {
|
if (parsed.verb().isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (tutorialGuide != null && tutorialGuide.isActive()
|
||||||
|
&& parsed.verb().equals("skip")) {
|
||||||
|
tutorialGuide.skip(ctx);
|
||||||
|
publishHud();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Optional<Command> cmd = registry.find(parsed.verb());
|
Optional<Command> cmd = registry.find(parsed.verb());
|
||||||
if (cmd.isEmpty()) {
|
if (cmd.isEmpty()) {
|
||||||
ctx.getIo().write("I don't understand '" + parsed.verb() + "'. Type 'help'.");
|
ctx.getIo().write("I don't understand '" + parsed.verb() + "'. Type 'help'.");
|
||||||
@@ -56,6 +73,9 @@ public class Game {
|
|||||||
if (ctx.getSession().getTurn() % 10 == 0) {
|
if (ctx.getSession().getTurn() % 10 == 0) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
}
|
}
|
||||||
|
if (tutorialGuide != null) {
|
||||||
|
tutorialGuide.onCommand(ctx, parsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
publishHud();
|
publishHud();
|
||||||
maybeEnd();
|
maybeEnd();
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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.LookCommand;
|
||||||
|
import thb.jeanluc.adventure.io.TestIO;
|
||||||
|
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.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class GameTutorialTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void guideBeginsAndAdvancesThenSkipEndsViaLoop() {
|
||||||
|
Room kitchen = new Room("kitchen", "Kitchen", "d");
|
||||||
|
World w = new World(Map.of("kitchen", kitchen), Map.of(), Map.of(), "t", "w");
|
||||||
|
TestIO io = new TestIO();
|
||||||
|
// scripted player input: a matching 'look', then 'skip'; then EOF (null) ends run()
|
||||||
|
io.enqueue("look").enqueue("skip");
|
||||||
|
GameContext ctx = new GameContext(new GameSession(w, new Player(kitchen, 0), "s"), io);
|
||||||
|
|
||||||
|
CommandRegistry registry = new CommandRegistry();
|
||||||
|
registry.register(new LookCommand(), "look", "l");
|
||||||
|
|
||||||
|
Tutorial t = new Tutorial("INTRO",
|
||||||
|
List.of(new TutorialStep("do look", "look", 0, "STEP1-OK", "hint"),
|
||||||
|
new TutorialStep("do look again", "look", 0, "STEP2-OK", "hint")),
|
||||||
|
"TIPS");
|
||||||
|
Game game = new Game(ctx, registry, new CommandParser());
|
||||||
|
game.setTutorialGuide(new TutorialGuide(t, registry));
|
||||||
|
|
||||||
|
game.run();
|
||||||
|
|
||||||
|
String out = io.allOutput();
|
||||||
|
assertThat(out).contains("INTRO"); // begin printed
|
||||||
|
assertThat(out).contains("STEP1-OK"); // first 'look' advanced step 1
|
||||||
|
assertThat(out).contains("Tutorial skipped"); // 'skip' ended it before step 2 finished
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user