Add QuickSave/QuickLoad with full state restore and visual rebuild
BoardState.CaptureSave/RestoreFromSave deep-copy every mutable field (grid, pieces, demands, transformers, buffers, stock, campaign progress) into a WorldSave slot. GameSim.QuickSave/QuickLoad expose slotted saves and emit StateSavedEvent / StateRestoredEvent — the latter carries a fresh BoardSnapshot so the presentation can rebuild board, pieces, trajectories, objectives, stock, camera, and control bar in one pass. F5/F9 trigger it in Main; harness gains quick_save/quick_load commands so UI tests can checkpoint a scenario and resume without replaying from scratch. Seven xUnit tests cover the roundtrip (including independence from post-save mutations, campaign state, and multi-slot isolation).
This commit is contained in:
parent
bd1763f372
commit
2537bfe828
12 changed files with 521 additions and 5 deletions
|
|
@ -27,6 +27,8 @@ internal class AutomationFacade
|
|||
public Action BackToMenu { get; }
|
||||
public Action<float> SetSpeed { get; }
|
||||
public Action Quit { get; }
|
||||
public Action QuickSave { get; }
|
||||
public Action QuickLoad { get; }
|
||||
|
||||
public AutomationFacade(
|
||||
Func<GameSim?> sim,
|
||||
|
|
@ -41,7 +43,9 @@ internal class AutomationFacade
|
|||
Action togglePlayPause,
|
||||
Action backToMenu,
|
||||
Action<float> setSpeed,
|
||||
Action quit)
|
||||
Action quit,
|
||||
Action quickSave,
|
||||
Action quickLoad)
|
||||
{
|
||||
Sim = sim;
|
||||
Input = input;
|
||||
|
|
@ -56,5 +60,7 @@ internal class AutomationFacade
|
|||
BackToMenu = backToMenu;
|
||||
SetSpeed = setSpeed;
|
||||
Quit = quit;
|
||||
QuickSave = quickSave;
|
||||
QuickLoad = quickLoad;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ internal class CommandDispatcher
|
|||
"set_speed" => SetSpeed(args),
|
||||
"load_mission" => LoadMission(args),
|
||||
"back_to_menu" => BackToMenu(),
|
||||
"quick_save" => QuickSave(),
|
||||
"quick_load" => QuickLoad(),
|
||||
"quit" => Quit(),
|
||||
_ => throw new InvalidOperationException($"Unknown command: {cmd}"),
|
||||
};
|
||||
|
|
@ -221,6 +223,32 @@ internal class CommandDispatcher
|
|||
return new JsonObject();
|
||||
}
|
||||
|
||||
private JsonNode? QuickSave()
|
||||
{
|
||||
_facade.QuickSave();
|
||||
var sim = _facade.Sim();
|
||||
return new JsonObject
|
||||
{
|
||||
["saved"] = sim != null,
|
||||
["turn"] = sim?.GetSnapshot().TurnNumber
|
||||
};
|
||||
}
|
||||
|
||||
private JsonNode? QuickLoad()
|
||||
{
|
||||
_facade.QuickLoad();
|
||||
var sim = _facade.Sim();
|
||||
if (sim == null) return new JsonObject { ["loaded"] = false };
|
||||
var snap = sim.GetSnapshot();
|
||||
return new JsonObject
|
||||
{
|
||||
["loaded"] = true,
|
||||
["turn"] = snap.TurnNumber,
|
||||
["phase"] = snap.Phase.ToString(),
|
||||
["missionIndex"] = snap.Campaign?.CurrentMissionIndex
|
||||
};
|
||||
}
|
||||
|
||||
private JsonNode? Quit()
|
||||
{
|
||||
// Defer so we can write the result first.
|
||||
|
|
|
|||
|
|
@ -107,7 +107,9 @@ public partial class Main : Node2D
|
|||
togglePlayPause: TogglePlayPause,
|
||||
backToMenu: HarnessBackToMenu,
|
||||
setSpeed: HarnessSetSpeed,
|
||||
quit: () => GetTree().Quit());
|
||||
quit: () => GetTree().Quit(),
|
||||
quickSave: OnQuickSave,
|
||||
quickLoad: OnQuickLoad);
|
||||
|
||||
_automationHarness = new AutomationHarness(dir, facade);
|
||||
AddChild(_automationHarness);
|
||||
|
|
@ -181,10 +183,23 @@ public partial class Main : Node2D
|
|||
_rightDragged = true;
|
||||
_camera.Position -= motion.Relative / _camera.Zoom;
|
||||
}
|
||||
else if (@event is InputEventKey key && key.Pressed && !key.Echo && key.Keycode == Key.Space)
|
||||
else if (@event is InputEventKey key && key.Pressed && !key.Echo)
|
||||
{
|
||||
TogglePlayPause();
|
||||
GetViewport().SetInputAsHandled();
|
||||
if (key.Keycode == Key.Space)
|
||||
{
|
||||
TogglePlayPause();
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
else if (key.Keycode == Key.F5)
|
||||
{
|
||||
OnQuickSave();
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
else if (key.Keycode == Key.F9)
|
||||
{
|
||||
OnQuickLoad();
|
||||
GetViewport().SetInputAsHandled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -767,6 +782,56 @@ public partial class Main : Node2D
|
|||
);
|
||||
}
|
||||
|
||||
private void OnQuickSave()
|
||||
{
|
||||
if (_sim == null) return;
|
||||
var events = _sim.QuickSave();
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt is StateSavedEvent saved)
|
||||
GD.Print($"[QuickSave] Slot {saved.SlotId} — turn {saved.TurnNumber}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuickLoad()
|
||||
{
|
||||
if (_sim == null || _campaignDef == null) return;
|
||||
if (!_sim.HasSave())
|
||||
{
|
||||
GD.Print("[QuickLoad] No save available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Halt any running simulation loop before rebuilding visuals
|
||||
_running = false;
|
||||
_simTimer.Stop();
|
||||
|
||||
var events = _sim.QuickLoad();
|
||||
foreach (var evt in events)
|
||||
{
|
||||
if (evt is StateRestoredEvent restored)
|
||||
{
|
||||
GD.Print($"[QuickLoad] Slot {restored.SlotId} — turn {restored.Snapshot.TurnNumber}");
|
||||
ApplyRestoredSnapshot(restored.Snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRestoredSnapshot(BoardSnapshot snap)
|
||||
{
|
||||
if (_campaignDef == null) return;
|
||||
|
||||
var mission = _campaignDef.Missions[snap.Campaign!.CurrentMissionIndex];
|
||||
|
||||
BuildBoardFromSnapshot(snap);
|
||||
SetupUIForMission(snap, mission);
|
||||
|
||||
CenterCameraOnBoard(snap.Width, snap.Height);
|
||||
_inputMapper.SetSnapshot(snap);
|
||||
_controlBar.UpdateTurn(snap.TurnNumber);
|
||||
_controlBar.UpdateForPhase(snap.Phase);
|
||||
}
|
||||
|
||||
private void OnMissionAdvanced()
|
||||
{
|
||||
// Auto-advance happened during simulation — rebuild board seamlessly
|
||||
|
|
|
|||
|
|
@ -33,3 +33,7 @@ public record TurnEndedEvent(int TurnNumber) : IWorldEvent;
|
|||
|
||||
// Drag & drop
|
||||
public record PieceMovedByPlayerEvent(int PieceId, Coords OldStart, Coords OldEnd, Coords NewStart, Coords NewEnd) : IWorldEvent;
|
||||
|
||||
// QuickSave/QuickLoad — presentation must rebuild all visuals from Snapshot on Restored.
|
||||
public record StateSavedEvent(int TurnNumber, int? SlotId) : IWorldEvent;
|
||||
public record StateRestoredEvent(BoardSnapshot Snapshot, int? SlotId) : IWorldEvent;
|
||||
|
|
|
|||
|
|
@ -222,6 +222,100 @@ public class BoardState
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture a deep copy of every mutable field (for QuickSave).
|
||||
/// Immutable defs and CampaignDef are shared by reference.
|
||||
/// </summary>
|
||||
public WorldSave CaptureSave()
|
||||
{
|
||||
var grid = new CellType[Width, Height];
|
||||
Array.Copy(Grid, grid, Grid.Length);
|
||||
|
||||
return new WorldSave
|
||||
{
|
||||
Width = Width,
|
||||
Height = Height,
|
||||
Grid = grid,
|
||||
Phase = Phase,
|
||||
TurnNumber = TurnNumber,
|
||||
NextPieceId = NextPieceId,
|
||||
Productions = new Dictionary<Coords, ProductionDef>(Productions),
|
||||
ProductionBuffers = new Dictionary<Coords, int>(ProductionBuffers),
|
||||
Demands = Demands.ToDictionary(kv => kv.Key, kv => kv.Value.Clone()),
|
||||
Transformers = new Dictionary<Coords, TransformerDef>(Transformers),
|
||||
TransformerInputBuffers = new Dictionary<Coords, int>(TransformerInputBuffers),
|
||||
TransformerOutputBuffers = new Dictionary<Coords, int>(TransformerOutputBuffers),
|
||||
Pieces = Pieces.Select(p => p.Clone()).ToList(),
|
||||
DestroyedPieces = DestroyedPieces.Select(p => p.Clone()).ToList(),
|
||||
RemainingStock = new Dictionary<PieceKind, int>(RemainingStock),
|
||||
OccupiedCells = new HashSet<Coords>(OccupiedCells),
|
||||
Campaign = Campaign == null ? null : new CampaignSaveData
|
||||
{
|
||||
CurrentMissionIndex = Campaign.CurrentMissionIndex,
|
||||
CompletedMissions = new List<int>(Campaign.CompletedMissions),
|
||||
AvailablePieceKinds = new HashSet<PieceKind>(Campaign.AvailablePieceKinds),
|
||||
AvailableLevels = new HashSet<PieceUpgrade>(Campaign.AvailableLevels)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore every mutable field from a save. Board dimensions can grow
|
||||
/// or shrink; the grid is fully replaced.
|
||||
/// </summary>
|
||||
public void RestoreFromSave(WorldSave save)
|
||||
{
|
||||
Width = save.Width;
|
||||
Height = save.Height;
|
||||
Grid = new CellType[Width, Height];
|
||||
Array.Copy(save.Grid, Grid, save.Grid.Length);
|
||||
|
||||
Phase = save.Phase;
|
||||
TurnNumber = save.TurnNumber;
|
||||
NextPieceId = save.NextPieceId;
|
||||
|
||||
Productions.Clear();
|
||||
foreach (var kv in save.Productions) Productions[kv.Key] = kv.Value;
|
||||
|
||||
ProductionBuffers.Clear();
|
||||
foreach (var kv in save.ProductionBuffers) ProductionBuffers[kv.Key] = kv.Value;
|
||||
|
||||
Demands.Clear();
|
||||
foreach (var kv in save.Demands) Demands[kv.Key] = kv.Value.Clone();
|
||||
|
||||
Transformers.Clear();
|
||||
foreach (var kv in save.Transformers) Transformers[kv.Key] = kv.Value;
|
||||
|
||||
TransformerInputBuffers.Clear();
|
||||
foreach (var kv in save.TransformerInputBuffers) TransformerInputBuffers[kv.Key] = kv.Value;
|
||||
|
||||
TransformerOutputBuffers.Clear();
|
||||
foreach (var kv in save.TransformerOutputBuffers) TransformerOutputBuffers[kv.Key] = kv.Value;
|
||||
|
||||
Pieces.Clear();
|
||||
foreach (var p in save.Pieces) Pieces.Add(p.Clone());
|
||||
|
||||
DestroyedPieces.Clear();
|
||||
foreach (var p in save.DestroyedPieces) DestroyedPieces.Add(p.Clone());
|
||||
|
||||
RemainingStock.Clear();
|
||||
foreach (var kv in save.RemainingStock) RemainingStock[kv.Key] = kv.Value;
|
||||
|
||||
OccupiedCells.Clear();
|
||||
foreach (var c in save.OccupiedCells) OccupiedCells.Add(c);
|
||||
|
||||
if (save.Campaign != null && Campaign != null)
|
||||
{
|
||||
Campaign.CurrentMissionIndex = save.Campaign.CurrentMissionIndex;
|
||||
Campaign.CompletedMissions.Clear();
|
||||
Campaign.CompletedMissions.AddRange(save.Campaign.CompletedMissions);
|
||||
Campaign.AvailablePieceKinds.Clear();
|
||||
foreach (var k in save.Campaign.AvailablePieceKinds) Campaign.AvailablePieceKinds.Add(k);
|
||||
Campaign.AvailableLevels.Clear();
|
||||
foreach (var u in save.Campaign.AvailableLevels) Campaign.AvailableLevels.Add(u);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearBuildingAt(Coords coords)
|
||||
{
|
||||
Productions.Remove(coords);
|
||||
|
|
|
|||
|
|
@ -19,4 +19,9 @@ public class DemandState
|
|||
public CargoType Cargo => Definition.Cargo;
|
||||
public int Required => Definition.Amount;
|
||||
public int Deadline => Definition.Deadline;
|
||||
|
||||
public DemandState Clone()
|
||||
{
|
||||
return new DemandState(Definition, MissionIndex) { ReceivedCount = ReceivedCount };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,4 +40,15 @@ public class PieceState
|
|||
EndCell = newEnd;
|
||||
CurrentCell = newStart;
|
||||
}
|
||||
|
||||
public PieceState Clone()
|
||||
{
|
||||
var clone = new PieceState(Id, Kind, StartCell, EndCell, PlacementOrder, Level)
|
||||
{
|
||||
CurrentCell = CurrentCell,
|
||||
Cargo = Cargo,
|
||||
CargoFilter = CargoFilter
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
chessistics-engine/Model/WorldSave.cs
Normal file
40
chessistics-engine/Model/WorldSave.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
namespace Chessistics.Engine.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Deep copy of every mutable field needed to restore a BoardState to an
|
||||
/// earlier point. Used by QuickSave/QuickLoad for rapid iteration during
|
||||
/// UI/UX testing and by the harness to restore checkpoints.
|
||||
///
|
||||
/// Immutable refs (defs, CampaignDef) are shared; mutable state (Pieces,
|
||||
/// DemandState, buffers, stock, campaign progression) is copied by value.
|
||||
/// </summary>
|
||||
public sealed class WorldSave
|
||||
{
|
||||
public int Width { get; init; }
|
||||
public int Height { get; init; }
|
||||
public CellType[,] Grid { get; init; } = new CellType[0, 0];
|
||||
public SimPhase Phase { get; init; }
|
||||
public int TurnNumber { get; init; }
|
||||
public int NextPieceId { get; init; }
|
||||
|
||||
public Dictionary<Coords, ProductionDef> Productions { get; init; } = new();
|
||||
public Dictionary<Coords, int> ProductionBuffers { get; init; } = new();
|
||||
public Dictionary<Coords, DemandState> Demands { get; init; } = new();
|
||||
public Dictionary<Coords, TransformerDef> Transformers { get; init; } = new();
|
||||
public Dictionary<Coords, int> TransformerInputBuffers { get; init; } = new();
|
||||
public Dictionary<Coords, int> TransformerOutputBuffers { get; init; } = new();
|
||||
public List<PieceState> Pieces { get; init; } = new();
|
||||
public List<PieceState> DestroyedPieces { get; init; } = new();
|
||||
public Dictionary<PieceKind, int> RemainingStock { get; init; } = new();
|
||||
public HashSet<Coords> OccupiedCells { get; init; } = new();
|
||||
|
||||
public CampaignSaveData? Campaign { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CampaignSaveData
|
||||
{
|
||||
public int CurrentMissionIndex { get; init; }
|
||||
public List<int> CompletedMissions { get; init; } = new();
|
||||
public HashSet<PieceKind> AvailablePieceKinds { get; init; } = new();
|
||||
public HashSet<PieceUpgrade> AvailableLevels { get; init; } = new();
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ namespace Chessistics.Engine.Simulation;
|
|||
public class GameSim
|
||||
{
|
||||
private readonly BoardState _state;
|
||||
private readonly Dictionary<int, WorldSave> _saveSlots = new();
|
||||
private const int DefaultSlot = 0;
|
||||
|
||||
public GameSim(LevelDef level)
|
||||
{
|
||||
|
|
@ -33,4 +35,30 @@ public class GameSim
|
|||
}
|
||||
|
||||
public BoardSnapshot GetSnapshot() => new(_state);
|
||||
|
||||
/// <summary>
|
||||
/// Capture a full deep-copy of the world into an in-memory slot.
|
||||
/// Returns a single StateSavedEvent — no state mutation.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IWorldEvent> QuickSave(int slot = DefaultSlot)
|
||||
{
|
||||
_saveSlots[slot] = _state.CaptureSave();
|
||||
return [new StateSavedEvent(_state.TurnNumber, slot)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore the world from a saved slot. Emits StateRestoredEvent with a
|
||||
/// fresh snapshot — the presentation layer must rebuild all visuals.
|
||||
/// Returns an empty list (no event) if the slot is empty.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IWorldEvent> QuickLoad(int slot = DefaultSlot)
|
||||
{
|
||||
if (!_saveSlots.TryGetValue(slot, out var save))
|
||||
return [];
|
||||
|
||||
_state.RestoreFromSave(save);
|
||||
return [new StateRestoredEvent(new BoardSnapshot(_state), slot)];
|
||||
}
|
||||
|
||||
public bool HasSave(int slot = DefaultSlot) => _saveSlots.ContainsKey(slot);
|
||||
}
|
||||
|
|
|
|||
181
chessistics-tests/Simulation/QuickSaveTests.cs
Normal file
181
chessistics-tests/Simulation/QuickSaveTests.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using Chessistics.Engine.Commands;
|
||||
using Chessistics.Engine.Events;
|
||||
using Chessistics.Engine.Model;
|
||||
using Chessistics.Tests.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Chessistics.Tests.Simulation;
|
||||
|
||||
public class QuickSaveTests
|
||||
{
|
||||
private SimHelper CreateSim()
|
||||
{
|
||||
var level = new BoardBuilder(4, 4)
|
||||
.WithProduction(0, 0, "Scierie", CargoType.Wood, amount: 1)
|
||||
.WithDemand(3, 0, "Depot", CargoType.Wood, 3, 30)
|
||||
.WithStock(PieceKind.Rook, 3)
|
||||
.Build();
|
||||
return SimHelper.FromLevel(level);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuickSave_ReturnsSavedEvent()
|
||||
{
|
||||
var sim = CreateSim();
|
||||
var events = sim.Sim.QuickSave();
|
||||
Assert.Single(events);
|
||||
Assert.IsType<StateSavedEvent>(events[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuickLoad_WithoutSave_ReturnsEmpty()
|
||||
{
|
||||
var sim = CreateSim();
|
||||
var events = sim.Sim.QuickLoad();
|
||||
Assert.Empty(events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuickLoad_AfterSave_EmitsRestoredEvent()
|
||||
{
|
||||
var sim = CreateSim();
|
||||
sim.Sim.QuickSave();
|
||||
var events = sim.Sim.QuickLoad();
|
||||
Assert.Single(events);
|
||||
var restored = Assert.IsType<StateRestoredEvent>(events[0]);
|
||||
Assert.NotNull(restored.Snapshot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_MutateState_Load_RestoresPreviousState()
|
||||
{
|
||||
var sim = CreateSim();
|
||||
|
||||
// Place a piece, then save
|
||||
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
|
||||
sim.Sim.QuickSave();
|
||||
|
||||
var beforeSnap = sim.Snapshot;
|
||||
Assert.Single(beforeSnap.Pieces);
|
||||
Assert.Equal(3 - 1, beforeSnap.RemainingStock[PieceKind.Rook]);
|
||||
|
||||
// Mutate: place another piece, step simulation
|
||||
sim.Place(PieceKind.Rook, (0, 1), (1, 1));
|
||||
sim.Step();
|
||||
sim.Step();
|
||||
|
||||
var dirtySnap = sim.Snapshot;
|
||||
Assert.Equal(2, dirtySnap.Pieces.Count);
|
||||
Assert.Equal(2, dirtySnap.TurnNumber);
|
||||
|
||||
// Load: should match beforeSnap
|
||||
sim.Sim.QuickLoad();
|
||||
var afterSnap = sim.Snapshot;
|
||||
|
||||
Assert.Single(afterSnap.Pieces);
|
||||
Assert.Equal(0, afterSnap.TurnNumber);
|
||||
Assert.Equal(3 - 1, afterSnap.RemainingStock[PieceKind.Rook]);
|
||||
Assert.Equal(beforeSnap.Pieces[0].Id, afterSnap.Pieces[0].Id);
|
||||
Assert.Equal(beforeSnap.Pieces[0].StartCell, afterSnap.Pieces[0].StartCell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_IsIndependent_FromFurtherChanges()
|
||||
{
|
||||
// Saving should not alias; mutating state after save must not affect the save.
|
||||
var sim = CreateSim();
|
||||
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
|
||||
sim.Sim.QuickSave();
|
||||
|
||||
// Mutate after save
|
||||
sim.Place(PieceKind.Rook, (0, 1), (1, 1));
|
||||
sim.Step();
|
||||
|
||||
// Load should restore to 1 piece, turn 0
|
||||
sim.Sim.QuickLoad();
|
||||
var snap = sim.Snapshot;
|
||||
Assert.Single(snap.Pieces);
|
||||
Assert.Equal(0, snap.TurnNumber);
|
||||
|
||||
// Mutate again, load again — save still usable
|
||||
sim.Place(PieceKind.Rook, (0, 2), (1, 2));
|
||||
sim.Sim.QuickLoad();
|
||||
snap = sim.Snapshot;
|
||||
Assert.Single(snap.Pieces);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuickSave_PreservesCampaignProgression()
|
||||
{
|
||||
var campaign = CampaignBuilder();
|
||||
var sim = SimHelper.FromCampaign(campaign);
|
||||
sim.Sim.ProcessCommand(new LoadCampaignCommand());
|
||||
|
||||
sim.Place(PieceKind.Pawn, (0, 0), (1, 0));
|
||||
sim.Sim.QuickSave();
|
||||
|
||||
var before = sim.Snapshot;
|
||||
Assert.Equal(0, before.Campaign!.CurrentMissionIndex);
|
||||
|
||||
// Mutate
|
||||
sim.Step();
|
||||
sim.Step();
|
||||
|
||||
sim.Sim.QuickLoad();
|
||||
var after = sim.Snapshot;
|
||||
Assert.Equal(before.Campaign!.CurrentMissionIndex, after.Campaign!.CurrentMissionIndex);
|
||||
Assert.Equal(before.Pieces.Count, after.Pieces.Count);
|
||||
Assert.Equal(0, after.TurnNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleSlots_AreIndependent()
|
||||
{
|
||||
var sim = CreateSim();
|
||||
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
|
||||
sim.Sim.QuickSave(slot: 1);
|
||||
|
||||
sim.Place(PieceKind.Rook, (0, 1), (1, 1));
|
||||
sim.Sim.QuickSave(slot: 2);
|
||||
|
||||
// Slot 1 should have 1 piece; slot 2 should have 2
|
||||
sim.Sim.QuickLoad(slot: 1);
|
||||
Assert.Single(sim.Snapshot.Pieces);
|
||||
|
||||
sim.Sim.QuickLoad(slot: 2);
|
||||
Assert.Equal(2, sim.Snapshot.Pieces.Count);
|
||||
}
|
||||
|
||||
private static CampaignDef CampaignBuilder()
|
||||
{
|
||||
return new CampaignDef
|
||||
{
|
||||
Name = "TestCampaign",
|
||||
InitialWidth = 4,
|
||||
InitialHeight = 4,
|
||||
Missions = new List<MissionDef>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Name = "M1",
|
||||
TerrainPatch = new TerrainPatch
|
||||
{
|
||||
NewWidth = 4,
|
||||
NewHeight = 4,
|
||||
Cells = new List<PatchCell>
|
||||
{
|
||||
new() { Col = 0, Row = 0, Type = CellType.Production,
|
||||
Production = new ProductionDef(new Coords(0, 0), "S", CargoType.Wood, 1) },
|
||||
new() { Col = 3, Row = 0, Type = CellType.Demand,
|
||||
Demand = new DemandDef(new Coords(3, 0), "D", CargoType.Wood, 3) }
|
||||
}
|
||||
},
|
||||
UnlockedPieces = new List<PieceKind> { PieceKind.Pawn },
|
||||
UnlockedLevels = new List<PieceUpgrade> { new(PieceKind.Pawn, 1) },
|
||||
Stock = new List<PieceStock> { new(PieceKind.Pawn, 4) }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -268,6 +268,12 @@ class Harness:
|
|||
def back_to_menu(self) -> dict[str, Any]:
|
||||
return self.send("back_to_menu")
|
||||
|
||||
def quick_save(self) -> dict[str, Any]:
|
||||
return self.send("quick_save")
|
||||
|
||||
def quick_load(self) -> dict[str, Any]:
|
||||
return self.send("quick_load")
|
||||
|
||||
def quit(self) -> dict[str, Any]:
|
||||
return self.send("quit", timeout=5.0)
|
||||
|
||||
|
|
|
|||
48
tools/automation/test_quicksave.py
Normal file
48
tools/automation/test_quicksave.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""End-to-end smoke test for quick save / quick load."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from tools.automation.harness import Harness
|
||||
|
||||
|
||||
def main():
|
||||
with Harness.launch(run_name="quicksave") as h:
|
||||
h.load_mission("campaign_01", 0)
|
||||
h.screenshot("01_loaded")
|
||||
|
||||
initial = h.state()
|
||||
print(f"[initial] turn={initial['turn']} pieces={len(initial['pieces'])}")
|
||||
|
||||
# Save a clean checkpoint
|
||||
saved = h.quick_save()
|
||||
print(f"[quick_save] {saved}")
|
||||
|
||||
# Mutate: place a piece and step
|
||||
h.place("Pawn", (0, 0), (0, 1))
|
||||
h.screenshot("02_after_place")
|
||||
h.set_speed(0.1)
|
||||
h.play()
|
||||
import time as _t
|
||||
_t.sleep(1.5)
|
||||
h.pause()
|
||||
|
||||
dirty = h.state()
|
||||
print(f"[dirty] turn={dirty['turn']} pieces={len(dirty['pieces'])}")
|
||||
|
||||
# Load — should be back to initial state
|
||||
loaded = h.quick_load()
|
||||
print(f"[quick_load] {loaded}")
|
||||
h.screenshot("03_after_load")
|
||||
|
||||
restored = h.state()
|
||||
print(f"[restored] turn={restored['turn']} pieces={len(restored['pieces'])}")
|
||||
|
||||
assert restored['turn'] == initial['turn'], "Turn mismatch after restore"
|
||||
assert len(restored['pieces']) == len(initial['pieces']), "Piece count mismatch"
|
||||
print("OK — quick save/load roundtrip successful")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Reference in a new issue