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

Advent of Code 2023

2023/day10.livemd

Advent of Code 2023

Mix.install([
  {:req, "~> 0.3.2"},
  {:kino, "~> 0.11.3"}
])

Day 10

input =
  "https://adventofcode.com/2023/day/10/input"
  |> Req.get!(headers: [cookie: "session=#{System.get_env("AOC_COOKIE")}"])
  |> Map.get(:body)
sample = """
7-F7-
.FJ|7
SJLL7
|F--J
LJ.LJ
"""
sample1 = """
...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
...........
"""
sample2 = """
..........
.S------7.
.|F----7|.
.||....||.
.||....||.
.|L-7F-J|.
.|..||..|.
.L--JL--J.
..........
"""
sample3 = """
.F----7F7F7F7F-7....
.|F--7||||||||FJ....
.||.FJ||||||||L7....
FJL7L7LJLJ||LJ.L-7..
L--J.L7...LJS7F-7L7.
....F-J..F7FJ|L7L7L7
....L7.F7||L7|.L7L7|
.....|FJLJ|FJ|F7|.LJ
....FJL-7.||.||||...
....L---J.LJ.LJLJ...
"""
sample4 = """
FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJ7F7FJ-
L---JF-JLJ.||-FJLJJ7
|F|F-JF---7F7-L7L|7|
|FFJF7L7F-JF7|JL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L
"""
defmodule A do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&String.split(&1, "", trim: true))
  end

  def info(rows) do
    {s_row, s_y} =
      rows
      |> Enum.with_index()
      |> Enum.find(&("S" in elem(&1, 0)))

    {_s_c, s_x} =
      s_row
      |> Enum.with_index()
      |> Enum.find(&(elem(&1, 0) == "S"))

    data =
      for {row, y} <- Enum.with_index(rows),
          {c, x} <- Enum.with_index(row),
          into: %{},
          do: {{x, y}, c}

    %{
      data: data,
      h: Enum.count(rows),
      w: Enum.count(hd(rows)),
      s: {s_x, s_y}
    }

    # |> IO.inspect()
  end

  def walk(%{s: s} = info) do
    Stream.iterate(0, &amp;(&amp;1 + 1))
    |> Enum.reduce_while({[{s, 0}], Map.new()}, fn _i, {open, visited} ->
      # if i == 30 do
      #   raise "too many iterations"
      # end

      case open do
        [cur | rest] ->
          {v, steps} = cur
          to_visit = visit(cur, visited, info)
          visited = Map.put(visited, v, steps)

          {:cont, {rest ++ to_visit, visited}}

        [] ->
          {:halt, visited}
      end
    end)
  end

  def draw_visited(visited) do
    {{w, _}, _} = Enum.max_by(visited, fn {{x, _y}, _v} -> x end)
    {{_, h}, _} = Enum.max_by(visited, fn {{_x, y}, _v} -> y end)

    padding =
      Enum.max_by(visited, fn {{_x, _y}, v} -> v end)
      |> elem(1)
      |> Integer.to_string()
      |> String.length()

    Enum.map(0..h, fn y ->
      Enum.map(0..w, fn x ->
        Map.get(visited, {x, y})
        |> case do
          nil ->
            " " |> List.duplicate(padding) |> Enum.join()

          n ->
            n
            |> Integer.to_string()
            |> String.pad_leading(padding, " ")
        end
      end)
      |> Enum.join("|")
    end)
    |> Enum.join("\n")
    |> IO.puts()

    visited
  end

  def visit({pos, step}, visited, info) do
    expand(pos)
    |> remove_lower_visited(visited, step)
    |> remove_out_of_bounds(info.w, info.h)
    |> remove_unwalkable(pos, info.data)
    |> Enum.map(&amp;{&amp;1, step + 1})

    # |> dbg()
  end

  def expand({x, y}) do
    [
      {1, 0},
      {-1, 0},
      {0, 1},
      {0, -1}
    ]
    |> Enum.map(fn {dx, dy} -> {x + dx, y + dy} end)
  end

  def remove_lower_visited(vs, visited, step) do
    vs
    |> Enum.filter(fn {x, y} ->
      case Map.get(visited, {x, y}) do
        nil -> true
        n -> n > step
      end
    end)
  end

  def remove_out_of_bounds(vs, w, h) do
    vs
    |> Enum.filter(fn {x, y} ->
      x >= 0 &amp;&amp; x < w &amp;&amp; y >= 0 &amp;&amp; y < h
    end)
  end

  def remove_unwalkable(vs, {x, y}, data) do
    s = get_symbol(data, x, y)
    dirs = symbol_to_dirs(s)

    vs
    |> Enum.filter(fn {x2, y2} ->
      s2 = get_symbol(data, x2, y2)
      dirs2 = symbol_to_dirs(s2)

      case {x2 - x, y2 - y} do
        {0, 1} -> :s in dirs &amp;&amp; :n in dirs2
        {0, -1} -> :n in dirs &amp;&amp; :s in dirs2
        {1, 0} -> :e in dirs &amp;&amp; :w in dirs2
        {-1, 0} -> :w in dirs &amp;&amp; :e in dirs2
      end
    end)
  end

  def get_symbol(data, x, y) do
    Map.fetch!(data, {x, y})
  end

  def symbol_to_dirs("|"), do: [:n, :s]
  def symbol_to_dirs("-"), do: [:e, :w]
  def symbol_to_dirs("L"), do: [:n, :e]
  def symbol_to_dirs("J"), do: [:n, :w]
  def symbol_to_dirs("7"), do: [:s, :w]
  def symbol_to_dirs("F"), do: [:s, :e]
  def symbol_to_dirs("S"), do: [:n, :s, :e, :w]
  def symbol_to_dirs(_), do: []

  def part1(input) do
    input
    |> parse()
    |> info()
    |> walk()
    |> Enum.max_by(&amp;elem(&amp;1, 1))
    |> elem(1)
  end

  def part2(input) do
    input
    |> parse()
    |> info()
    |> walk()
  end
end

Part 1

input
|> A.part1()

Part 2

zoom = fn input ->
  info =
    input
    |> A.parse()
    |> A.info()

  visited = info |> A.walk()

  # replace S
  {{x, y}, _s} = info.data |> Enum.find(fn {_k, v} -> v == "S" end)

  new_symbol =
    [
      {1, 0},
      {-1, 0},
      {0, 1},
      {0, -1}
    ]
    |> Enum.map(fn {x1, y1} -> {x1 + x, y1 + y} end)
    |> A.remove_out_of_bounds(info.w, info.h)
    |> A.remove_unwalkable({x, y}, info.data)
    |> Enum.map(fn {x1, y1} -> {x1 - x, y1 - y} end)
    |> Enum.map(fn
      {1, 0} -> :e
      {-1, 0} -> :w
      {0, 1} -> :s
      {0, -1} -> :n
    end)
    |> Enum.sort()
    |> case do
      [:e, :s] -> "F"
      [:e, :w] -> "-"
      [:n, :s] -> "|"
      [:n, :w] -> "J"
      [:s, :w] -> "7"
      [:e, :n] -> "L"
    end

  info = put_in(info, [:data, {x, y}], new_symbol)

  blocked =
    visited
    |> Map.keys()
    |> Enum.reduce(MapSet.new(), fn {x, y}, acc ->
      case Map.get(info.data, {x, y}) do
        "F" -> [{1, 1}, {1, 2}, {2, 1}]
        "J" -> [{0, 1}, {1, 1}, {1, 0}]
        "7" -> [{0, 1}, {1, 1}, {1, 2}]
        "L" -> [{1, 0}, {1, 1}, {2, 1}]
        "-" -> [{0, 1}, {1, 1}, {2, 1}]
        "|" -> [{1, 0}, {1, 1}, {1, 2}]
        _ -> []
      end
      |> Enum.map(fn {x1, y1} -> {3 * x + x1, 3 * y + y1} end)
      |> Enum.reduce(acc, fn v, acc -> MapSet.put(acc, v) end)
    end)

  {_, outside} =
    Stream.iterate(0, &amp;(&amp;1 + 1))
    # |> Enum.take(1000)
    |> Enum.reduce_while({[{0, 0}], MapSet.new()}, fn i, {open, outside} ->
      case open do
        [] ->
          {:halt, {i, outside}}

        [{cx, cy} = cur | rest] ->
          to_explore =
            [{1, 0}, {-1, 0}, {0, 1}, {0, -1}]
            |> Enum.map(fn {dx, dy} -> {cx + dx, cy + dy} end)
            |> Enum.filter(fn {x, y} ->
              x >= 0 &amp;&amp; x < info.w * 3 &amp;&amp; y >= 0 &amp;&amp; y < info.h * 3
            end)
            |> Enum.reject(fn v -> MapSet.member?(blocked, v) end)
            |> Enum.reject(fn v -> MapSet.member?(outside, v) end)

          {:cont, {(rest ++ to_explore) |> Enum.uniq(), MapSet.put(outside, cur)}}
      end
    end)

  # |> IO.inspect()

  all = for y <- 0..(info.h * 3), x <- 0..(info.w * 3), into: MapSet.new(), do: {x, y}

  inside =
    all
    |> MapSet.difference(blocked)
    |> MapSet.difference(outside)

  actual =
    inside
    |> Enum.filter(fn {x, y} -> rem(x, 3) == 1 &amp;&amp; rem(y, 3) == 1 end)
    |> Enum.into(MapSet.new())

  MapSet.size(actual) |> IO.inspect()

  for y <- 0..(info.h * 3) do
    for x <- 0..(info.w * 3) do
      cond do
        MapSet.member?(blocked, {x, y}) -> "█"
        MapSet.member?(outside, {x, y}) -> "o"
        MapSet.member?(actual, {x, y}) -> "X"
        MapSet.member?(inside, {x, y}) -> "I"
        true -> " "
      end
    end
    |> Enum.join("")
  end
  |> Enum.join("\n")
  |> then(fn s ->
    [
      "
",
      s,
      "
"
] |> Enum.join("") |> Kino.Markdown.new() end) end zoom.(input)

Draw

draw = fn input ->
  info =
    input
    |> A.parse()
    |> A.info()

  visited = info |> A.walk()

  for y <- 0..info.h do
    for x <- 0..info.w do
      if Map.has_key?(visited, {x, y}) do
        Map.get(info.data, {x, y})
      else
        "."
      end
    end
    |> Enum.join("")
  end
  |> Enum.join("\n")
  |> String.replace("F", "╔")
  |> String.replace("7", "╗")
  |> String.replace("L", "╚")
  |> String.replace("J", "╝")
  |> String.replace("-", "═")
  |> String.replace("|", "║")
  |> then(fn s ->
    [
      "
",
      s,
      "
"
] |> Enum.join("") |> Kino.Markdown.new() end) end
draw.(sample1)
draw.(sample2)
draw.(sample3)
draw.(sample4)
draw.(input)