Day 04
Setup
Mix.install([:kino])
input = Kino.Input.textarea("Puzzle Input")
Part 1 First Try module
defmodule Day4 do
  @base for i <- 0..4, do: i
  columns =
    @base
    |> Enum.map(fn b ->
      for b2 <- @base, do: b + 5 * b2
    end)
  rows =
    @base
    |> Enum.map(&(&1 * 5))
    |> Enum.map(fn rs ->
      for b <- @base, do: rs + b
    end)
  # NO DIAGONALS!
  # diagonals =
  #   for fun <- [& &1, &(4 - &1)] do
  #     for b <- @base, do: Enum.at(rows, b) |> Enum.at(fun.(b))
  #   end
  @bingos columns ++ rows
  def process_input(input) do
    [draws_input | boards_input] =
      input
      |> Kino.Input.read()
      |> String.split("\n", trim: true)
    draws =
      draws_input
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)
    boards =
      boards_input
      |> Enum.chunk_every(5)
      |> Enum.map(
        &(&1
          |> Enum.join(" ")
          |> String.split(" ", trim: true)
          |> Enum.map(fn square -> String.to_integer(square) end)
          # append empty [] for tracking marked squares
          |> then(fn board -> {board, []} end))
      )
    {boards, draws}
  end
  def mark_boards(boards, draw) do
    boards
    |> Enum.map(&mark_board(&1, draw))
  end
  def mark_board({board, marks}, draw) do
    index = Enum.find_index(board, &(&1 == draw))
    {board, marks ++ [index]}
  end
  def process({boards, []}), do: boards
  def process({boards, [draw | draw_tail]}) do
    process({mark_boards(boards, draw), draw_tail})
  end
  def is_bingo?({_board, marks}) when length(marks) > 4 do
    for bingo <- @bingos do
      bingo
      |> Enum.reject(&(&1 in marks))
      |> then(&(length(&1) == 0))
    end
    |> Enum.any?()
  end
  def is_bingo?(_), do: false
  # Start on 1, not 0
  def draw_numbers(marked_boards, round) do
    marked_boards
    |> Enum.map(fn {board, marks} ->
      is_bingo?({board, Enum.slice(marks, 0, round)})
    end)
  end
  def play_bingo(marked_boards, round, []) do
    board_counts = for i <- 0..(length(marked_boards) - 1), do: i
    bingo_status = draw_numbers(marked_boards, round)
    bingo_boards =
      Enum.reduce(board_counts, [], fn i, bboards ->
        case Enum.at(bingo_status, i) do
          true ->
            [Enum.at(marked_boards, i) | bboards]
          false ->
            bboards
        end
      end)
    play_bingo(marked_boards, round + 1, bingo_boards)
  end
  def play_bingo(_, round, bingo_boards) do
    # I'm leaving this here!
    IO.puts("BINGO!")
    # assume only one bingo for now
    [{board, marks} | _] = bingo_boards
    used_marks = Enum.slice(marks, 0, round - 1)
    final_num = Enum.at(board, Enum.at(used_marks, -1))
    {nillify_board(board, used_marks), final_num}
  end
  def nillify_board(board, [nil | mark_tail]) do
    nillify_board(board, mark_tail)
  end
  def nillify_board(board, [mark | mark_tail]) do
    List.replace_at(board, mark, nil)
    |> nillify_board(mark_tail)
  end
  def nillify_board(board, []) do
    board
  end
end
Part 1 First Try exec
Day4.process_input(input)
|> Day4.process()
|> Day4.play_bingo(1, [])
|> then(fn {squares, bingo} ->
  squares
  |> Enum.filter(&(&1 != nil))
  |> Enum.sum()
  |> then(fn sum -> sum * bingo end)
end)
The Jose Way module
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{grid: grid, numbers: numbers}) 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
The Jose Way exec
[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)
{number, board = %Board{}} =
  numbers
  |> String.split(",")
  |> Enum.map(&String.to_integer/1)
  |> Enum.reduce_while(boards, fn number, boards ->
    boards = Enum.map(boards, &Board.mark(&1, number))
    if board = Enum.find(boards, &Board.won?/1) do
      {:halt, {number, board}}
    else
      {:cont, boards}
    end
  end)
number * Board.unmarked_sum(board)
Module Refactor
defmodule MyBoard do
  def process_input(input) do
    [raw_numbers | raw_boards] =
      input
      |> Kino.Input.read()
      |> String.split("\n", trim: true)
    numbers =
      raw_numbers
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)
    boards =
      raw_boards
      |> Enum.chunk_every(5)
      |> Enum.map(fn rows ->
        for {line, row} <- Enum.with_index(rows),
            {number, col} <- Enum.with_index(String.split(line)) do
          %{String.to_integer(number) => {row, col, false}}
        end
        |> Enum.reduce(%{}, fn square, acc -> Map.merge(acc, square) end)
      end)
    {numbers, boards}
  end
  def play({numbers, boards}) do
    numbers
    |> Enum.reduce_while(boards, fn number, boards ->
      marked_boards = Enum.map(boards, &MyBoard.mark(&1, number))
      if board = Enum.find(marked_boards, &MyBoard.won?/1) do
        {:halt, {number, board}}
      else
        {:cont, marked_boards}
      end
    end)
  end
  def play_all([board], [number | _]) do
    {number, MyBoard.mark(board, number)}
  end
  def play_all(boards, [number | numbers]) do
    boards
    |> Enum.map(&MyBoard.mark(&1, number))
    |> Enum.reject(&MyBoard.won?/1)
    |> MyBoard.play_all(numbers)
  end
  def mark(board, number) do
    case board do
      %{^number => {row, col, false}} ->
        put_in(board, [number], {row, col, true})
      _ ->
        board
    end
  end
  def won?(board) do
    marks = Map.filter(board, fn {_, {_, _, b}} -> b end)
    if length(Map.keys(marks)) >= 5 do
      [rows, cols, _] =
        marks
        |> Map.values()
        |> List.zip()
        |> Enum.map(
          &(&1
            |> Tuple.to_list()
            |> Enum.frequencies()
            |> Map.values())
        )
      Enum.any?(rows, &(&1 == 5)) or Enum.any?(cols, &(&1 == 5))
    end
  end
  def unmarked_sum(board) do
    board
    |> Map.reject(fn {_, {_, _, b}} -> b end)
    |> Map.keys()
    |> Enum.sum()
  end
end
Part 1 Refactor exec
MyBoard.process_input(input)
|> MyBoard.play()
|> then(fn {number, board} ->
  MyBoard.unmarked_sum(board) * number
end)
Part 2 exec
{numbers, boards} = MyBoard.process_input(input)
{number, board} = MyBoard.play_all(boards, numbers)
number * MyBoard.unmarked_sum(board)
Scratch