Sudoku Game State with Memento and Memory Storage Abstractions

May 03, 2025

One of the challenges I faced while developing a Sudoku game was managing undoable, persistent game state in a clean, extensible way. This blog post walks through how I designed my game state system using the Memento Pattern, clean abstractions, and dual in-memory and Azure Blob-based implementations.

memento design pattern

The Goal

I needed to:

  • Track the game board state as players made moves.
  • Provide undo functionality.
  • Support both ephemeral (in-memory) and durable (Azure Blob) state storage.
  • Keep the architecture clean and testable.

GameStateMemento

At the core of the architecture is the GameStateMemento class — a snapshot of the game state at a point in time:

public class GameStateMemento
{
    public GameStateMemento(string puzzleId, Cell[] board, int score)
    {
        PuzzleId = puzzleId;
        Board = board;
        Score = score;
        LastUpdated = DateTime.UtcNow;
    }

    public string PuzzleId { get; init; }
    public Cell[] Board { get; private set; }
    public int Score { get; private set; }
    public DateTime LastUpdated { get; init; }
}

It represents the full state needed to restore a puzzle.


IGameStateManager Interface

This abstraction allows my app to plug in different backing stores without changing how game logic interacts with state:

public interface IGameStateManager
{
    GameStateMemoryType MemoryType { get; }
    Task DeleteAsync(string puzzleId);
    Task<GameStateMemento?> LoadAsync(string puzzleId);
    Task SaveAsync(GameStateMemento gameState);
    Task<GameStateMemento?> UndoAsync(string puzzleId);
}

This lets me easily swap between in-memory and persistent implementations.


In-Memory Storage Implementation with Circular Undo

The in-memory game state manager uses a circular stack to manage the game state. This class is used mostly for generating new puzzles, but could easily be used for offline game play.

public class InMemoryGameStateManager : IGameStateManager
{
	private readonly CircularStack<GameStateMemory> _gameState = new(50);

    public GameStateMemoryType MemoryType => GameStateMemoryType.InMemory;

    public Task DeleteAsync(string puzzleId)
    {
        ... // code hidden for brevity
    }

    public Task<GameStateMemory> LoadAsync(string puzzleId)
    {
        ...
    }

    public Task SaveAsync(GameStateMemory gameState)
    {
        ...
    }

    public Task<GameStateMemory> UndoAsync(string puzzleId)
    {
        ...
    }
}

To support multiple undos while avoiding memory bloat, I used a bounded circular stack:

public class CircularStack<T>
{
    private readonly T[] _buffer;
    private int _top;

    public int Capacity { get; }
    public int Count { get; private set; }

    public CircularStack(int capacity)
    {
        Capacity = capacity;
        _buffer = new T[capacity];
        _top = -1;
    }

    public void Push(T item)
    {
        _top = (_top + 1) % Capacity;
        _buffer[_top] = item;
        if (Count < Capacity) Count++;
    }

    public T Pop()
    {
        var item = _buffer[_top];
        _top = (_top - 1 + Capacity) % Capacity;
        Count--;
        return item;
    }

    public T Peek() => _buffer[_top];
    public void Clear() => Count = 0;
}

This keeps the last N (e.g., 50) game states available for undo, using a fixed-size buffer.


Persistent Storage

So I chose Azure Blob storage over Redis cache mostly for costs. Since this is just a demo app, I can tolerate slower storage performance in exchange for lower cost. If your application requires lower-latency persistence, Redis may be a better fit. Should your needs change, Redis can easily be swapped in for Azure Blob storage via the shared IGameStateManager interface.

public class AzureStorageGameStateManager(IStorageService storageService) : IGameStateManager, IDisposable
{
    public GameStateMemoryType MemoryType => GameStateMemoryType.AzureBlobPersistence;

    ... // code hidden for brevity
}

Undo is handled by deleting the latest blob and loading the previous one. A semaphore ensures safe concurrent access in multi-threaded environments.

public async Task<GameStateMemory> UndoAsync(string puzzleId)
{
    await _semaphore.WaitAsync();

    try
    {
        var blobList = await GetSortedBlobNamesAsync(puzzleId);

        if (blobList.Count == 0)
        {
            return null;
        }

        var latestBlobName = blobList.Last();
        await storageService.DeleteAsync(ContainerName, latestBlobName);

        blobList.RemoveAt(blobList.Count - 1);

        if (blobList.Count == 0)
        {
            return null;
        }

        var previousBlobName = blobList.Last();

        return await storageService.LoadAsync<GameStateMemory>(ContainerName, previousBlobName);
    }
    finally
    {
        _semaphore.Release();
    }
}

TrimHistoryIfNeededAsync ensures that the number of states doesn't go beyond a given threshold. This just ensures the persistent storage behaves the same as the in-memory storage.

private async Task TrimHistoryIfNeededAsync(string puzzleId)
{
    var blobs = await GetSortedBlobNamesAsync(puzzleId);
    if (blobs.Count > MaxUndoHistory)
    {
        var excess = blobs.Count - MaxUndoHistory;
        var oldest = blobs.Take(excess);

        foreach (var blobName in oldest)
        {
            await storageService.DeleteAsync(ContainerName, blobName);
        }
    }
}

Switching Memory Types

Because of the shared IGameStateManager interface, components like my puzzle solver or game controller can simply ask for the memory type they want:

services.AddSingleton<Func<string, IGameStateManager>>(sp => key =>
{
    return key switch
    {
        GameStateTypes.InMemory => sp.GetRequiredService<InMemoryGameStateManager>(),
        GameStateTypes.AzurePersistent => sp.GetRequiredService<AzureStorageGameStateManager>(),
        _ => throw new ArgumentException($"Unknown game state memory type: {key}")
    };
});
public class PuzzleSolver(IEnumerable<SolverStrategy> strategies, Func<string, IGameStateManager> gameStateMemoryFactory) : IPuzzleSolver
{
    private readonly IGameStateManager _gameStateMemory = gameStateMemoryFactory(GameStateTypes.InMemory);

    ... // code hidden for brevity
}

Using DI and named instances, I can route the correct implementation based on the situation (generating a puzzle vs. playing a game).


Why This Design Works

Undo Support: Both memory types support it via versioning or stack.

Swappable: Storage is abstracted away, enabling testing and expansion (e.g., LiteDB, SQLite).

Performant: In-memory is fast, while Blob is scalable for cloud users.

Extendable: Future ideas like auto-saving, time-travel debugging, or multiplayer replay become possible.


Next Steps

I plan to:

  • Add encryption to blob-stored states.
  • Track more metadata (e.g., elapsed time, move history).

If you're building games, puzzle engines, or any app requiring time-travel debugging or undoable state, consider combining the Memento pattern with bounded stacks and cloud storage abstractions like this.


Have you built a system with undoable state or used similar design patterns? I’d love to hear how you approached it. Drop a comment or send me your thoughts — especially if you have ideas for improving this architecture further.


Profile picture

Written by Shannon Stewart residing in beautify Colorado, building projects with code and some with wood