Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 06

day06.livemd

Day 06

Mix.install([
  {:kino, "~> 0.14.2"}
])

IEx.Helpers.c("/Users/johnb/dev/2024adventOfCode/advent_of_code.ex")
alias AdventOfCode, as: AOC
alias Kino.Input

# Note: when making the next template, something like this works well:
#   `cat day04.livemd | sed 's/06/04/' > day04.livemd`
#
# When inspecting lists of numbers, use "charlists: :as_lists"

Installation and Data

input_p1example = Kino.Input.textarea("Example Data", monospace: true)
input_p1puzzleInput = Kino.Input.textarea("Puzzle Input", monospace: true)
input_source_select =
  Kino.Input.select("Source", [{:example, "example"}, {:puzzle_input, "puzzle input"}])
p1data = fn ->
  (Kino.Input.read(input_source_select) == :example &&
     Kino.Input.read(input_p1example)) ||
    Kino.Input.read(input_p1puzzleInput)
end

Solution

defmodule Day06 do
  @guards ["^", ">", "v", "<"]
  @floor "."
  @obstacle "#"
  @looping_obstacle "O"
  @right_turn %{"^" => ">", ">" => "v", "v" => "<", "<" => "^"}
  @forever 1..20000 #_500_000
  
  def find_guard(grid) do
    Enum.find(AOC.grid_cells(grid), fn c -> grid[c] in @guards end)
  end

  def next_pos(grid, pos, dir) do
    case {dir, AOC.grid_x(grid, pos), AOC.grid_y(grid, pos)} do
      {"^", _, 0} -> :off_board
      {"^", _, _} -> pos - grid.grid_width
      {">", rrr, _} when rrr == (grid.grid_width - 1) -> :off_board
      {">", _, _} -> pos + 1
      {"v", _, ddd} when ddd == (grid.grid_height - 1) -> :off_board
      {"v", _, _} -> pos + grid.grid_width
      {"<", 0, _} -> :off_board
      {"<", _, _} -> pos - 1
    end
  end

  def turn_right(grid, pos, dir) do
    new_dir = @right_turn[dir]
    new_pos = next_pos(grid, pos, new_dir)
    
    {new_pos, new_dir}
  end

  def find_route(grid, position) do
    original_direction = grid[position]
    grid = Map.put(grid, position, @floor)
    path = MapSet.new([position])
    
    result = Enum.reduce_while(@forever, {position, original_direction, path}, fn _, {pos, dir, set} ->
        pos2 = next_pos(grid, pos, dir)
        {new_pos, new_dir} = turn_right(grid, pos, dir)
  
        case {pos2, grid[pos2], new_dir} 
        do
          {:off_board, _, _} -> {:halt, {grid, set}}
          {_, @floor, _} -> {:cont, {pos2, dir, MapSet.put(set, pos2)}}
          {_, @obstacle, :off_board} -> {:halt, {grid, set}}
          {_, @obstacle, _} -> {:cont, {new_pos, new_dir, MapSet.put(set, new_pos)}}
        end
      end)
    case result do
      {grid, set} -> {grid, set}
      {_position, _direction, set} -> {:huh, set}
    end
  end

  def find_route_and_mark_path(original_grid, position, block_pos) do
    Enum.reduce_while(@forever, 
      {position, original_grid[position], MapSet.new(), 
        Map.put(original_grid, block_pos, @looping_obstacle)
      },
      fn _iteration, {pos, ahead_dir, path, grid} ->
        ahead_pos = next_pos(grid, pos, ahead_dir)

        case {ahead_pos, ahead_dir, grid[ahead_pos], 
          MapSet.member?(path, {ahead_pos, ahead_dir})
        } do
          {:off_board, _, _, _} ->
            {:halt, {:off_board, path, grid}}
          {_, _, obstacle, _} when obstacle in [@obstacle, @looping_obstacle] -> 
            {:cont, {pos, @right_turn[ahead_dir], path, grid
                    # |> AOC.display_grid("73")

            }}
          {_, _, _, true} -> 
            {:halt, {:loop, path, grid |> Map.put(ahead_pos, "L")}}
          {_, _, _, _} -> 
            {:cont, {ahead_pos, ahead_dir, MapSet.put(path, {ahead_pos, ahead_dir}), 
              Map.put(grid, pos, ahead_dir)}}
        end   
      end)
  end

  def solve1(text) do
    grid = AOC.as_grid(text)
    position = find_guard(grid)
    {_result, path} = find_route(grid, position)
    Enum.count(path)
  end

  def solve2(text) do
    grid = AOC.as_grid(text)
    position = find_guard(grid)
    
    {_result, path} = find_route(grid, position)
    possible_obstacles = MapSet.to_list(path) -- [position]

    possible_obstacles
    |> Enum.reduce(0, fn blocking_spot, acc ->
      result = find_route_and_mark_path(grid, position, blocking_spot)

      case result do
        {:loop, _path, _grid} -> acc + 1
        {:off_board, _path, _grid} -> acc
        _ -> acc
      end      
    end)
  end
end

# Example:

p1data.()
|> Day06.solve1()
|> IO.inspect(label: "\n*** Part 1 solution (example: 41)")
# 4711

p1data.()
|> Day06.solve2()
|> IO.inspect(label: "\n*** Part 2 solution (example: 6)")
# 1562