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

Day 4

2021/day_04.livemd

Day 4

Input

Mix.install([{:kino, github: "livebook-dev/kino"}])
textarea = Kino.Input.textarea("Input:")

Common

defmodule Day4 do
  def parse_input(raw_input) do
    [raw_numbers | raw_boards] = String.split(raw_input, "\n\n")

    numbers =
      raw_numbers
      |> String.split(",", trim: true)
      |> Enum.map(&String.to_integer/1)

    boards =
      raw_boards
      |> Enum.map(fn raw_board ->
        raw_board
        |> String.split("\n", trim: true)
        |> Enum.map(fn raw_board_line ->
          raw_board_line
          |> String.split()
          |> Enum.map(&String.to_integer/1)
        end)
        |> Board.new()
      end)

    {numbers, boards}
  end
end

defmodule Board do
  defstruct [:lines, :rows]

  def new(lines) do
    rows =
      lines
      |> List.zip()
      |> Enum.map(&Tuple.to_list/1)

    lines = map_all_items(lines, &{&1, false})
    rows = map_all_items(rows, &{&1, false})

    %__MODULE__{lines: lines, rows: rows}
  end

  def mark(%__MODULE__{lines: lines, rows: rows}, number) do
    mark_fun = fn
      {^number, _marked} -> {number, true}
      tuple -> tuple
    end

    lines = map_all_items(lines, mark_fun)
    rows = map_all_items(rows, mark_fun)

    %__MODULE__{lines: lines, rows: rows}
  end

  def wins?(%__MODULE__{lines: lines, rows: rows}) do
    Enum.find(lines, nil, fn line ->
      Enum.all?(line, fn {_number, marked} -> marked end)
    end) ||
      Enum.find(rows, nil, fn row ->
        Enum.all?(row, fn {_number, marked} -> marked end)
      end)
  end

  def winning_score(%__MODULE__{lines: lines}, last_number) do
    sum =
      lines
      |> List.flatten()
      |> Enum.reject(&elem(&1, 1))
      |> Enum.map(&elem(&1, 0))
      |> Enum.sum()

    sum * last_number
  end

  #
  # private
  # 

  defp map_all_items(list_of_lists, fun) do
    Enum.map(list_of_lists, fn list ->
      Enum.map(list, fn item -> fun.(item) end)
    end)
  end
end
raw_input = Kino.Input.read(textarea)
input = Day4.parse_input(raw_input)

Part 1

defmodule Day4.Part1 do
  def run({numbers, boards}) do
    Enum.reduce_while(numbers, boards, fn number, boards ->
      boards = Enum.map(boards, &Board.mark(&1, number))

      case Enum.find(boards, nil, &Board.wins?(&1)) do
        nil ->
          {:cont, boards}

        board ->
          score = Board.winning_score(board, number)

          {:halt, score}
      end
    end)
  end
end
Day4.Part1.run(input)

Part 2

defmodule Day4.Part2 do
  def run({numbers, boards}) do
    Enum.reduce_while(numbers, boards, fn number, boards ->
      result =
        Enum.reduce_while(0..Enum.count(boards), boards, fn index, boards ->
          boards = List.update_at(boards, index, &Board.mark(&1, number))

          if all_boards_wins?(boards) do
            board = Enum.at(boards, index)

            {:halt, Board.winning_score(board, number)}
          else
            {:cont, boards}
          end
        end)

      case result do
        [_ | _] = boards -> {:cont, boards}
        score -> {:halt, score}
      end
    end)
  end

  def all_boards_wins?(boards), do: Enum.all?(boards, &Board.wins?/1)
end
Day4.Part2.run(input)