= {
input const map = raw.split('\n').map(d => d.split("")) // 2D array
const [nrow, ncol] = [map.length, map[0].length] // dims
const start_index = map.flat().indexOf("^") // guard start
// use integer & mod div to find location
const [curr_row, curr_col] = [Math.floor(start_index/ncol), start_index % ncol]
const clean_map = map.slice()
= '.' // replace so it's counted as an empty spot
clean_map[curr_row][curr_col]
const input = {
start: [curr_row, curr_col],
layout: clean_map,
dir: 0, // up
dims: [nrow, ncol]
}
return input
}
2024 Day 6: Guard Gallivant
Advent of Code
Part 1
Today, it feels like we are designing a videogame! I usually hide the input parsing, though I thought today’s was notable in that I found a better way to map out a grid using integer and mod division to calculate the starting position:
Now that that is sorted, let’s proceed to part 1…
= [
possible_moves -1, 0], // up
[0, 1], // right
[1, 0], // down
[0, -1] // left
[ ]
First, since we know we are navigating around a grid, let’s define movements in the four cardinal directions.
= (input, possible_moves) => {
move_guard const pos = [] // positions
let [[row, col], dir] = [input.start, input.dir]
while (true) { // until guard leaves grid
// add current position and heading
.push([row, col, dir])
pos
// get next pos and direction
const [next_row, next_col] = [row + possible_moves[dir][0],
+ possible_moves[dir][1]]
col
// OOB check
if (next_row < 0 || next_row >= input.dims[0] ||
< 0 || next_col >= input.dims[1]) {
next_col return pos
}
// if blocked, turn right
if (input.layout[next_row][next_col] === "#") {
= (dir + 1) % 4
dir
}
// update position so it gets pushed for next iter
, col] = [row + possible_moves[dir][0],
[row+ possible_moves[dir][1]]
col
} }
Now we’re getting serious – the main bulk of the problem!
This function traces the guard’s path by running through each step in a while loop. This way, we can set the condition to break on the guard leaving the bounds of our grid. Also, since we are keeping track of the direction using an integer, we can utilize it as an index with a little creative math.
Though this function looks clean, it was not my first attempt. I originally thought that this would be a great use case for a recursive function – each call would move the guard a step forward, then call itself again (assuming it stayed in bounds). However, as soon as I scaled up to the full input, I exceeded the call stack, so I went back to the drawing board.
new Set(move_guard(input, possible_moves)
.map(d => JSON.stringify( [ d[0], d[1] ] ))).size
Once we have our function, it’s just a matter of finding the number of unique positions visited. I chose to do this with JSON.stringify()
, then creating a Set from those strings and counting the length. After the fact, I saw someone’s solution for another day that used bitwise shifts and joins to calculate uniques (which I’ll use in Part 2).
Part 1 complete!
⭐
Part 2
Uh oh, more complex now… we have to check how many discrete locations we can add an extra obstacle to in the guard’s path to send him into an infinite loop. This was daunting to start with, because anything that has to deal with checking for infinite loops seems difficult to do without launching into an infinite loop yourself. For example, if you revisit a specific location, who’s to say that your next step will continue the loop?
However, after some pondering, I realized that that is not the case here. We have very specific rules about movement, so we can say with certainty that if a position is repeated, and a guard is facing the same way as before, then a loop is happening. In fact, we can abstract it away farther, and only care about checking the turn points for overlaps, since those are the ‘decision points’ where a loop could be entered.
= (input, possible_moves) => {
check_loop const pos = new Set()
let [[row, col], dir] = [input.start, input.dir]
let turn = false
while(true) {
// if move is a turn
if (turn) {
// check if position and heading exists
const current_pos = (row << 16) | (col << 8) | dir
if (pos.has(current_pos)) {
// exit and say yes loop exists
return true
}// otherwise add and keep going
.add(current_pos)
pos
}
// get next pos and direction
const [next_row, next_col] = [row + possible_moves[dir][0],
+ possible_moves[dir][1]]
col
// OOB check
if (next_row < 0 || next_row >= input.dims[0] ||
< 0 || next_col >= input.dims[1]) {
next_col return false // no loop, exits grid
}
if (input.layout[next_row][next_col] === "#") {
= (dir + 1) % 4
dir = true
turn else {
} = next_row
row = next_col
col = false
turn
}
} }
To start, let’s modify the previous function to log all the positions as a Set, as well as tracking the heading in the position Set, as that will be critical to determining if we are in a loop.
Another change of note is we are now keeping a boolean flag turn
to keep track of whether each move is a turn or not. This way, we can say that if the guard is turning, check if the position has been reached before; otherwise, continue as is onto the next step.
If you look closely, you’ll also see the bit shifting mentioned above being used to collapse row, col, and dir down to one number.
{// get unique route positions
const all_route = move_guard(input, possible_moves)
.map(d => (d[0] << 8) | d[1] )
const unique_pos = Array.from(new Set(all_route))
// multiply by masking hex codes to shorten down the bits
.map(d => ( [(d >> 8) & 0xFF, d & 0xFF] ))
// keep track of new obstacles
const loop_counter = new Set()
// for each position on the route
.forEach(d => {
unique_pos// add obstacle
.layout[ d[0] ][ d[1] ] = "#"
input// see if loop exists
if (check_loop(input, possible_moves)) {
// add to tracker
.add( (d[0] << 8) | d[1] )
loop_counter
}// reset layout for next iter
.layout[ d[0] ][ d[1] ] = "."
input
})// return unique counts
return loop_counter.size - 1 // duped the start position
}
Unfortunately, there wasn’t many better ways to find all the loops than to try each one, adding an obstacle to a different location each time. To shrink the search space some, I decided to only try adding obstacles to the locations that the guard visited in Part 1 – after all, if he never runs into it, then it won’t be a loop since we know that he exits cleanly with the default map.
Anyways, we just need to iterate through those positions, modifying the grid and running our check_loop()
function on each; once we have the resultant positions, we just need the count of uniques.
⭐
Ramping up in difficulty for sure now!
-CH