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

Day 04

day04.livemd

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(&amp;(&amp;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(&amp;String.to_integer/1)

    boards =
      boards_input
      |> Enum.chunk_every(5)
      |> Enum.map(
        &amp;(&amp;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(&amp;mark_board(&amp;1, draw))
  end

  def mark_board({board, marks}, draw) do
    index = Enum.find_index(board, &amp;(&amp;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(&amp;(&amp;1 in marks))
      |> then(&amp;(length(&amp;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(&amp;(&amp;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(&amp;String.to_integer/1)
  |> Enum.reduce_while(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)

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(&amp;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, &amp;MyBoard.mark(&amp;1, number))

      if board = Enum.find(marked_boards, &amp;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(&amp;MyBoard.mark(&amp;1, number))
    |> Enum.reject(&amp;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(
          &amp;(&amp;1
            |> Tuple.to_list()
            |> Enum.frequencies()
            |> Map.values())
        )

      Enum.any?(rows, &amp;(&amp;1 == 5)) or Enum.any?(cols, &amp;(&amp;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