Undo/redo subsystem for a board game. It takes full-board snapshots (mementos) of every card's pile, index and face state and the game state, and restores those snapshots to implement undo, redo, push, and clearing history.
using System.Collections.Generic;
using System.Linq;
namespace CardGames;
/// <summary>
/// Undo/redo for board games. A full-board snapshot ("memento") is pushed before each action; reversing
/// any action is just restoring the previous board, so we don't track what each action did. Cheap for
/// ~52 cards, and restoring re-targets cards through the normal layout so it snaps cleanly into place.
/// </summary>
public abstract partial class BoardGame
{
/// <summary>
/// One card's place + face state within a snapshot.
/// </summary>
private readonly record struct CardSnapshot( CardObject Card, Pile Pile, int Index, bool FaceUp, int FaceIndex );
/// <summary>
/// A whole-board snapshot: every card's pile/order/face, plus the game phase.
/// </summary>
private sealed class BoardMemento
{
public List<CardSnapshot> Cards { get; init; }
public GameState State { get; init; }
public string StatusText { get; init; }
}
private readonly Stack<BoardMemento> _undo = new();
private readonly Stack<BoardMemento> _redo = new();
public bool CanUndo => _undo.Count > 0;
public bool CanRedo => _redo.Count > 0;
private BoardMemento Snapshot()
{
var cards = new List<CardSnapshot>();
foreach ( var pile in Scene.GetAllComponents<Pile>() )
for ( int i = 0; i < pile.Cards.Count; i++ )
{
var c = pile.Cards[i];
cards.Add( new CardSnapshot( c, pile, i, c.FaceUp, c.FaceIndex ) );
}
return new BoardMemento { Cards = cards, State = State, StatusText = StatusText };
}
private void Restore( BoardMemento m )
{
// Rebuild every pile from the snapshot, then snap the cards home.
var piles = Scene.GetAllComponents<Pile>().ToList();
foreach ( var pile in piles )
pile.Cards.Clear();
foreach ( var snap in m.Cards.OrderBy( s => s.Index ) )
{
if ( !snap.Card.IsValid() || !snap.Pile.IsValid() ) continue;
snap.Pile.Add( snap.Card ); // re-sets the synced Pile / StackIndex
snap.Card.FaceUp = snap.FaceUp;
snap.Card.FaceIndex = snap.FaceIndex;
}
foreach ( var pile in piles )
pile.Arrange( animate: false ); // undo/redo snaps instantly - no glide or flip
State = m.State;
StatusText = m.StatusText;
}
/// <summary>
/// Host: snapshot the board before an action so it can be undone. Forks the redo branch. No-ops when
/// the game opted out via <see cref="SupportsUndo"/>.
/// </summary>
protected void PushUndo()
{
if ( !SupportsUndo ) return;
_undo.Push( Snapshot() );
_redo.Clear();
}
/// <summary>
/// Host: take back the last action.
/// </summary>
protected void Undo()
{
if ( _undo.Count == 0 ) return;
_redo.Push( Snapshot() );
Restore( _undo.Pop() );
}
/// <summary>
/// Host: re-apply the last undone action.
/// </summary>
protected void Redo()
{
if ( _redo.Count == 0 ) return;
_undo.Push( Snapshot() );
Restore( _redo.Pop() );
}
/// <summary>
/// Host: drop all undo/redo history (e.g. on a fresh deal).
/// </summary>
protected void ClearHistory()
{
_undo.Clear();
_redo.Clear();
}
}