From 4604952f633d558fd8c1cc4000706ae15bf7ff49 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 15:17:46 +0200 Subject: [PATCH] feat(app): main-menu shell loop wiring; gitignore saves/ Replace App.run with a main-menu shell (New Game / Load / Settings / Quit) above the game loop, wire SaveCommand + MenuCommand into the registry with an autosave callback, and remove the now-unused QuitCommand. Migrate GameTest from QuitCommand to MenuCommand. Co-Authored-By: Claude Opus 4.8 (1M context) --- Semesterprojekt/.gitignore | 3 + .../main/java/thb/jeanluc/adventure/App.java | 96 ++++++++++++++----- .../adventure/command/impl/QuitCommand.java | 41 -------- .../thb/jeanluc/adventure/game/GameTest.java | 14 ++- 4 files changed, 84 insertions(+), 70 deletions(-) delete mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/QuitCommand.java diff --git a/Semesterprojekt/.gitignore b/Semesterprojekt/.gitignore index b55fdef..90926aa 100644 --- a/Semesterprojekt/.gitignore +++ b/Semesterprojekt/.gitignore @@ -40,3 +40,6 @@ build/ ### Brainstorming visual companion ### .superpowers/ + +# Local save games and settings +saves/ diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java index c609650..e20b232 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/App.java @@ -11,23 +11,36 @@ import thb.jeanluc.adventure.command.impl.HelpCommand; import thb.jeanluc.adventure.command.impl.InventoryCommand; import thb.jeanluc.adventure.command.impl.LookCommand; import thb.jeanluc.adventure.command.impl.MapCommand; +import thb.jeanluc.adventure.command.impl.MenuCommand; import thb.jeanluc.adventure.command.impl.QuestsCommand; -import thb.jeanluc.adventure.command.impl.QuitCommand; import thb.jeanluc.adventure.command.impl.ReadCommand; +import thb.jeanluc.adventure.command.impl.SaveCommand; import thb.jeanluc.adventure.command.impl.TakeCommand; import thb.jeanluc.adventure.command.impl.TalkCommand; import thb.jeanluc.adventure.command.impl.UseCommand; import thb.jeanluc.adventure.game.Game; import thb.jeanluc.adventure.game.GameContext; +import thb.jeanluc.adventure.game.GameSession; import thb.jeanluc.adventure.io.ConsoleIO; import thb.jeanluc.adventure.io.GameIO; import thb.jeanluc.adventure.io.text.Banner; import thb.jeanluc.adventure.loader.WorldLoader; +import thb.jeanluc.adventure.menu.MainMenu; +import thb.jeanluc.adventure.menu.MenuAction; +import thb.jeanluc.adventure.menu.SettingsMenu; +import thb.jeanluc.adventure.save.SaveException; +import thb.jeanluc.adventure.save.SaveService; +import thb.jeanluc.adventure.save.SaveSlotInfo; +import thb.jeanluc.adventure.save.Settings; +import thb.jeanluc.adventure.save.SettingsStore; + +import java.util.List; /** - * Entry point for the console version of the game. Loads the world from - * YAML, wires up the command registry, and hands control to the - * {@link Game} loop. + * Console entry point. Loads settings, then runs the main-menu shell loop: + * New Game / Load / Settings / Quit. Each game runs the shared {@link Game} + * loop and returns here when the player saves-and-exits, reaches an ending, + * or input ends. Shared by console and GUI via {@link #run(GameIO)}. */ @Slf4j public final class App { @@ -35,31 +48,63 @@ public final class App { private App() { } - /** - * Standard JVM entry point. - * - * @param args ignored - */ public static void main(String[] args) { GameIO io = new ConsoleIO(); try { run(io); } catch (RuntimeException e) { - log.error("Fatal error during game startup", e); + log.error("Fatal error", e); io.write("Fatal error: " + e.getMessage()); System.exit(1); } } - /** - * Boots the game on the given IO channel. Reusable by both - * {@link App} (console) and {@code AppGui} (Swing worker thread). - * - * @param io the IO channel to use - */ + /** Runs the menu shell on the given IO until the player quits. */ public static void run(GameIO io) { + SaveService saves = new SaveService(); + SettingsStore settingsStore = new SettingsStore(); + Settings settings = settingsStore.load(); + ConsoleIO consoleIo = io instanceof ConsoleIO c ? c : null; + SettingsMenu.apply(settings, consoleIo); + + boolean running = true; + while (running) { + switch (MainMenu.show(io)) { + case NEW_GAME -> play(io, saves, newSession(io, saves)); + case LOAD -> { + SaveSlotInfo slot = MainMenu.pickSlot(io, saves); + if (slot != null) { + try { + play(io, saves, saves.load(slot.slug())); + } catch (SaveException e) { + io.write("Could not load: " + e.getUserMessage()); + } + } + } + case SETTINGS -> settings = SettingsMenu.show(io, settings, settingsStore, consoleIo); + case QUIT -> running = false; + } + } + io.write("Farewell."); + } + + /** Loads a fresh world and binds it to a newly named session. */ + private static GameSession newSession(GameIO io, SaveService saves) { WorldLoader.LoadResult loaded = new WorldLoader().load(); - GameContext ctx = new GameContext(loaded.world(), loaded.player(), io); + String name = MainMenu.promptName(io, "Manor"); + return new GameSession(loaded.world(), loaded.player(), name); + } + + /** Builds the registry, runs one game to completion, autosaving to its slot. */ + private static void play(GameIO io, SaveService saves, GameSession session) { + GameContext ctx = new GameContext(session, io); + ctx.setSaveCallback(() -> { + try { + saves.save(session); + } catch (SaveException e) { + log.warn("Autosave failed", e); + } + }); CommandRegistry registry = new CommandRegistry(); registry.register(new GoCommand(), "go", "move", "walk"); @@ -74,17 +119,18 @@ public final class App { registry.register(new QuestsCommand(), "quests", "log", "journal"); registry.register(new TalkCommand(), "talk", "speak"); registry.register(new GiveCommand(), "give"); - registry.register(new HelpCommand(registry), "help", "?"); - - QuitCommand quit = new QuitCommand(); - registry.register(quit, "quit", "exit"); + registry.register(new SaveCommand(saves), "save"); Game game = new Game(ctx, registry, new CommandParser()); - quit.bind(game); - io.print(Banner.welcome(loaded.world().getTitle())); - io.write(loaded.world().getWelcomeMessage()); - new LookCommand().execute(ctx, java.util.List.of()); + MenuCommand menu = new MenuCommand(saves); + menu.bind(game); + registry.register(menu, "menu", "quit", "exit"); + registry.register(new HelpCommand(registry), "help", "?"); + + io.print(Banner.welcome(session.getWorld().getTitle())); + io.write(session.getWorld().getWelcomeMessage()); + new LookCommand().execute(ctx, List.of()); game.run(); } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/QuitCommand.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/QuitCommand.java deleted file mode 100644 index 7282498..0000000 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/command/impl/QuitCommand.java +++ /dev/null @@ -1,41 +0,0 @@ -package thb.jeanluc.adventure.command.impl; - -import thb.jeanluc.adventure.command.Command; -import thb.jeanluc.adventure.game.Game; -import thb.jeanluc.adventure.game.GameContext; - -import java.util.List; - -/** - * Ends the game loop. Holds a reference to the {@link Game} so it can - * flip the loop flag. Usage: {@code quit}. - */ -public class QuitCommand implements Command { - - /** The game whose loop is to be stopped. Set via {@link #bind(Game)}. */ - private Game game; - - /** - * Binds the game instance after construction. Two-phase wiring is - * necessary because the registry is typically built before the game - * starts running. - * - * @param game the running game; must not be null - */ - public void bind(Game game) { - this.game = game; - } - - @Override - public void execute(GameContext ctx, List args) { - ctx.getIo().write("Goodbye."); - if (game != null) { - game.stop(); - } - } - - @Override - public String help() { - return "quit - exit the game (alias: exit)"; - } -} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java index 4b85ced..8b8b91d 100644 --- a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/GameTest.java @@ -1,17 +1,20 @@ package thb.jeanluc.adventure.game; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import thb.jeanluc.adventure.command.CommandParser; import thb.jeanluc.adventure.command.CommandRegistry; import thb.jeanluc.adventure.command.impl.GoCommand; import thb.jeanluc.adventure.command.impl.LookCommand; -import thb.jeanluc.adventure.command.impl.QuitCommand; +import thb.jeanluc.adventure.command.impl.MenuCommand; 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.World; +import thb.jeanluc.adventure.save.SaveService; +import java.nio.file.Path; import java.util.LinkedHashMap; import java.util.Map; @@ -19,6 +22,9 @@ import static org.assertj.core.api.Assertions.assertThat; class GameTest { + @TempDir + Path savesDir; + @Test void run_dispatchesCommands_andQuitsCleanly() { Room kitchen = new Room("kitchen", "Kitchen", "kd"); @@ -36,7 +42,7 @@ class GameTest { CommandRegistry registry = new CommandRegistry(); registry.register(new GoCommand(), "go"); registry.register(new LookCommand(), "look"); - QuitCommand quit = new QuitCommand(); + MenuCommand quit = new MenuCommand(new SaveService(savesDir)); registry.register(quit, "quit"); Game game = new Game(ctx, registry, new CommandParser()); @@ -44,7 +50,7 @@ class GameTest { game.run(); assertThat(player.getCurrentRoom()).isEqualTo(hallway); - assertThat(io.allOutput()).contains("Goodbye"); + assertThat(io.allOutput()).contains("Returning to the main menu"); } @Test @@ -55,7 +61,7 @@ class GameTest { GameContext ctx = new GameContext(new World(Map.of("r", r), Map.of(), Map.of(), "t", "w"), player, io); CommandRegistry registry = new CommandRegistry(); - QuitCommand quit = new QuitCommand(); + MenuCommand quit = new MenuCommand(new SaveService(savesDir)); registry.register(quit, "quit"); Game game = new Game(ctx, registry, new CommandParser()); quit.bind(game);