GameSim snapshots the state before each undoable command (PlacePiece / RemovePiece / MovePiece) into a bounded LinkedList stack (max 32). Undo() pops the last checkpoint and emits StateRestoredEvent, reusing the presentation rebuild path already wired for QuickLoad. Ctrl+Z in Main triggers the engine method; the harness exposes undo() for tests. QuickLoad clears the stack (fresh timeline). Seven unit tests cover empty stack, place/remove/move undo, reverse-order multiple undos, rejected commands not checkpointing, and post-simulation rewind.
107 lines
3.3 KiB
C#
107 lines
3.3 KiB
C#
using Chessistics.Engine.Commands;
|
|
using Chessistics.Engine.Events;
|
|
using Chessistics.Engine.Model;
|
|
|
|
namespace Chessistics.Engine.Simulation;
|
|
|
|
public class GameSim
|
|
{
|
|
private readonly BoardState _state;
|
|
private readonly Dictionary<int, WorldSave> _saveSlots = new();
|
|
private readonly LinkedList<WorldSave> _undoStack = new();
|
|
private const int DefaultSlot = 0;
|
|
private const int UndoStackLimit = 32;
|
|
|
|
public GameSim(LevelDef level)
|
|
{
|
|
_state = BoardState.FromLevel(level);
|
|
}
|
|
|
|
public GameSim(CampaignDef campaign)
|
|
{
|
|
_state = BoardState.FromCampaign(campaign);
|
|
}
|
|
|
|
public IReadOnlyList<IWorldEvent> ProcessCommand(IWorldCommand command)
|
|
{
|
|
WorldSave? undoCheckpoint = IsUndoable(command) ? _state.CaptureSave() : null;
|
|
|
|
var changeList = new List<IWorldEvent>();
|
|
try
|
|
{
|
|
command.Apply(_state, changeList);
|
|
}
|
|
catch (CommandRejectedException ex)
|
|
{
|
|
return [ex.RejectionEvent];
|
|
}
|
|
|
|
if (undoCheckpoint != null && ContainsMutation(changeList))
|
|
PushUndo(undoCheckpoint);
|
|
|
|
return changeList;
|
|
}
|
|
|
|
private static bool IsUndoable(IWorldCommand command) => command switch
|
|
{
|
|
PlacePieceCommand => true,
|
|
RemovePieceCommand => true,
|
|
MovePieceCommand => true,
|
|
_ => false
|
|
};
|
|
|
|
private static bool ContainsMutation(List<IWorldEvent> events) =>
|
|
events.Any(e => e is PiecePlacedEvent or PieceRemovedEvent or PieceMovedByPlayerEvent);
|
|
|
|
private void PushUndo(WorldSave save)
|
|
{
|
|
_undoStack.AddLast(save);
|
|
while (_undoStack.Count > UndoStackLimit)
|
|
_undoStack.RemoveFirst();
|
|
}
|
|
|
|
public bool CanUndo => _undoStack.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Revert the last undoable mutation (placement, removal, or move) by
|
|
/// restoring the pre-command snapshot. Emits StateRestoredEvent so the
|
|
/// presentation can rebuild visuals. Returns empty if nothing to undo.
|
|
/// </summary>
|
|
public IReadOnlyList<IWorldEvent> Undo()
|
|
{
|
|
if (_undoStack.Count == 0) return [];
|
|
var save = _undoStack.Last!.Value;
|
|
_undoStack.RemoveLast();
|
|
_state.RestoreFromSave(save);
|
|
return [new StateRestoredEvent(new BoardSnapshot(_state), null)];
|
|
}
|
|
|
|
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);
|
|
_undoStack.Clear();
|
|
return [new StateRestoredEvent(new BoardSnapshot(_state), slot)];
|
|
}
|
|
|
|
public bool HasSave(int slot = DefaultSlot) => _saveSlots.ContainsKey(slot);
|
|
}
|