diff --git a/2021/README.md b/2021/README.md index d662ecb..2a3d671 100644 --- a/2021/README.md +++ b/2021/README.md @@ -14,7 +14,7 @@ | S | M | T | W | T | F | S | | :-: | :-: | :-: | :-: | :-: | :-: | :-: | -| | | | [1] | [2] | [3] | 4 | +| | | | [1] | [2] | [3] | [4] | | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | 12 | 13 | 14 | 15 | 16 | 17 | 18 | | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @@ -24,3 +24,4 @@ [1]: ./lib/2021/1.ex [2]: ./lib/2021/2.ex [3]: ./lib/2021/3.ex +[4]: ./lib/2021/4.ex diff --git a/2021/lib/2021/4.ex b/2021/lib/2021/4.ex new file mode 100644 index 0000000..228f82e --- /dev/null +++ b/2021/lib/2021/4.ex @@ -0,0 +1,154 @@ +import AOC + +defmodule BingoPlayer do + use GenServer + + defstruct [:board, :uncalled_numbers, :positions] + + @impl GenServer + def init(board) do + numbers = + board + |> List.flatten() + |> MapSet.new() + + {board, positions} = + for {row, x} <- Enum.with_index(board), {cell, y} <- Enum.with_index(row) do + {{{x, y}, {cell, false}}, {cell, {x, y}}} + end + |> Enum.unzip() + + board = Map.new(board) + positions = Map.new(positions) + + state = %__MODULE__{ + board: board, + uncalled_numbers: numbers, + positions: positions + } + + {:ok, state} + end + + def call_number(pid, n) do + GenServer.call(pid, {:number_called, n}) + end + + @impl GenServer + def handle_call( + {:number_called, n}, + _from, + %__MODULE__{board: board, uncalled_numbers: uncalled_numbers, positions: positions} = + state + ) do + if MapSet.member?(uncalled_numbers, n) do + uncalled_numbers = MapSet.delete(uncalled_numbers, n) + board = Map.put(board, Map.get(positions, n), {n, true}) + + state = %{state | uncalled_numbers: uncalled_numbers, board: board} + + if is_winning_board?(board) do + # Prevent winning boards from continuing to play + uncalled_numbers = MapSet.new() + state = %{state | uncalled_numbers: uncalled_numbers} + {:reply, {:win, score_winning_board(board, n)}, state} + else + {:reply, :hit, state} + end + else + {:reply, :noop, state} + end + end + + defp is_winning_board?(board) do + Enum.any?(0..4, fn x -> + Enum.all?(0..4, fn y -> + Map.get(board, {x, y}) |> then(&elem(&1, 1)) + end) + end) or + Enum.any?(0..4, fn y -> + Enum.all?(0..4, fn x -> + Map.get(board, {x, y}) |> then(&elem(&1, 1)) + end) + end) + end + + defp score_winning_board(board, last_called) do + sum = + board + |> Map.values() + |> Enum.reject(&elem(&1, 1)) + |> Enum.map(&elem(&1, 0)) + |> Enum.sum() + + sum * last_called + end +end + +aoc 2021, 4 do + def parse_input() do + [calls | boards] = input_string() |> String.split("\n\n", trim: true) + + calls = String.split(calls, ",", trim: true) |> Enum.map(&String.to_integer/1) + + boards = + for board <- boards do + for row <- String.split(board, "\n") do + String.split(row, ~r/\W+/, trim: true) + |> Enum.map(&String.to_integer/1) + end + end + + {calls, boards} + end + + def p1 do + {calls, boards} = parse_input() + + players = + for board <- boards do + {:ok, pid} = GenServer.start_link(BingoPlayer, board) + pid + end + + {:halt, score} = + for call <- calls, player <- players, reduce: :cont do + {:halt, _} = resp -> + resp + + :cont -> + case BingoPlayer.call_number(player, call) do + {:win, score} -> {:halt, score} + _ -> :cont + end + end + + Enum.each(players, &GenServer.stop/1) + + score + end + + def p2 do + {calls, boards} = parse_input() + + players = + for board <- boards do + {:ok, pid} = GenServer.start_link(BingoPlayer, board) + pid + end + + [last_winning_score | _scores] = + for call <- calls, player <- players, reduce: [] do + acc -> + case BingoPlayer.call_number(player, call) do + {:win, 0} -> acc + {:win, score} -> [score | acc] + _ -> acc + end + end + + Enum.each(players, &GenServer.stop/1) + + last_winning_score + end +end