Day 4
Setup
Mix.install(
[
{:kino, "~> 0.5.0"}
],
consolidate_protocols: false
)
input = Kino.Input.textarea("Please paste your input:")
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{numbers: numbers, grid: grid}) 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
[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)
numbers =
numbers
|> String.split(",")
|> Enum.map(&String.to_integer/1)
Part 1
{number, board = %Board{}} =
Enum.reduce_while(numbers, 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)
Part 2
{number, board = %Board{}} =
Enum.reduce_while(numbers, boards, fn number, boards ->
boards = Enum.map(boards, &Board.mark(&1, number))
case Enum.reject(boards, &Board.won?/1) do
[] ->
# We can assume there was only one board left,
# otherwise AoC gave me a bad input.
[board] = boards
{:halt, {number, board}}
boards ->
{:cont, boards}
end
end)
number * Board.unmarked_sum(board)
Ramblings
# Print the Board struct using christmas colors
defimpl Inspect, for: Board do
import Inspect.Algebra
def inspect(%Board{grid: grid, numbers: numbers}, _opts) do
inverse = for {k, v} <- numbers, into: %{}, do: {v, k}
contents =
for row <- 0..4 do
for col <- 0..4 do
string = String.pad_leading(Integer.to_string(inverse[{row, col}]), 2)
color = if grid |> elem(row) |> elem(col), do: :red, else: :green
color(color, string)
end
|> Enum.intersperse(" ")
|> IO.iodata_to_binary()
end
|> Enum.intersperse(line())
|> concat()
force_unfit(
concat([
color(:green, "~B"),
color(:red, "\""),
color(:green, "\""),
color(:red, "\""),
line(),
contents,
line(),
color(:green, "\""),
color(:red, "\""),
color(:green, "\"")
])
)
end
defp color(color, string) do
IO.ANSI.format([color, string])
|> IO.iodata_to_binary()
end
end
board
# Ideally this would be in the Board module,
# but I want to keep it in the Ramblings section.
defmodule BoardSigil do
defmacro sigil_B({:<<>>, _, [grid]}, []) do
rows = String.split(grid, "\n")
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
|> Board.new()
|> Macro.escape()
end
end
# only: :sigils requires Elixir v1.13
import BoardSigil, only: :sigils
board = ~B"""
51 34 66 87 17
20 54 74 14 55
84 64 96 31 2
62 43 76 5 45
98 71 50 56 82
"""