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

Day15

2024/elixir/day15.livemd

Day15

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

Setup

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

Helpers

defmodule Aoc.Grid do
  @type aoc_grid :: %{grid: map(), mx: non_neg_integer(), my: non_neg_integer()}
  @type data :: String.t()

  @up {-1, 0}
  @down {1, 0}
  @left {0, -1}
  @right {0, 1}
  @moves [@up, @down, @left, @right]
  @dirs %{"^" => @up, "v" => @down, "<" => @left, ">" => @right}

  @spec parse(data()) :: aoc_grid()
  def parse(data), do: parse(data, &amp;(&amp;1))

  @spec parse(data(), fun()) :: aoc_grid()
  def parse(data, fun) do
    raw =
      data
      |> String.split("\n", trim: true)
      |> Enum.map(&amp;String.graphemes/1)

    my = length(raw)
    mx = raw |> hd() |> length()

    grid =
      for {row, y} <- Enum.with_index(raw),
          {sym, x} <- Enum.with_index(row),
          into: %{} do
        {{y, x}, fun.(sym)}
      end

    %{grid: grid, mx: mx - 1, my: my - 1}
  end

  def in_grid?({y, x}, aoc) do
    y >= 0 and y <= aoc.my and x >= 0 and x <= aoc.mx
  end

  def can_move?({y, x}, aoc) do
    in_grid?({y, x}, aoc) and aoc.grid[{y, x}] != "#"
  end

  def next_moves({y, x}, aoc) do
    @moves
    |> Enum.filter(fn {dy, dx} -> in_grid?({y + dy, x + dx}, aoc) end)
    |> Enum.map(fn {dy, dx} -> {y + dy, x + dx} end)
  end

  def all_moves({y, x}) do
    Enum.map(@moves, fn {dy, dx} -> {y + dy, x + dx} end)
  end

  def moves, do: @moves
  def dirs, do: @dirs

  def cw({r, c}), do: {c, -r}
  def ccw({r, c}), do: {-c, r}
  def fwd({y, x}, {dy, dx}), do: {y + dy, x + dx}

  def plot(aoc) do
    Enum.map(0..aoc.my, fn y ->
      Enum.reduce(0..aoc.mx, "", fn x, acc -> acc <> aoc.grid[{y, x}] end)
    end)
    |> Enum.join("\n")
    |> IO.puts()
  end
end

Solve

defmodule Day15 do
  alias Aoc.Grid, as: G
  @dup %{"#" => "##", "O" => "[]", "." => "..", "@" => "@."}

  def parse(data, dup \\ false) do
    [grid, cmds] = data |> String.trim() |> String.split("\n\n", trim: true)

    grid = if dup do
      String.replace(grid, ~w(# O . @), fn char -> @dup[char] end)
    else
      grid
    end

    do_parse(grid, cmds)
  end

  def do_parse(grid, cmds) do
    aoc = G.parse(grid)
    cmds = cmds
      |> String.split("\n", trim: true)
      |> Enum.map(fn row -> String.split(row, "", trim: true) end)
      |> List.flatten()
    {aoc, cmds}
  end

  def solve(data, opts \\ []) do
    verbose = Keyword.get(opts, :verbose, false)
    dup = Keyword.get(opts, :dup, false)

    {aoc, cmds} = parse(data, dup)

    verbose &amp;&amp; G.plot(aoc)

    {r, "@"} = find(aoc, "@")
    mfun = if dup, do: &amp;move2/3, else: &amp;move1/3

    {aoc, _r} = mfun.(cmds, r, aoc)

    verbose &amp;&amp; G.plot(aoc)
    calc_coord(aoc, dup)
  end

  def calc_coord(aoc, dup) do
    calc_sym = dup &amp;&amp; "[" || "O"
    Enum.reduce(aoc.grid, 0, fn
      {{y, x}, ^calc_sym}, acc ->
        (100 * y + x) + acc
      _, acc ->
        acc
    end)
  end

  def move2([], r, aoc), do: {aoc, r}

  def move2([cmd | t], r, aoc) do
    dir = G.dirs[cmd]

    case can_move2(MapSet.new([r]), dir, aoc, true) do
      {_, false} ->
        move2(t, r, aoc)

      {items, true} ->
        old_g = aoc.grid
        nr = G.fwd(r, dir)

        # clean old
        clean = Enum.reduce(items, aoc, fn pos, aoc ->
          put_in(aoc, [:grid, pos], ".")
        end)

        # write new
        new = Enum.reduce(items, clean, fn pos, aoc ->
          nxt = G.fwd(pos, dir)
          put_in(aoc, [:grid, nxt], old_g[pos])
        end)

        move2(t, nr, new)
    end
  end

  def can_move2(items, dir, aoc, check) do
    {acc, check} =
      Enum.reduce(items, {MapSet.new(), check}, fn pos, {acc, check} ->
        nxt = G.fwd(pos, dir)
        case aoc.grid[nxt] do
          "#" -> {MapSet.put(acc, pos), false}
          "." -> {MapSet.put(acc, pos), check}

          "]" -> # + [
            l = G.fwd(nxt, {0, -1})
            acc = acc |> MapSet.put(pos) |> MapSet.put(nxt) |> MapSet.put(l)
            {acc, check}

          "[" -> # + ]
            r = G.fwd(nxt, {0, 1})
            acc = acc |> MapSet.put(pos) |> MapSet.put(nxt) |> MapSet.put(r)
            {acc, check}
        end
      end)

    if MapSet.size(items) == MapSet.size(acc) or !check do
      {acc, check}
    else
      can_move2(acc, dir, aoc, check)
    end
  end

  def move1([], r, aoc), do: {aoc, r}

  def move1([cmd | t], r, aoc) do
    dir = G.dirs[cmd]

    case can_move([r], dir, aoc) do
      {false, _} -> move1(t, r, aoc)
      {true, acc} ->
        [r | boxes] = Enum.reverse(acc)
        nr = G.fwd(r, dir)
        aoc =
          Enum.reduce(boxes, aoc, fn pos, aoc ->
            nxt = G.fwd(pos, dir)
            put_in(aoc, [:grid, nxt], "O")
          end)
          |> put_in([:grid, nr], "@")
          |> put_in([:grid, r], ".")

        move1(t, nr, aoc)
    end
  end

  def can_move(acc, dir, aoc) do
    [h | _t] = acc
    nxt = G.fwd(h, dir)
    case aoc.grid[nxt] do
      "." -> {true, acc}
      "#" -> {false, acc}
      "O" -> can_move([nxt | acc], dir, aoc)
    end
  end

  def find(%{grid: g}, el) do
    Enum.find(g, fn {_p, k} -> k == el end)
  end
end

t2 = """
##########
#..O..O.O#
#......O.#
#.OO..O.O#
#..O@..O.#
#O#..O...#
#O..O..O.#
#.OO.O.OO#
#....O...#
##########

^v>^vv^v>v<>v^v<<><>>v^v^>^<<<><^
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<^<^^>>>^<>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^v^^<^^vv<
<>^^^^>>>v^<>vvv^>^^^vv^^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
^><^><>>><>^^<<^^v>>><^^>v>>>^v><>^v><<<>vvvv>^<><<>^><
^>><>^v<><^vvv<^^<><^v<<<><<<^^<^>>^<<<^>>^v^>>^v>vv>^<<^v<>><<><<>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
<><^^>^^^<>^vv<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
^^>vv<^v^v^<>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><
v^^>>><<^^<>>^v^v^<<>^<^v^v><^<<<><<^vv>>v>v^<<^
"""

t1 = """
########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########

<^^>>>vv>v<<
"""

t3 = """
#######
#...#.#
#.....#
#..OO@#
#..O..#
#.....#
#######

 IO.inspect(label: "t1-1")
Day15.solve(t2) |> IO.inspect(label: "t2-1")
Day15.solve(t3, verbose: true, dup: true) |> IO.inspect(label: "t3-2")
Day15.solve(t2, dup: true) |> IO.inspect(label: "t2-2")

Day15.solve(data) |> IO.inspect(label: "r1") #1490942
Day15.solve(data, dup: true) |> IO.inspect(label: "r2") # 1519202
:ok