Back to Blog

Sudoku Game State with Memento and Memory Storage Abstractions

May 03, 2025

Featured image for Sudoku Game State with Memento and Memory Storage Abstractions

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 I am a software architect and engineer focused on Azure, C#, and AI. I write about real-world projects, cloud-native patterns, and building smarter systems with modern tools.