feat(game): GameSession bundles savable state; turn + autosave hook
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user