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) <noreply@anthropic.com>
This commit is contained in:
3
Semesterprojekt/.gitignore
vendored
3
Semesterprojekt/.gitignore
vendored
@@ -40,3 +40,6 @@ build/
|
|||||||
|
|
||||||
### Brainstorming visual companion ###
|
### Brainstorming visual companion ###
|
||||||
.superpowers/
|
.superpowers/
|
||||||
|
|
||||||
|
# Local save games and settings
|
||||||
|
saves/
|
||||||
|
|||||||
@@ -11,23 +11,36 @@ import thb.jeanluc.adventure.command.impl.HelpCommand;
|
|||||||
import thb.jeanluc.adventure.command.impl.InventoryCommand;
|
import thb.jeanluc.adventure.command.impl.InventoryCommand;
|
||||||
import thb.jeanluc.adventure.command.impl.LookCommand;
|
import thb.jeanluc.adventure.command.impl.LookCommand;
|
||||||
import thb.jeanluc.adventure.command.impl.MapCommand;
|
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.QuestsCommand;
|
||||||
import thb.jeanluc.adventure.command.impl.QuitCommand;
|
|
||||||
import thb.jeanluc.adventure.command.impl.ReadCommand;
|
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.TakeCommand;
|
||||||
import thb.jeanluc.adventure.command.impl.TalkCommand;
|
import thb.jeanluc.adventure.command.impl.TalkCommand;
|
||||||
import thb.jeanluc.adventure.command.impl.UseCommand;
|
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.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;
|
||||||
import thb.jeanluc.adventure.loader.WorldLoader;
|
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
|
* Console entry point. Loads settings, then runs the main-menu shell loop:
|
||||||
* YAML, wires up the command registry, and hands control to the
|
* New Game / Load / Settings / Quit. Each game runs the shared {@link Game}
|
||||||
* {@link Game} loop.
|
* 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
|
@Slf4j
|
||||||
public final class App {
|
public final class App {
|
||||||
@@ -35,31 +48,63 @@ public final class App {
|
|||||||
private App() {
|
private App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard JVM entry point.
|
|
||||||
*
|
|
||||||
* @param args ignored
|
|
||||||
*/
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
GameIO io = new ConsoleIO();
|
GameIO io = new ConsoleIO();
|
||||||
try {
|
try {
|
||||||
run(io);
|
run(io);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
log.error("Fatal error during game startup", e);
|
log.error("Fatal error", e);
|
||||||
io.write("Fatal error: " + e.getMessage());
|
io.write("Fatal error: " + e.getMessage());
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Runs the menu shell on the given IO until the player quits. */
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
public static void run(GameIO io) {
|
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();
|
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();
|
CommandRegistry registry = new CommandRegistry();
|
||||||
registry.register(new GoCommand(), "go", "move", "walk");
|
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 QuestsCommand(), "quests", "log", "journal");
|
||||||
registry.register(new TalkCommand(), "talk", "speak");
|
registry.register(new TalkCommand(), "talk", "speak");
|
||||||
registry.register(new GiveCommand(), "give");
|
registry.register(new GiveCommand(), "give");
|
||||||
registry.register(new HelpCommand(registry), "help", "?");
|
registry.register(new SaveCommand(saves), "save");
|
||||||
|
|
||||||
QuitCommand quit = new QuitCommand();
|
|
||||||
registry.register(quit, "quit", "exit");
|
|
||||||
|
|
||||||
Game game = new Game(ctx, registry, new CommandParser());
|
Game game = new Game(ctx, registry, new CommandParser());
|
||||||
quit.bind(game);
|
|
||||||
|
|
||||||
io.print(Banner.welcome(loaded.world().getTitle()));
|
MenuCommand menu = new MenuCommand(saves);
|
||||||
io.write(loaded.world().getWelcomeMessage());
|
menu.bind(game);
|
||||||
new LookCommand().execute(ctx, java.util.List.of());
|
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();
|
game.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String> args) {
|
|
||||||
ctx.getIo().write("Goodbye.");
|
|
||||||
if (game != null) {
|
|
||||||
game.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String help() {
|
|
||||||
return "quit - exit the game (alias: exit)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
package thb.jeanluc.adventure.game;
|
package thb.jeanluc.adventure.game;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
import thb.jeanluc.adventure.command.CommandParser;
|
import thb.jeanluc.adventure.command.CommandParser;
|
||||||
import thb.jeanluc.adventure.command.CommandRegistry;
|
import thb.jeanluc.adventure.command.CommandRegistry;
|
||||||
import thb.jeanluc.adventure.command.impl.GoCommand;
|
import thb.jeanluc.adventure.command.impl.GoCommand;
|
||||||
import thb.jeanluc.adventure.command.impl.LookCommand;
|
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.io.TestIO;
|
||||||
import thb.jeanluc.adventure.model.Direction;
|
import thb.jeanluc.adventure.model.Direction;
|
||||||
import thb.jeanluc.adventure.model.Player;
|
import thb.jeanluc.adventure.model.Player;
|
||||||
import thb.jeanluc.adventure.model.Room;
|
import thb.jeanluc.adventure.model.Room;
|
||||||
import thb.jeanluc.adventure.model.World;
|
import thb.jeanluc.adventure.model.World;
|
||||||
|
import thb.jeanluc.adventure.save.SaveService;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -19,6 +22,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class GameTest {
|
class GameTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path savesDir;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void run_dispatchesCommands_andQuitsCleanly() {
|
void run_dispatchesCommands_andQuitsCleanly() {
|
||||||
Room kitchen = new Room("kitchen", "Kitchen", "kd");
|
Room kitchen = new Room("kitchen", "Kitchen", "kd");
|
||||||
@@ -36,7 +42,7 @@ class GameTest {
|
|||||||
CommandRegistry registry = new CommandRegistry();
|
CommandRegistry registry = new CommandRegistry();
|
||||||
registry.register(new GoCommand(), "go");
|
registry.register(new GoCommand(), "go");
|
||||||
registry.register(new LookCommand(), "look");
|
registry.register(new LookCommand(), "look");
|
||||||
QuitCommand quit = new QuitCommand();
|
MenuCommand quit = new MenuCommand(new SaveService(savesDir));
|
||||||
registry.register(quit, "quit");
|
registry.register(quit, "quit");
|
||||||
|
|
||||||
Game game = new Game(ctx, registry, new CommandParser());
|
Game game = new Game(ctx, registry, new CommandParser());
|
||||||
@@ -44,7 +50,7 @@ class GameTest {
|
|||||||
game.run();
|
game.run();
|
||||||
|
|
||||||
assertThat(player.getCurrentRoom()).isEqualTo(hallway);
|
assertThat(player.getCurrentRoom()).isEqualTo(hallway);
|
||||||
assertThat(io.allOutput()).contains("Goodbye");
|
assertThat(io.allOutput()).contains("Returning to the main menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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);
|
GameContext ctx = new GameContext(new World(Map.of("r", r), Map.of(), Map.of(), "t", "w"), player, io);
|
||||||
|
|
||||||
CommandRegistry registry = new CommandRegistry();
|
CommandRegistry registry = new CommandRegistry();
|
||||||
QuitCommand quit = new QuitCommand();
|
MenuCommand quit = new MenuCommand(new SaveService(savesDir));
|
||||||
registry.register(quit, "quit");
|
registry.register(quit, "quit");
|
||||||
Game game = new Game(ctx, registry, new CommandParser());
|
Game game = new Game(ctx, registry, new CommandParser());
|
||||||
quit.bind(game);
|
quit.bind(game);
|
||||||
|
|||||||
Reference in New Issue
Block a user