AoC 2024 Day 06: Guard Gallivant

Dated Aug 23, 2025; last modified on Sat, 23 Aug 2025

Parsing

The map shows the current position of the guard with ^ (to indicate the guard is facing up from the perspective of the map). Any obstructions - crates, desks, alchemical reactors, etc., are shown as #.

....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
using System.Collections.Immutable;

namespace AoC2024;

public partial class GuardGallivant
{
    public enum Orientation { Up, Right, Down, Left }
    public readonly record struct Coordinate(int R, int C);
    public readonly record struct Visit(Coordinate Coordinate, Orientation Orientation);
    
    public readonly (int RowCount, int ColCount, HashSet<Coordinate> Obstacles) AreaMap;
    public readonly Visit StartingPosition;

    public GuardGallivant(string filePath)
    {
        using StreamReader inputReader = new(filePath);
    
        HashSet<Coordinate> obstacles = [];
        Visit? maybeStartingPosition = null;

        string? line;
        int r = 0;
        int colsCount = 0;
        while((line = inputReader.ReadLine()) is not null)
        {
            var cols = line.ToCharArray().Index().ToList();
            cols
                .ForEach(pair => {
                    Coordinate coordinate = new(r, pair.Index);
                    if (IsObstacle(pair.Item))
                    {
                        obstacles.Add(coordinate);
                        return;
                    }

                    if (ToOrientation(pair.Item) is not Orientation orientation)
                        return;
                    
                    if (maybeStartingPosition is not null)
                        throw new ArgumentException(
                            $"Multiple starting positions detected at {maybeStartingPosition} and {(r, pair.Index, orientation)}");
                    
                    maybeStartingPosition = new Visit(coordinate, orientation);
                });
            
            if (colsCount != 0 && cols.Count != colsCount)
                throw new ArgumentException($"Illegal grid. Found differing column counts: {colsCount} vs. {cols.Count}");
            
            colsCount = cols.Count;
            r++;
        }

        if (maybeStartingPosition is not Visit startingPosition)
            throw new ArgumentException("No starting position found");

        StartingPosition = startingPosition;
        AreaMap = new(r, colsCount, obstacles);
    }

    private static bool IsObstacle(char c) => c == '#';

    private static Orientation? ToOrientation(char c) => c switch
    {
        '^' => Orientation.Up,
        '<' => Orientation.Left,
        '>' => Orientation.Right,
        'v' => Orientation.Down,
        _ => null,
    };

    private char ToCharVisualization(Coordinate coordinate)
    {
        if (AreaMap.Obstacles.Contains(coordinate))
            return '#';
        
        if (coordinate == StartingPosition.Coordinate)
        {
            return StartingPosition.Orientation switch {
                Orientation.Up => '^',
                Orientation.Left => '<',
                Orientation.Right => '>',
                Orientation.Down => 'v',
                _ => throw new ArgumentException(
                    $"Unrecognized orientation {StartingPosition.Orientation}")
            };
        }

        return '.';
    }

    private void VisualizeMapWithGuardMovement(HashSet<Visit> visits)
    {
        HashSet<Coordinate> visitedCoordinates = [.. visits.Select(v => v.Coordinate)];
        for (int r = 0; r < AreaMap.RowCount; r++)
        {
            for (int c = 0; c < AreaMap.ColCount; c++)
            {
                Coordinate coordinate = new(r, c);
                char representation = visitedCoordinates.Contains(coordinate)
                    ? 'X'
                    : ToCharVisualization(coordinate);
                Console.Write(representation);
            }
            Console.WriteLine();
        }
    }
}

Part One

The guard follows a strict patrol protocol which involves repeatedly following these steps:

  • If there is something directly in front of you, turn right 90 degrees.
  • Otherwise, take a step forward.

How many distinct positions will the guard visit before leaving the mapped area?

namespace AoC2024;

public partial class GuardGallivant
{
    public int PartOne() => SimulateGuardMovement().VisitedPositions.Count();

    private (IEnumerable<Coordinate> VisitedPositions, bool IsTrapped) SimulateGuardMovement()
    {
        HashSet<Visit> visits = [StartingPosition];
        var orientation = StartingPosition.Orientation;
        var (dr, dc) = ToVector(orientation);
        var nr = StartingPosition.Coordinate.R + dr;
        var nc = StartingPosition.Coordinate.C + dc;
        var isTrapped = false;

        while (InBounds(nr, nc))
        {
            if (AreaMap.Obstacles.Contains(new(nr, nc)))
            {
                (nr, nc) = (nr - dr, nc - dc);
                orientation = TurnRight90Degrees(orientation);
                (dr, dc) = ToVector(orientation);
                continue;
            }

            Visit visit = new(new(nr, nc), orientation);

            if (visits.Contains(visit))
            {
                isTrapped = true;
                break;
            }
            
            visits.Add(visit);

            (nr, nc) = (nr + dr, nc + dc);
        }

        // VisualizeMapWithGuardMovement(visits);

        return (
            visits.GroupBy(visit => visit.Coordinate).Select(g => g.Key),
            isTrapped);
    }

    private static (int dr, int dc) ToVector(Orientation orientation) => orientation switch {
        Orientation.Up => (-1, 0),
        Orientation.Left => (0, -1),
        Orientation.Right => (0, 1),
        Orientation.Down => (1, 0),
        _ => throw new ArgumentException($"Unrecognized input: {orientation}")
    };

    private static Orientation TurnRight90Degrees(Orientation orientation) => orientation switch {
        Orientation.Up => Orientation.Right,
        Orientation.Right => Orientation.Down,
        Orientation.Down => Orientation.Left,
        Orientation.Left => Orientation.Up,
        _ => throw new ArgumentException("Invalid direction vector")
    };

    private bool InBounds(int r, int c) =>
        r >= 0 && r < AreaMap.RowCount && c >= 0 && c < AreaMap.ColCount;
}

Part Two

How many different positions can you choose for adding a new obstruction that gets the guard stuck in a loop? The new obstruction can’t be placed at the guard’s starting position.

namespace AoC2024;

public partial class GuardGallivant
{
    public int PartTwo() {
        var (visitedPositions, isTrapped) = SimulateGuardMovement();

        return visitedPositions
            .Where(position => position != StartingPosition.Coordinate)
            .Aggregate(
                isTrapped ? 1 : 0,
                (sum, coordinate) =>
                    sum + (IsGuardTrappedByAdditionalObstacle(coordinate) ? 1 : 0));
    }

    private bool IsGuardTrappedByAdditionalObstacle(Coordinate coordinate)
    {
        AreaMap.Obstacles.Add(coordinate);
        var isTrapped = SimulateGuardMovement().IsTrapped;
        AreaMap.Obstacles.Remove(coordinate);
        return isTrapped;
    }
}

The performance of this step is noteworthy. At first, I used PositionState[,], a 2D array, to represent the area map. I solved by creating a new 2D array with a new obstacle at \((r, c)\) slotted in. This copied a lot of data around, and the test took 20s to run. moved from PositionState[,] to (int RowCount, int ColCount, HashSet<Coordinate> Obstacles) AreaMap, which is a more memory efficient representation. To solve part 2, I mutated HashSet<Coordinate> Obstacles in place, but that took 25s to run! Aha, no need to place obstacles at locations where the guard will never visit; that takes the runtime down from 25s to 3s! Hindsight is 20/20.

Reference

  1. Day 06 - Advent of Code 2024: Guard Gallivant. Eric Wastl. adventofcode.com . Accessed Aug 23, 2025.
  2. [AoC 2024] [Guard Gallivant] Use sparse representation of map ยท dchege711/programming_challenges@56c85e9. github.com . Sep 11, 2025. Accessed Sep 11, 2025.