Outside Context Problem
MonoGame

As I'm writing this blog after the fact, rather than during the puzzle solving process, I'm going to jump around between topics that come to my head when I have time to write a post.

In this one, I'd like to cover how I came to use MonoGame for visualisations.

As previously mentioned, I was dissatisfied with all the conditional compilation in my code for visualising tricky problems. Also, the console driven visualisations could be incredibly slow.

What finally made me change my approach was 2018.13, a fun puzzle with an Indiana Jones vibe. The size of the map was 150 × 150, just unrealistically doable in the console.

I'm sure there are many options for visualising data, creating animations etc... but I went with MonoGame because I had been playing about with it in my spare time prior to discovering Advent of Code.

I'd been messing around with creation of an isometric engine using it (using Perlin noise for terrain generation), and also completed a friend's space invaders challenge with it. I also just seemed to gel with the framework, frankly.

I also thought it would be fun to draw the tracks and carts and watch them play out the scenario. However, doing the maths, even on my QHD monitors, that map size only left 9.6 pixels per map tile. As there were so many actors present at different points on the map, scrolling wasn't really a viable option. The input also kind of put me in mind of a circuit board so I changed the tracks to wires and the carts to electrical sparks.

You can see the results here:

I won't go into how I solved this puzzle right now, but I want to talk about how I went about using MonoGame for visualisations without being too intrusive on the puzzle solving code.

As an aside, if you watch the videos in chronological order, you'll notice I try to make them more interesting by adding YouTube's royalty-free music over the top. And then as I get more used to using YouTube studio, how I try to match the timing of the music to what is happening in the visualisation.

The solution I settled on is as follows. I created an IVisualiser interface which the visualiser implements. This is designed to allow the puzzle to report to the visualiser when a notable state change that should be rendered is reached.

public interface IVisualiser<in T>
{
    void PuzzleStateChanged(T state);
}

A puzzle class that wants visualisation just needs to expose a constructor that takes an IVisualiser.

protected Base(IVisualiser<PuzzleState> visualiser)
{
    _visualiser = visualiser;
}

PuzzleState is a class implemented per puzzle to represent the visualisable elements of the puzzle. What the visualiser does with this is up to it.

The puzzle class can then simply has a method it calls when the state has changed. If there is no visualiser present, nothing happens, so the only cost is a null check. This will vary from puzzle to puzzle, but looks like this in 2018.13.

protected void Visualise(Point collisionPoint = null, bool isFinalState = false)
{
    if (_visualiser != null)
    {
        _visualiser.PuzzleStateChanged(new PuzzleState 
                                           { 
                                               Map = _map, 
                                               Carts = Carts.Select(c => new Cart(c)).ToList(), 
                                               CollisionPoint = collisionPoint, 
                                               IsFinalState = isFinalState 
                                           });
    }
}

That's it from the puzzle's perspective. Much cleaner than the plethora of conditional compilation sections I was using.

On to the visualiser...

There is a base class that all visualisations will inherit from.

public abstract class VisualisationBase<T> : Game, IVisualiser<T>, IMultiPartVisualiser
{
    protected bool HasNextState => _stateQueue.Count > 0;

    private readonly Queue<T> _stateQueue = new();

    protected Solution Puzzle { get; set; }

    private Task _puzzleTask;

    private readonly CancellationTokenSource _cancellationTokenSource = new();

    public abstract void SetPart(int part);

    protected override void Initialize()
    {
        _puzzleTask = new Task(() => Puzzle.GetAnswer(), _cancellationTokenSource.Token);

        _puzzleTask.Start();

        base.Initialize();
    }

    protected override void OnExiting(object sender, EventArgs args)
    {
        _cancellationTokenSource.Cancel();

        base.OnExiting(sender, args);
    }

    public void PuzzleStateChanged(T state)
    {
        if (_stateQueue.Count > 1000)
        {
            Thread.Sleep(1000);
        }

        _stateQueue.Enqueue(state);
    }

    protected T GetNextState()
    {
        return _stateQueue.Dequeue();
    }
}

IMultiPartVisualiser and SetPart are just to deal with the fact each puzzle has 2 parts.

The rest is really about managing the state.

The basic architecture is that the puzzle is kicked off in its own thread and will add items to the state queue as it deems necessary. The visualiser can pull items off this queue when it needs them. In some cases this will be every frame, in others it may wait for an animation to finish before doing so.

I can't quite remember why I had to add the throttling in PuzzleStateChanged as with modern hardware and memory space, I can't see the queue filling up, but I put the code there so there must have been a reason. Maybe one particular puzzle just provided too many state changes while the visualisation was catching up.

Even though this isn't a game as such, I'm using MonoGame. At a basic level, a game program is the following in an infinite loop:

Process input is irrelevant to these visualisations, so we are left with the other two.

Update game object state pulls an item off the queue if required (i.e. any non-key-frame animations are complete), and render view is Ronseal.

That's it really. I found MonoGame really easy to work with regarding sprite manipulation etc... I'm firmly old-school and happy working with graphics in 2D planes. I'm sure MonoGame is also great for 3D, but that's a learning journey for another time.

On a personal note, my favourite visualisations are:

And I find this non-MonoGame based one quite therapeutic.

All source available here.