From 0f05680170af2275912825441a8a7abd0c018898 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 15:05:27 +0200 Subject: [PATCH] feat(command): save + menu commands (QuitCommand removal deferred to Task 11) --- .../adventure/command/impl/MenuCommand.java | 44 +++++++++++++++++ .../adventure/command/impl/SaveCommand.java | 31 ++++++++++++ .../command/SaveMenuCommandTest.java | 49 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/MenuCommand.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/SaveCommand.java create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/SaveMenuCommandTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/MenuCommand.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/MenuCommand.java new file mode 100644 index 0000000..ee6145e --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/MenuCommand.java @@ -0,0 +1,44 @@ +package thb.jeanluc.adventure.command.impl; + +import lombok.RequiredArgsConstructor; +import thb.jeanluc.adventure.command.Command; +import thb.jeanluc.adventure.game.Game; +import thb.jeanluc.adventure.game.GameContext; +import thb.jeanluc.adventure.save.SaveException; +import thb.jeanluc.adventure.save.SaveService; + +import java.util.List; + +/** + * Saves the active slot and returns to the main menu by stopping the loop. + * Usage: {@code menu} (aliases {@code quit}, {@code exit}). + */ +@RequiredArgsConstructor +public class MenuCommand implements Command { + + private final SaveService saves; + private Game game; + + /** Binds the running game (two-phase wiring, as the registry is built first). */ + public void bind(Game game) { + this.game = game; + } + + @Override + public void execute(GameContext ctx, List args) { + try { + saves.save(ctx.getSession()); + ctx.getIo().write("Game saved. Returning to the main menu."); + } catch (SaveException e) { + ctx.getIo().write("Could not save: " + e.getUserMessage()); + } + if (game != null) { + game.stop(); + } + } + + @Override + public String help() { + return "menu - save and return to the main menu (aliases: quit, exit)"; + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/SaveCommand.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/SaveCommand.java new file mode 100644 index 0000000..54ce7fd --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/SaveCommand.java @@ -0,0 +1,31 @@ +package thb.jeanluc.adventure.command.impl; + +import lombok.RequiredArgsConstructor; +import thb.jeanluc.adventure.command.Command; +import thb.jeanluc.adventure.game.GameContext; +import thb.jeanluc.adventure.save.SaveException; +import thb.jeanluc.adventure.save.SaveService; + +import java.util.List; + +/** Manually saves the active slot. Usage: {@code save}. */ +@RequiredArgsConstructor +public class SaveCommand implements Command { + + private final SaveService saves; + + @Override + public void execute(GameContext ctx, List args) { + try { + saves.save(ctx.getSession()); + ctx.getIo().write("Game saved."); + } catch (SaveException e) { + ctx.getIo().write(e.getUserMessage()); + } + } + + @Override + public String help() { + return "save - save your game to the current slot"; + } +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/SaveMenuCommandTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/SaveMenuCommandTest.java new file mode 100644 index 0000000..dbcd07b --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/command/SaveMenuCommandTest.java @@ -0,0 +1,49 @@ +package thb.jeanluc.adventure.command; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import thb.jeanluc.adventure.command.impl.MenuCommand; +import thb.jeanluc.adventure.command.impl.SaveCommand; +import thb.jeanluc.adventure.game.Game; +import thb.jeanluc.adventure.game.GameContext; +import thb.jeanluc.adventure.game.GameSession; +import thb.jeanluc.adventure.io.TestIO; +import thb.jeanluc.adventure.loader.WorldLoader; +import thb.jeanluc.adventure.save.SaveService; + +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class SaveMenuCommandTest { + + private GameContext ctx(String slot) { + WorldLoader.LoadResult r = new WorldLoader().load(); + return new GameContext(new GameSession(r.world(), r.player(), slot), new TestIO()); + } + + @Test + void saveCommandWritesSlotAndReportsSuccess(@TempDir Path dir) { + SaveService svc = new SaveService(dir); + GameContext ctx = ctx("game-a"); + new SaveCommand(svc).execute(ctx, List.of()); + assertThat(svc.list()).extracting(s -> s.slotName()).containsExactly("game-a"); + assertThat(((TestIO) ctx.getIo()).allOutput()).contains("Game saved"); + } + + @Test + void menuCommandSavesThenStopsLoop(@TempDir Path dir) { + SaveService svc = new SaveService(dir); + GameContext ctx = ctx("game-b"); + Game game = new Game(ctx, new CommandRegistry(), new CommandParser()); + MenuCommand menu = new MenuCommand(svc); + menu.bind(game); + menu.execute(ctx, List.of()); + assertThat(svc.list()).hasSize(1); + // loop is stopped: run() must return without consuming queued input + ((TestIO) ctx.getIo()).enqueue("look"); + game.run(); + assertThat(ctx.getIo().readLine()).isEqualTo("look"); // sentinel still unread + } +}