View on GitHub

MSEngine ZA Ultra

A zero allocation c# minesweeper engine/solver

MSEngine ZA Ultra

My journey creating a zero allocation minesweeper engine and solver

Before I begin this series of posts documenting this project, I have a confession to make.

I have never actually played a single minesweeper game using this engine

It seems somewhat ironic to spend countless hours programming such an iconic game and never bother to manually play it. It’s really just a combination of not wanting to play from the CLI or build out a front end. If I do ever get around to it, it will definitely be with Unity though. Or maybe the canvas with React. Or Blazor. Probably best to limit the scope, since this has already taken vastly longer than originally anticipated.

What/Why is MSEngine ZA Ultra?

The initial goal of this project was just to create a simple minesweeper engine containing all the required game logic, but without the UI. I wanted to contribute my first open source project and NuGet package that someone could actually use and implement a full minesweeper game with. (Whether someone has taken on that endeavor, I don’t know). I also wanted to experiment with automated testing, since my programming career has not yet afforded that possibility.

After the initial engine was created along with a suite of tests, I figured it only made sense to take a stab at implementing an automated solver. I did not realize at the time what a rabbit hole this would take me down. To this day, I’m still not sure I understand the true scope of a probabilistic solver.

Anyway, after writing a ton of code and eventually getting bored, I needed to think of something to renew my interest in continuing this project. There are three main overlapping drivers for this project now; a matrix/linear algebra based solver, a desire for blazing performance, and learning/using SIMD/instrinsics.

A Few Notes before the Journey Begins

minesweeper bug before reveal

..and the aftermath. Note the blue node remains hidden.

minesweeper bug after reveal

Seems like an obvious bug to me. Maybe this logic is true to the original windows 3.1 version? This is something I would like to explore further. Once again, this realistically doesn’t impact normal play, but could potentially interfere with an AI/ML approach.

Show me your code

The most complicated aspect of any minesweeper engine is without a doubt the “Chain Reaction” logic. Revealing a node with zero adjacement mines will trigger this chain reaction, revealing other nodes and cascading this behavior until certain boundaries are encountered (flags/mines). I’ve refactored this snippet of code a ton already, but there is still room for improvement. It may look simple/clean now, but the initial version was an ugly mess (something I assume most people encounter).

internal static void VisitNode(Matrix<Node> matrix, int nodeIndex, ReadOnlySpan<int> visitedIndexes, ref Span<int>.Enumerator enumerator)
{
    Debug.Assert(nodeIndex >= 0);

    var pass = enumerator.MoveNext();
    Debug.Assert(pass);
    enumerator.Current = nodeIndex;

    Span<int> buffer = stackalloc int[MaxNodeEdges];
    buffer.FillAdjacentNodeIndexes(matrix.Nodes.Length, nodeIndex, matrix.ColumnCount);

    foreach (var i in buffer)
    {
        if (i == -1) { continue; }

        ref var node = ref matrix.Nodes[i];

        if (node.State == NodeState.Flagged) { continue; }

        if (node.State == NodeState.Hidden)
        {
            node = new Node(node, NodeState.Revealed);
        }

        if (node.MineCount == 0 && !visitedIndexes.Contains(i))
        {
            VisitNode(matrix, i, visitedIndexes, ref enumerator);
        }
    }
}

Ok then

I think that is enough for this first post. In the next, I want to discuss deterministic solver strategies, and why that is so difficult to code.

Link to source code on github