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