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

Advent of Code - Day 6

2024/06.livemd

Advent of Code - Day 6

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

Problem

https://adventofcode.com/2024/day/6

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "6", System.fetch_env!("LB_AOC_SESSION"))
defmodule GridRender do
  def render(w, h, size, style_fn) do
    cells =
      for y <- 0..(h-1), x <- 0..(w-1), into: "" do
        {text, fill} = style_fn.({x, y})

        xoff = x * size
        yoff = y * size

        ~s"""
        
        
        #{text}
        
        """
      end
    svg = """
    
      
        text { font-weight: bold; }
      
      #{cells}
    
    """
    # IO.puts(svg)
    svg |> Kino.Image.new(:svg)
  end
  
  
end
defmodule Day6 do
  @turn_directions %{"^" => ">", ">" => "v", "v" => "<", "<" => "^"}

  
  def parse(str) do
    [{w, _}] = Regex.run(~r/\n/, str, return: :index)

    grid = str |> String.split("\n")
      |> Enum.with_index()
      |> Enum.reduce([], fn {row, idx}, acc ->
        res = row |> String.graphemes() |> Enum.with_index() |> Enum.map(fn {char, char_idx} ->
          {{char_idx, idx}, char}
        end)
        acc ++ res
      end)

    grid |> Map.new()
  end

  def is_guard(direction) when direction in ["^", ">", "v", "<"], do: true
  def is_guard(_), do: false

  def get_guard(map) do
    Enum.find(map, fn {_, char} -> is_guard(char) end)
  end

  def up(map, {x,y}) do
    get_pos(map, {x , y - 1})
  end
  
  def right(map, {x,y}) do
    get_pos(map, {x+1 , y})
  end

  def left(map, {x,y}) do
    get_pos(map, {x-1 , y})
  end
  
  def down(map, {x,y}) do
    get_pos(map, {x, y + 1})
  end

  def get_pos(map, pos) do    
    found = Map.get(map, pos)
    if found do
      {pos, found}
    else
      nil
    end
  end

  def turn_right(map, {pos, _}) do    
    Map.update!(map, pos, &amp;Map.fetch!(@turn_directions, &amp;1))
  end

  def move_guard(map, {guard_coord, guard_dir}, new_coord) do
    %{
      map | guard_coord => ".",
      new_coord => guard_dir
    }
  end

  def patrol(map) do
    {guard_pos, _} = get_guard(map)
    patrol(map, MapSet.new(), guard_pos, 0, [])
  end

  def patrol(map, acc, guard_pos, turn_count, guard_moves) do
    guard = get_pos(map, guard_pos)
    if guard &amp;&amp; turn_count < 5 do
      {guard_pos, guard_dir} = guard
      new_pos = case guard_dir do
        "^" -> up(map, guard_pos)
        ">" -> right(map, guard_pos)
        "v" -> down(map, guard_pos)
        "<" -> left(map, guard_pos)
      end
      if new_pos do
        {next_coord, next_char } = new_pos
        if next_char == "#" do
          patrol(turn_right(map, guard), acc, guard_pos, turn_count + 1, guard_moves)
        else
          patrol(move_guard(map, guard, next_coord), MapSet.put(acc, next_coord), next_coord, 0, guard_moves ++ [{next_coord, guard_dir}])
        end
      else
        {map, acc, guard_moves}
      end
    else
      {map, acc, guard_moves}
    end
    
  end

  def render_map(map) do
    {{_,h}, _} = Enum.max_by(map, fn {{_,y}, _} -> y end)
    {{_,w}, _} = Enum.max_by(map, fn {{_,x}, _} -> x end)
    
    GridRender.render(w + 1, h + 1, 40, fn pos ->
      {_, char } = get_pos(map, pos) 

      cond do 
        is_guard(char) -> { HtmlEntities.encode(char), "#f00" }
        char == "#" -> { "", "#000" }
        true -> { "", "#ccc" }
      end
    end)
  end

  def solve_part1(str) do
    {_, acc, _} = parse(str) |> patrol()

    acc |> MapSet.size()
  end

  def animate_part1(str, step_speed_ms) do
    map = parse(str)

    {guard_pos, _} = get_guard(map)

    map = Map.put(map, guard_pos, ".")
    
    {_, _, guard_moves} = parse(str) |> patrol()

    frames = Enum.map(guard_moves, fn {pos, dir} ->
      updated_map = Map.put(map, pos, dir)
      render_map(updated_map)
    end)

    Kino.animate(step_speed_ms, fn t ->
      idx = rem(t, length(frames))
      Enum.at(frames,idx)
    end)
  end
end
example_input = "....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#..."
small_map = Day6.parse("...
.<.
...")
Day6.render_map(small_map)
Day6.solve_part1(example_input)
Day6.animate_part1(example_input, 200)
Day6.solve_part1(puzzle_input)

Part 2