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

Day 16

livebook/16.livemd

Day 16

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

Part 1

# input = File.read!("16/example.txt") |> String.split("\n")
input = File.read!("16/input.txt") |> String.split("\n")
defmodule U do
  def parse(input) do
    parse(input, {{0, 0}, {0, 0}, {0, 0}, %{}})
  end

  def parse([], res), do: res

  def parse([line | tail], {{_, y}, r, edge, m}) do
    {w, r, edge, m} =
      Enum.reduce(String.codepoints(line), {0, r, edge, m}, fn char, {x, r, edge, m} ->
        {m, r, edge} =
          case char do
            "." -> {m, r, edge}
            "S" -> {m, {{x, y}, :l}, edge}
            "E" -> {Map.put(m, {x, y}, :edge), r, {x, y}}
            "#" -> {Map.put(m, {x, y}, :wall), r, edge}
          end

        {x + 1, r, edge, m}
      end)

    parse(tail, {{w, y + 1}, r, edge, m})
  end

  def render({w, h}, map, front) do
    Enum.map(0..(h - 1), fn y ->
      [
        Enum.map(0..(w - 1), fn x ->
          f = Enum.find_value(front, fn 
            {{{^x, ^y}, dir}, _} -> dir 
            _ -> nil 
            end)
          o = map[{x, y}]

          if f do
            [
              :red,
              case f do
                :r -> ">"
                :d -> "v"
                :l -> "<"
                :u -> "^"
              end,
              :reset
            ]
          else
            case o do
              :wall -> "#"
              :edge -> [:green, "E", :reset]
              nil -> "."
            end
          end
        end),
        "\n"
      ]
    end)
    |> IO.ANSI.format()
    |> IO.iodata_to_binary()
    |> Kino.Text.new(terminal: true)
  end
end
{size, start, edge, map} = U.parse(input)
defmodule P1 do
  def search({pos, dir}, map, clb) do
    search(%{{pos, dir} => 0}, MapSet.new(), map, clb)
  end

  def search(front, visited, map, clb) do
    clb.(front)
    {{pos, dir} = k, cost} = cheapest(front)
    front = Map.delete(front, k)
    if map[pos] == :edge do
      cost
    else
      visited = MapSet.put(visited, k)
      front = Enum.reduce(neighbors(pos, dir, map), front, fn {k, ncost}, front ->
        if MapSet.member?(visited, k) do
          front
        else
          cost = cost + ncost
          case front[k] do
            nil -> Map.put(front, k, cost)
            in_front_cost when cost < in_front_cost -> Map.put(front, k, cost)
            _ -> front
          end
        end
      end)
      search(front, visited, map, clb)
    end
  end

  def cheapest(front) do
    [x | _ ] = Enum.sort(front, fn {_, cost1}, {_, cost2} -> cost1 <= cost2 end)
    x
  end

  def neighbors({x, y}, dir, map) do
    Enum.filter(
      [
        {{{x + 1, y}, :r}, cost(dir, :r)},
        {{{x, y + 1}, :d}, cost(dir, :d)},
        {{{x - 1, y}, :l}, cost(dir, :l)},
        {{{x, y - 1}, :u}, cost(dir, :u)}
      ],
      fn {{pos, _}, _} ->
        map[pos] != :wall
      end
    )
  end

  def cost(dir1, dir2) do
    case {dir1, dir2} do
      {x, x} -> 1
      {:r, x} when x in [:d, :u] -> 1001
      {:r, :l} -> 2001
      {:d, x} when x in [:r, :l] -> 1001
      {:d, :u} -> 2001
      {:l, x} when x in [:u, :d] -> 1001
      {:l, :r} -> 2001
      {:u, x} when x in [:l, :r] -> 1001
      {:u, :d} -> 2001
    end
  end
end
frame = Kino.Frame.new() |> Kino.render()
P1.search(start, map, fn front ->
  # Kino.Frame.render(frame, U.render(size, map, front))
  # :timer.sleep(100)
end)
defmodule P2 do
  def search({pos, dir}, map) do
    search(%{{pos, dir} => {0, [pos]}}, MapSet.new(), map, MapSet.new())
  end

  def search(front, visited, map, res) do
    {{pos, dir} = k, {cost, path}} = cheapest(front)
    front = Map.delete(front, k)
    visited = MapSet.put(visited, k)
    if map[pos] == :edge do
      (Enum.reduce(path, MapSet.new(), fn x, acc -> MapSet.put(acc, x) end) |> Enum.count())
    else
      front = Enum.reduce(P1.neighbors(pos, dir, map), front, fn {{pos, _} = k, ncost}, front ->
        if MapSet.member?(visited, k) do
          front
        else
          cost = cost + ncost
          case front[k] do
            nil -> Map.put(front, k, {cost, [pos | path]})
            {in_front_cost, _} when cost < in_front_cost -> Map.put(front, k, {cost, [pos | path]})
            {in_front_cost, in_front_path} when cost == in_front_cost -> Map.put(front, k, {cost, in_front_path ++ path})
            _ -> front
          end
        end
      end)
      search(front, visited, map, res)
    end
  end

  def cheapest(front) do
    [x | _ ] = Enum.sort(front, fn {_, {cost1, _}}, {_, {cost2, _}} -> cost1 <= cost2 end)
    x
  end
end
P2.search(start, map)