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