Powered by AppSignal & Oban Pro

Day 04

2025/day04.livemd

Day 04

Mix.install([:kino_aoc])

Parse

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2025", "4", System.fetch_env!("LB_ADVENT_OF_CODE_SESSION"))
rolls =
  puzzle_input
  |> String.split("\n", trim: true)
  |> Enum.with_index()
  |> Enum.flat_map(fn {line, row} ->
    line
    |> String.to_charlist()
    |> Enum.with_index()
    |> Enum.filter(&(elem(&1, 0) == ?@))
    |> Enum.map(&{elem(&1, 1), row})
  end)
  |> MapSet.new()

Implementation

defmodule PaperRolls do
  defp adjacent({x, y}) do
    for dx <- -1..1,
        dy <- -1..1,
        {dx, dy} != {0, 0},
        do: {x + dx, y + dy}
  end

  def movable?(pos, map) do
    pos
    |> adjacent()
    |> Enum.count(&amp;(&amp;1 in map))
    |> then(&amp;(&amp;1 < 4))
  end
end

Part 1

Enum.count(rolls, &amp;PaperRolls.movable?(&amp;1, rolls))

Part 2

cleaned =
  Stream.repeatedly(fn -> [] end)
  |> Enum.reduce_while(rolls, fn _, acc ->
    removable =
      Enum.filter(acc, &amp;PaperRolls.movable?(&amp;1, acc))
      |> MapSet.new()

    case MapSet.difference(acc, removable) do
      ^acc -> {:halt, acc}
      remaining -> {:cont, remaining}
    end
  end)
MapSet.size(rolls) - MapSet.size(cleaned)