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

Day 4

advent_of_code/2021/day-04.livemd

Day 4

Setup

Mix.install(
  [
    {:kino, "~> 0.5.0"}
  ],
  consolidate_protocols: false
)
input = Kino.Input.textarea("Please paste your input:")
defmodule Board do
  empty_board = Tuple.duplicate(Tuple.duplicate(false, 5), 5)
  @enforce_keys [:numbers]
  defstruct numbers: %{}, grid: empty_board

  def new(numbers) when is_map(numbers) do
    %Board{numbers: numbers}
  end

  def mark(%Board{numbers: numbers} = board, number) do
    case numbers do
      %{^number => {row, col}} ->
        put_in(board, [Access.key(:grid), Access.elem(row), Access.elem(col)], true)

      %{} ->
        board
    end
  end

  def unmarked_sum(%Board{numbers: numbers, grid: grid}) do
    Enum.sum(
      for {number, {row, col}} <- numbers,
          grid |> elem(row) |> elem(col) == false,
          do: number
    )
  end

  def won?(%Board{grid: grid}) do
    row_won?(grid) or column_won?(grid)
  end

  defp column_won?(grid) do
    Enum.any?(0..4, fn col ->
      Enum.all?(0..4, fn row -> grid |> elem(row) |> elem(col) end)
    end)
  end

  defp row_won?(grid) do
    Enum.any?(0..4, fn row ->
      elem(grid, row) == {true, true, true, true, true}
    end)
  end
end
[numbers | grids] =
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)

boards =
  grids
  |> Enum.chunk_every(5)
  |> Enum.map(fn rows ->
    Board.new(
      for {line, row} <- Enum.with_index(rows, 0),
          {number, col} <- Enum.with_index(String.split(line), 0),
          into: %{} do
        {String.to_integer(number), {row, col}}
      end
    )
  end)

numbers =
  numbers
  |> String.split(",")
  |> Enum.map(&amp;String.to_integer/1)

Part 1

{number, board = %Board{}} =
  Enum.reduce_while(numbers, boards, fn number, boards ->
    boards = Enum.map(boards, &amp;Board.mark(&amp;1, number))

    if board = Enum.find(boards, &amp;Board.won?/1) do
      {:halt, {number, board}}
    else
      {:cont, boards}
    end
  end)

number * Board.unmarked_sum(board)

Part 2

{number, board = %Board{}} =
  Enum.reduce_while(numbers, boards, fn number, boards ->
    boards = Enum.map(boards, &amp;Board.mark(&amp;1, number))

    case Enum.reject(boards, &amp;Board.won?/1) do
      [] ->
        # We can assume there was only one board left,
        # otherwise AoC gave me a bad input.
        [board] = boards
        {:halt, {number, board}}

      boards ->
        {:cont, boards}
    end
  end)

number * Board.unmarked_sum(board)

Ramblings

# Print the Board struct using christmas colors
defimpl Inspect, for: Board do
  import Inspect.Algebra

  def inspect(%Board{grid: grid, numbers: numbers}, _opts) do
    inverse = for {k, v} <- numbers, into: %{}, do: {v, k}

    contents =
      for row <- 0..4 do
        for col <- 0..4 do
          string = String.pad_leading(Integer.to_string(inverse[{row, col}]), 2)
          color = if grid |> elem(row) |> elem(col), do: :red, else: :green
          color(color, string)
        end
        |> Enum.intersperse(" ")
        |> IO.iodata_to_binary()
      end
      |> Enum.intersperse(line())
      |> concat()

    force_unfit(
      concat([
        color(:green, "~B"),
        color(:red, "\""),
        color(:green, "\""),
        color(:red, "\""),
        line(),
        contents,
        line(),
        color(:green, "\""),
        color(:red, "\""),
        color(:green, "\"")
      ])
    )
  end

  defp color(color, string) do
    IO.ANSI.format([color, string])
    |> IO.iodata_to_binary()
  end
end

board
# Ideally this would be in the Board module,
# but I want to keep it in the Ramblings section.
defmodule BoardSigil do
  defmacro sigil_B({:<<>>, _, [grid]}, []) do
    rows = String.split(grid, "\n")

    for {line, row} <- Enum.with_index(rows, 0),
        {number, col} <- Enum.with_index(String.split(line), 0),
        into: %{} do
      {String.to_integer(number), {row, col}}
    end
    |> Board.new()
    |> Macro.escape()
  end
end
# only: :sigils requires Elixir v1.13
import BoardSigil, only: :sigils

board = ~B"""
51 34 66 87 17
20 54 74 14 55
84 64 96 31  2
62 43 76  5 45
98 71 50 56 82
"""