Chessistics/chessistics-engine/Simulation/GameSim.cs
Samuel Bouchet 97bca7d7df Add Undo (Ctrl+Z) backed by the WorldSave checkpoint mechanism
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.
2026-04-17 22:14:06 +02:00

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);
}