feat(command): save + menu commands (QuitCommand removal deferred to Task 11)
This commit is contained in:
@@ -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<String> 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)";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user