feat(game): GameSession bundles savable state; turn + autosave hook

This commit is contained in:
2026-06-01 14:15:44 +02:00
parent d34104f928
commit 18336ecc44
4 changed files with 143 additions and 24 deletions

View File

@@ -32,9 +32,6 @@ 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;
/** Number of commands processed this session; shown in the HUD. */
private int turn = 0;
/** /**
* Runs the loop until {@link #stop()} is called or input is exhausted. * Runs the loop until {@link #stop()} is called or input is exhausted.
*/ */
@@ -55,7 +52,10 @@ public class Game {
ctx.getIo().write("I don't understand '" + parsed.verb() + "'. Type 'help'."); ctx.getIo().write("I don't understand '" + parsed.verb() + "'. Type 'help'.");
} else { } else {
cmd.get().execute(ctx, parsed.args()); cmd.get().execute(ctx, parsed.args());
turn++; ctx.getSession().incrementTurn();
if (ctx.getSession().getTurn() % 10 == 0) {
ctx.save();
}
} }
publishHud(); publishHud();
maybeEnd(); maybeEnd();
@@ -66,7 +66,7 @@ public class Game {
private void maybeEnd() { private void maybeEnd() {
Ending e = EndingEngine.triggered(ctx); Ending e = EndingEngine.triggered(ctx);
if (e != null) { if (e != null) {
ctx.getIo().print(EndingEngine.render(e, ctx, turn)); ctx.getIo().print(EndingEngine.render(e, ctx, ctx.getSession().getTurn()));
stop(); stop();
} }
} }
@@ -76,7 +76,7 @@ public class Game {
ctx.getIo().setHud(new Hud( ctx.getIo().setHud(new Hud(
ctx.getPlayer().getCurrentRoom().getName(), ctx.getPlayer().getCurrentRoom().getName(),
ctx.getPlayer().getGold(), ctx.getPlayer().getGold(),
turn, ctx.getSession().getTurn(),
Light.carryingLight(ctx))); Light.carryingLight(ctx)));
MapView map = MapLayout.compute( MapView map = MapLayout.compute(
ctx.getWorld(), ctx.getWorld(),

View File

@@ -1,32 +1,63 @@
package thb.jeanluc.adventure.game; package thb.jeanluc.adventure.game;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import thb.jeanluc.adventure.io.GameIO; import thb.jeanluc.adventure.io.GameIO;
import thb.jeanluc.adventure.model.Player; import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.World; import thb.jeanluc.adventure.model.World;
/** /**
* Bundle of everything a command needs to do its work: the world, the * Bundle of everything a command needs: the active {@link GameSession}
* player, and the IO channel. Passing this single object keeps command * (world, player, flags, quests, turn) and the IO channel. Getters delegate
* signatures small and consistent. * to the session so existing command code keeps working unchanged.
*/ */
@Getter
@RequiredArgsConstructor
public class GameContext { public class GameContext {
/** Loaded world (rooms, items, npcs). */ private final GameSession session;
private final World world;
/** The player avatar. */
private final Player player;
/** IO channel for reading input and writing output. */
private final GameIO io; private final GameIO io;
/** Mutable world flags. Created fresh per context; not a constructor arg. */ /** Silent autosave hook, wired by the app; no-op by default (tests). */
private final GameState state = new GameState(); private Runnable saveCallback = () -> { };
/** Runtime quest progress. Created fresh per context; not a constructor arg. */ public GameContext(GameSession session, GameIO io) {
private final QuestLog questLog = new QuestLog(); this.session = session;
this.io = io;
}
/** Convenience for tests/legacy callers: wraps a fresh single-use session. */
public GameContext(World world, Player player, GameIO io) {
this(new GameSession(world, player, "session"), io);
}
public GameSession getSession() {
return session;
}
public World getWorld() {
return session.getWorld();
}
public Player getPlayer() {
return session.getPlayer();
}
public GameState getState() {
return session.getState();
}
public QuestLog getQuestLog() {
return session.getQuestLog();
}
public GameIO getIo() {
return io;
}
/** Sets the silent autosave hook (called by interval/quest autosave). */
public void setSaveCallback(Runnable saveCallback) {
this.saveCallback = saveCallback;
}
/** Runs the silent autosave hook. */
public void save() {
saveCallback.run();
}
} }

View File

@@ -0,0 +1,36 @@
package thb.jeanluc.adventure.game;
import lombok.Getter;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.World;
/**
* Bundle of the mutable, savable game state for one playthrough: the
* loaded {@link World}, the {@link Player}, world {@link GameState} flags,
* {@link QuestLog} progress, the turn counter, and the bound save-slot name.
* Save/load read and write exactly this object.
*/
@Getter
public class GameSession {
private final World world;
private final Player player;
private final GameState state = new GameState();
private final QuestLog questLog = new QuestLog();
private final String slotName;
private int turn;
public GameSession(World world, Player player, String slotName) {
this.world = world;
this.player = player;
this.slotName = slotName;
}
public void setTurn(int turn) {
this.turn = turn;
}
public void incrementTurn() {
this.turn++;
}
}

View File

@@ -0,0 +1,52 @@
package thb.jeanluc.adventure.game;
import org.junit.jupiter.api.Test;
import thb.jeanluc.adventure.io.TestIO;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class GameSessionTest {
private GameSession session() {
Room k = new Room("k", "Kitchen", "d");
World w = new World(Map.of("k", k), Map.of(), Map.of(), "t", "w");
return new GameSession(w, new Player(k, 0), "slot-a");
}
@Test
void exposesStateAndIncrementsTurn() {
GameSession s = session();
assertThat(s.getSlotName()).isEqualTo("slot-a");
assertThat(s.getTurn()).isZero();
s.incrementTurn();
s.incrementTurn();
assertThat(s.getTurn()).isEqualTo(2);
s.setTurn(10);
assertThat(s.getTurn()).isEqualTo(10);
}
@Test
void contextDelegatesToSession() {
GameSession s = session();
GameContext ctx = new GameContext(s, new TestIO());
assertThat(ctx.getWorld()).isSameAs(s.getWorld());
assertThat(ctx.getPlayer()).isSameAs(s.getPlayer());
assertThat(ctx.getState()).isSameAs(s.getState());
assertThat(ctx.getQuestLog()).isSameAs(s.getQuestLog());
assertThat(ctx.getSession()).isSameAs(s);
}
@Test
void legacyConstructorBuildsFreshSession() {
Room k = new Room("k", "Kitchen", "d");
World w = new World(Map.of("k", k), Map.of(), Map.of(), "t", "w");
GameContext ctx = new GameContext(w, new Player(k, 0), new TestIO());
assertThat(ctx.getSession()).isNotNull();
assertThat(ctx.getSession().getTurn()).isZero();
}
}