Home

Awesome

Advent of Code Solvers Runner

Overview

This library provides helper API to write and run Advent of Code puzzles solving code for the .NET platform.

Here is the sample how one can write puzzle solver runner which will

  1. Automatically load input from the adventofcode.com site.
  2. Run solver on the parsed input.
  3. Automatically verify at the site the answer provided by solver.
  4. Run assert method to make test green or red.
[Fact(DisplayName = "Verifies that the puzzle answer is correct")]
public async Task Verifies_that_the_puzzle_answer_is_correct()
{
    var httpClient = new HttpClient();
    var sessionToken = "<your adventofcode.com session token>";

    var runner = new SiteRunner(httpClient, new Uri("https://adventofcode.com"), sessionToken);
    await runner
        .Puzzle(2021, day: 1, level: 1)
        .SolveUsing<Day01Level1Solver>()
        .HandlingResultCorrectness(Assert.True)
        .ParsingInputWith(int.Parse)
        .Run();
}

Installation

Add NuGet package AdventOfCode.Runner to your project project:

dotnet add package AdventOfCode.Runner

Solver

Solver is the type implementing AdventOfCode.IAsyncSolver<TEntry, TResult> interface.

public interface IAsyncSolver<in TEntry, TResult>
{
    ValueTask<TResult> Solve(IAsyncEnumerable<TEntry> entries);
}

The type arguments are described in the next two sections.

TEntry type parameter

TEntry is the type of the input entry which is parsed from the single line of the puzzle text input provided by site at the https://adventofcode.com/<year>/day/<day>/input page.

E.g. for the year 2021 day 1 puzzles the site provides the input similar to this:

176
184
188
142
...

It makes sense to parse each line into the int value and provide solver with int values instead of strings. So the TEntry here will be int.

TEntry is not necessary has to be the simple type like int. It can be any type that you can parse from the single line of the input and that is more appropriate for your solver code.

TResult type parameter

TResult is the type of the answer your solver is calculated. In the vast majority of the cases it will be int.

Solve method

The only method declared by the IAsyncSolver<in TEntry, TResult> interface is the Solve method. It takes the typed input passed through the argument and calculates and returns answer.

The method is designed as asynchronous (returns ValueTask<TResult>) to do not restrict you in methods of puzzle solving.

It also takes the IAsyncEnumerable<TEntry> of input entries allowing you to deal with any kind of input. Either with one buffered in the memory or with one streamed from the any source.

Sometimes it is inconvenient to work with streamed input or with asynchronous result. In this case you can add the new base class implementing IAsyncSolver and declaring more convenient Solve-like method to implement. There is an abstract class SolverWithArrayInput derived from the IAsyncSolver which you can use as a base type for your solvers when you don't want deal with asynchronous input and result.

Sample

Here is the sample of the solver implementation for the fake puzzle which requires to calculate sum of the input numbers:

public class SumSolver : IAsyncSolver<int, int>
{
    public async ValueTask<int> Solve(IAsyncEnumerable<int> entries)
    {
        return await entries.SumAsync();
    }
}

Solvers Runner concept

The API is based on the concept of a solvers runner represented by AdventOfCode.Runner<TEntry, TResult> type.

When created, the runner takes as input and encapsulates a number of services that are called as the runner is running. In this way, the runner can be considered as an orchestrator of the services passed to it.

The runner is started by the Run method:

public class Runner<TEntry, TResult>
{
...
    public async ValueTask Run() {...}
...
}

Orchestrated services

Services are passed to the runner constructor:

public class Runner<TEntry, TResult>
{
...
    public Runner(
        IAsyncEnumerable<string> inputLines,
        Func<string, int, TEntry> lineParser,
        IAsyncSolver<TEntry, TResult> solver,
        IEnumerable<IRawInputObserver> rawInputObservers,
        IEnumerable<IInputObserver<TEntry>> inputObservers,
        IEnumerable<IResultObserver<TResult>> resultObservers,
        IEnumerable<Func<ValueTask>> afterRunHandlers
        ) {...}

    public Runner(
        IAsyncEnumerable<TEntry> inputEntries,
        IAsyncSolver<TEntry, TResult> solver,
        IEnumerable<IRawInputObserver> rawInputObservers,
        IEnumerable<IInputObserver<TEntry>> inputObservers,
        IEnumerable<IResultObserver<TResult>> resultObservers,
        IEnumerable<Func<ValueTask>> afterRunHandlers
        ) {...}
...
}

The following describes the services that are passed to the runner when it is created, and that are invoked while it is running.

Input

The puzzle input is represented in the runner as an async enumerable of typed entries

IAsyncEnumerable<TEntry> inputEntries

or as an async enumerable of raw lines

IAsyncEnumerable<string> inputLines

Only one type of the input (typed or raw) can be passed to the runner. When the raw input is passed, then the line parser is required to pass also so that the typed input could be created:

Func<string, int, TEntry> lineParser

Solver

IAsyncSolver<TEntry, TResult> solver

Runner calls the solver passing the typed input entries to it and gets the calculated result from it.

Input observers

Input observers accept each input entry and handle (one of the task of the input observing is a logging).

There are raw input observers:

IEnumerable<IRawInputObserver> rawInputObservers

public interface IRawInputObserver
{
	ValueTask Observe(string line);
}

or typed input observers:

IEnumerable<IInputObserver<TEntry>> inputObservers

public interface IInputObserver<in TEntry>
{
	ValueTask Observe(string line, TEntry entry);
}

The observer will be called in an order they follow in the observers enumerable before the solver starts calculating. The raw input observers are called before each line parsing.

Result observers

Result observer is called once the solver completes calculation. Observers will be called in an order they follow in the observers enumerable and will accept the result.

IEnumerable<IResultObserver<TResult>> resultObservers

public interface IResultObserver<in TResult>
{
	ValueTask Observe(TResult result);
}

After-Run handlers

To run something at the end of the processing one can use after-run handlers:

IEnumerable<Func<ValueTask>> afterRunHandlers

Processing

Method Run starts asynchronous process of the orchestrated services executing:

public class Runner<TEntry, TResult>
{
    public async ValueTask Run() {...}
}

Providing the input services and observers one can create specific process of obtaining puzzle input and handling the calculated result.

Here is the lifecycle of the the Run execution:

            OPTION 1                        OPTION 2
______________________________     ______________________________

------------------------------     ------------------------------
| Asynchronously enumerating |     | Asynchronously enumerating |
|          raw input         |     |         typed input        |
------------------------------     ------------------------------
              |                                    |
-------------------------------                    |
| Calling raw input observers |                    |
-------------------------------                    |
              |                                    |
  ----------------------------                     |
  | Parsing each line during |                     |
  |     input enumerating    |                     |
  ----------------------------                     |
              |                                    |
--------------------------------                   |
| Calling typed input observer |                   |
--------------------------------                   |
              |____________________________________|
                                |
             -----------------------------------------
             | Passing typed input enumerable to the |
             |             solver.Solve()            |
             -----------------------------------------
                                |
                  -----------------------------
                  | Solver calculating result |
                  -----------------------------
                                |
                   ----------------------------
                   | Calling result observers |
                   ----------------------------
                                |
                  ------------------------------
                  | Calling after-run handlers |
                  ------------------------------

Runner Builder API

To simplify solver runner creation and passing services to it there is a fluent API which guides you to build runner.

Look at the following sample:


static IAsyncEnumerable<string> LoadInputLinesAsync()
{
    // loads input from somewhere...
}

...

private class Year2021Day1Level1Solver : SolverWithArrayInput<int, long>
{
    protected override long Solve(int[] entries)
    {
        return entries.Sum();
    }
}

...

// Build runner and start it
await AdventOfCode
    .SolveUsing(new Year2021Day1Level1Solver())
    .WithInput(LoadInputLinesAsync())
    .ParsingInputWith(int.Parse)
    .ObservingResultWith(result => Console.WriteLine($"Result: {result}"))
    .Run();

The fluent API of the builder which is used here is self-explained. Methods of the builder have overrides to be convenient in different scenarios.

Follow the auto-completion in your IDE to explore more building options.

Automatic solution verification

Normally to check your solution you have to deal with adventofcode.com:

The library provides extensions of the runner and it's builder to do all these communications with the site automatically.

To use it, you create the instance of the SiteRunner type and write the code similar to the following:

var siteRunner = new SiteRunner(...);

await siteRunner
    .Puzzle(2021, 1, level: 1)
    .SolveUsing<int, int>(typeof(Day1Level1Resolver))
    .HandlingResultCorrectness((result, isResultCorrect) =>
            Console.WriteLine($"Your result {result} is{(isResultCorrect ? "" : " not")} correct"))
    .ParsingInputWith(int.Parse)
    .Run();

As you can expect there are overrides and extensions over the SiteRunner API.

Creating site runner instance

To create the instance of the SiteRunner type you need to call it's constructor providing the following objects to it:

var siteRunner = new SiteRunner(
    new HttpClient(),
    new Uri("https://adventofcode.com"),
    "<your session token>"
    );

Session Token

Session token is used to authorize you on adventofcode.com site.

To get it:

session=111222...999

Do not share your token!

One of the way to keep it secret is to use .net secret manager (see, Safe storage of app secrets in development in ASP.NET Core).

HttpClient

Your can configure and create http client in any way that works for you.

You can pre-configure your http client instance to set it's base address to the site url (https://adventofcode.com) and to set cookie with session token. In this case you don't need to pass url and session token to the SiteRunner constructor.

Site Runner lifetime

You can use single instance of the SiteRunner over your application.

Further extensions

SiteRunner API can be easily extended to meet your needs.

For example, have a look at the vladsm/adventofcode-2021-dotnet repository where the solution is verifying with the site runner and makes unit test green or red (PuzzlesTestsExtensions.cs).


<br/>

Happy solving!