Powered by AppSignal & Oban Pro

Day6

2024/elixir/day6.livemd

Day6

Mix.install([
  {:kino_aoc, git: "https://github.com/ljgago/kino_aoc"}
])

Setup

{:ok, data} = KinoAOC.download_puzzle("2024", "6", System.fetch_env!("LB_AOC_SECRET"))

Solve

defmodule Day6 do
  @up {-1, 0}

  def parse(data) do
    split_with(data, "")
  end

  def split_with(str, with) do
    str
    |> String.split("\n", trim: true)
    |> Enum.map(fn r ->
      r |> String.split(with, trim: true)
    end)
  end

  def to_grid(data) do
    rm = length(data)
    cm = data |> List.first() |> length()

    g = data
      |> Enum.with_index()
      |> Enum.reduce(%{}, fn {row, ri}, g ->
        row
        |> Enum.with_index()
        |> Enum.reduce(g, fn {c, ci}, g -> Map.put(g, {ri,ci}, c) end)
      end)
    {{rm, cm}, g}
  end

  def plot(grid, {mr, mc}) do
    Enum.map(0..mr-1, fn r ->
      Enum.reduce(0..mc-1, "", fn c, acc -> acc <> grid[{r, c}] end)
    end)
    |> Enum.join("\n")
    |> IO.puts()
  end

  def get_st(g), do: g |> Enum.find(fn {_k, v} -> v == "^" end) |> elem(0)

  def cw({r, c}), do: {c, -r}
  def step({r, c}, {dr, dc}), do: {r+dr, c+dc}

  def dots(g) do
    g
    |> Enum.filter(fn {_pos, v} -> v == "." end)
    |> Enum.map(&elem(&1, 0))
  end

  def do_run(g, max, st) do
    seen = MapSet.new([{st,@up}])
    {g, seen, is_loop} = go(g, max, st, @up, seen)
    {g, seen, is_loop}
  end

  def go(g, {rm, cm}, {r, c}, _dir, seen) when r in [-1, rm] or c in [-1, cm] do
    {g, seen, :end}
  end

  def go(g, max, pos, dir, seen) do
    nxt = step(pos, dir)
    if g[nxt] == "#" do
      dir = cw(dir)
      go(g, max, pos, dir, seen)
    else
      # g = g |> Map.put(pos, "x") # for fun
      if {nxt, dir} in seen do
        {g, seen, :loop}
      else
        seen = MapSet.put(seen, {pos, dir})
        go(g, max, nxt, dir, seen)
      end
    end
  end

  def solve(data) do
    {max, g} = data |> parse() |> to_grid()
    st = get_st(g)

    {task1(g, max, st), task2(g, max, st)}
  end

  def task1(g, max, st) do
    {_g, seen, :end} = do_run(g, max, st)

    seen
    |> Enum.map(fn {pos, _d} -> pos end)
    |> Enum.uniq()
    |> Enum.count()
  end

  def task2(g, max, st) do
    g
    |> dots()
    |> Enum.reduce(0, fn dot, acc ->
      g = Map.put(g, dot, "#")
      {_g, _seen, is_loop} = do_run(g, max, st)
      if is_loop == :loop, do: acc + 1, else: acc
    end)
  end
end

tdata = """
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...
"""

Day6.solve(tdata)
# Day6.solve(data) #{5269, 1957} runs 1m