Advent of Code 2021 - Day 4

Once the Bingo module was written…

defmodule Bingo do
  defdelegate new_board(list), as: :new, to: __MODULE__.Board
  defdelegate play(board, value), to: __MODULE__.Board
  defdelegate score(tuple), to: __MODULE__.Board
  defdelegate score(board, value), to: __MODULE__.Board

  defmodule Cell, do: defstruct [val: nil, marked: false]
  defmodule Board do
    @size 5
    alias Bingo.Cell
    defstruct [grid: %{}, win: false]

    def new(list) do
      {board, _} = Enum.reduce(list, {%__MODULE__{}, 0}, &reducer/2)
      board
    end

    def play(%__MODULE__{grid: grid} = board, value) do
      case Enum.find(grid, fn {_coord, cell} -> cell.val == value end) do
        {coord, cell} ->
          new_grid = Map.put(grid, coord, %{cell | marked: true})
          %{board | grid: new_grid, win: is_winning(new_grid, coord)}
        nil -> board
      end
    end

    def score({board, value}), do: score(board, value)
    def score(board, value) do
      board.grid
      |> Enum.filter(fn {{_row, _col}, %{marked: marked}} -> not marked end)
      |> Enum.map(fn {_coord, cell} -> cell.val end)
      |> Enum.sum()
      |> Kernel.*(value)
    end

    defp reducer(val, {board, current}) do
      grid = Map.put(board.grid, to_coord(current), %Cell{val: val})
      {%{board | grid: grid}, current + 1}
    end

    defp to_coord(index), do: {div(index, @size), rem(index, @size)}

    defp is_winning(grid, {row, col}) do
      Enum.all?(get_line(:row, grid, row), fn {_coord, cell} -> cell.marked end) ||
      Enum.all?(get_line(:col, grid, col), fn {_coord, cell} -> cell.marked end)
    end

    defp get_line(:row, grid, index),
      do: Enum.filter(grid, fn {{row, _col}, _cell} -> row == index end)
    defp get_line(:col, grid, index),
      do: Enum.filter(grid, fn {{_row, col}, _cell} -> col == index end)
  end
end

… the rest was simple

defmodule Day4 do
  def part1 do
    {moves, boards} = load_data()
    {boards, move} = moves
    |> Enum.reduce_while(boards, fn move, boards ->
      boards = Enum.map(boards, &Bingo.play(&1, move))
      if Enum.any?(boards, & &1.win),
        do: {:halt, {boards, move}},
        else: {:cont, boards}
    end)
    boards
    |> Enum.find(& &1.win)
    |> Bingo.score(move)
  end

  def part2 do
    {moves, boards} = load_data()
    moves
    |> Enum.reduce_while(boards, fn move, boards ->
      boards = Enum.map(boards, &Bingo.play(&1, move))
      case boards do
        [%{win: true} = board] -> {:halt, {board, move}}
        [board] -> {:cont, [board]}
        boards -> {:cont, Enum.filter(boards, & not(&1.win))}
      end
    end)
    |> Bingo.score()
  end

  def load_data do
    "input.txt"
    |> File.read!()
    |> String.split("\n", trim: true)
    |> sanitize()
  end

  defp sanitize([head | rows]) do
    moves = head
    |> String.split(",", trim: true)
    |> Enum.map(& String.to_integer(&1))

    boards = rows
    |> Enum.map(&String.split(&1, " ", trim: true))
    |> Enum.chunk_every(5)
    |> Enum.map(&sanitize_list/1)
    |> Enum.map(&Bingo.new_board(&1))

    {moves, boards}
  end

  defp sanitize_list(list),
    do: Enum.flat_map(list, fn l -> Enum.map(l, & String.to_integer(&1)) end)
end
1 Like