United Kingdom: +44 (0)208 088 8978

Tracking guards in Advent of Code

Who doesn't like a good puzzle? In this week's blog post, we look at an advent of code challenge, and get our hands dirty with List.unfold

We're hiring Software Developers

Click here to find out more

I'm a big fan of Advent of Code. This yearly programming challenge gives me achance to see how my problem solving skills have developed over the last year, and provides an opportunity to get my hands dirty with some lesser-used F# functions.

Day 6: Guard Gallivant

In this blog post, I want to have a look at day 6 of Advent of Code 2024, the puzzle tracking the whereabouts of a guard navigating a lab. The puzzle input is something like this:

........#..
.#.....#...
#....#.....
..#...#....
#....^....#
.#...#.....

The guard is indicated by the ^, while the # are obstructions. The guard initially starts moving north, and will rotate 90 degrees clockwise every time he encounters an obstacle.

The initial problem is: how many squares will the guard cover before leaving the grid?

Building the basis

Since we love domain modelling, let's establish our domain; the map is a bool array array, where true indicates an obstacle:

type Direction =
    | North
    | East
    | South
    | West

type GuardState =
    { Position: int * int
      Direction: Direction }

type Map = bool array array

Then the boring bit, parsing our data into our domain:

let parse (data: string array): (Map * GuardState)=
    let map =
        data
        |> Array.map (
            Seq.map (function
                | '#' -> true
                | _ -> false)
            >> Array.ofSeq
        )

    let guardState =
        let position =
            let row = data |> Array.findIndex (Seq.contains '^')
            let col = data[row] |> Seq.findIndex ((=) '^')
            row, col

        { Position = position
          Direction = North }

    (map, guardState)

We are ready to start solving!

Moving Around

In order to answer the question of how many squares the guard touches before he leaves the grid, let's model how the guard moves around.

Let's add a function move, that returns a new Guard in the new position:

let move map guard =
    let newPos =
        let x, y = guard.Position

        match guard.Direction with
        | North -> (x - 1), y
        | East -> x, (y + 1)
        | South -> (x + 1), y
        | West -> x, (y - 1)

    { guard with Position = newPos }

Very naive! What if we hit an obstacle? Or leave the grid!? Let's add a function that checks for both of these cases. if the location is out of bounds, we return None. Otherwise we return whether the position is blocked:

module Map =
    let tryIsBlocked (map: bool array array) (x, y) =
        if x >= 0 && x < map.Length && y >= 0 && y < map[0].Length then
            Some(map[x][y])
        else
            None

We add this check to our move function, which now becomes tryMove; if we leave the map we return None.

let tryMove map guard =
        let newPos =
            let x, y = guard.Position

            match guard.Direction with
            | North -> (x - 1), y
            | East -> x, (y + 1)
            | South -> (x + 1), y
            | West -> x, (y - 1)

        Map.tryIsBlocked map newPos
        |> Option.map(
            failWith "implement how we move based on the state of position we are facing"
        )

The next part is easy; if position we are facing is blocked, rotate. Otherwise, move into that square.

module Direction =
    let rotate =
        function
        | North -> East
        | East -> South
        | South -> West
        | West -> North

module Guard =
    let tryMove map guard =
        ...

        Map.tryIsBlocked map newPos
        |> Option.map (fun nextPosIsBlocked ->
            if nextPosIsBlocked then
                { guard with
                    Direction = Direction.rotate guard.Direction }
            else
                { guard with Position = newPos })

Tying it all together

That's all good, but how do we figure out how many positions we covered based on this?

We have:

  • A map we can navigate
  • A starting position
  • A function that can tell us where we go, based on a position

We want:

  • The number of squares we touched. It's probably easiest to create a list of positions, and then count those

How do we use that function to convert the state into a list? Presenting List.unfold!

Intermission: List.(un)fold

His sister List.fold is slightly better known; she transforms a collection into an aggregate given a starting state:

(folder: 'State -> 'T -> 'State) -> (initialState: 'State) -> (list: 'T list) -> 'State 

Unfold does the exact opposite: Given a known state and a generator, it can produce a list.

The generator takes a state, and returns a list item, as well as a new state, so the generator can be called on that again. It returns an option, so we can return None to indicate we can not generate more values.

(generator: 'State -> ('T * 'State) option) -> (state: 'State) -> `T list

Back to that pesky guard

So how do we apply List.unfold to our problem? We actually got really close to writing a generator already! Our Guard.tryMove has the following signature:

(map: bool array array) -> (guard: Guard) -> Guard option

We can partially apply map, because it will not change. We are left with a function of Guard -> Guard option. The generator should not just return the new state, but also something to add to the list. Let's map the result of our tryMove function to do that:

Guard.tryMove map >> Option.map(fun s -> (s.Position, s))

We can now pass our complete generator into List.unfold to get a list of all positions we visited:

let solve map guardState =
    let (positions: (int*int list)) =
        List.unfold (Guard.tryMove map >> Option.map(fun s -> (s.Position, s))) guardState

We're almost there! We might have visited a position twice, so let's filter those out before writing our solution to the console:

let solve map guardState =
    let (positions: (int*int list)) =
        List.unfold (Guard.tryMove map >> Option.map(fun s -> (s.Position, s))) guardState

    printfn $"The guard visited {positions |> List.distinct |> List.length} locations"

And there you have it!

I had a lot of fun solving this puzzle; it's a very clear problem, but figuring out how to solve it was definitely a bit tricky. Before I started solving this solution, I did not know about List.unfold, but once I wrote the function to model the movement, a quick look at the F# List module handed me exactly what I needed to tie it all together!

Check out my full solution to this and other Advent of Code problems on GitHub.