Day 11
Mix.install([
{:kino, "~> 0.8.0"},
{:deque, "~> 1.0"}
])
Puzzle Input
area = Kino.Input.textarea("Puzzle Input")
puzzle_input = Kino.Input.read(area)
example_input = """
Monkey 0:
Starting items: 79, 98
Operation: new = old * 19
Test: divisible by 23
If true: throw to monkey 2
If false: throw to monkey 3
Monkey 1:
Starting items: 54, 65, 75, 74
Operation: new = old + 6
Test: divisible by 19
If true: throw to monkey 2
If false: throw to monkey 0
Monkey 2:
Starting items: 79, 60, 97
Operation: new = old * old
Test: divisible by 13
If true: throw to monkey 1
If false: throw to monkey 3
Monkey 3:
Starting items: 74
Operation: new = old + 3
Test: divisible by 17
If true: throw to monkey 0
If false: throw to monkey 1
"""
Common
defmodule Monkey do
defstruct [:id]
def new(id), do: %Monkey{id: id}
end
defmodule Item do
defstruct [:id, :worry, :worries]
def new(worry), do: %Item{worry: worry, id: make_ref(), worries: %{}}
end
defmodule Inventory do
defstruct items: %{}, transactions: []
def new(), do: %Inventory{}
def set(%Inventory{} = inventory, %Monkey{} = monkey, items) do
%{inventory | items: Map.put(inventory.items, monkey, items)}
end
def hand(%Inventory{} = inventory, %Item{id: item_id} = item, %Monkey{} = from, %Monkey{} = to) do
from_inventory = Map.fetch!(inventory.items, from)
to_inventory = Map.get(inventory.items, to, [])
{%Item{id: ^item_id}, from_inventory} = List.pop_at(from_inventory, 0)
to_inventory = to_inventory ++ [item]
items = inventory.items |> Map.put(from, from_inventory) |> Map.put(to, to_inventory)
transactions = [{from, to, item} | inventory.transactions]
%{inventory | items: items, transactions: transactions}
end
def items(%Inventory{} = inventory, %Monkey{} = monkey) do
Map.get(inventory.items, monkey, [])
end
end
defmodule InspectBehaviour do
defstruct [:operation]
def new(operation), do: %InspectBehaviour{operation: operation}
def act(%InspectBehaviour{operation: operation}, %Item{} = item) do
worry =
case operation do
{:add, :self} -> item.worry + item.worry
{:add, value} -> item.worry + value
{:multiply, :self} -> item.worry * item.worry
{:multiply, value} -> item.worry * value
end
worry = floor(worry / 3)
%{item | worry: worry}
end
end
defmodule ThrowBehaviour do
defstruct [:operation]
def new(operation), do: %ThrowBehaviour{operation: operation}
def act(%ThrowBehaviour{operation: operation}, %Item{} = item) do
{divisor, first_monkey, second_monkey} = operation
if rem(item.worry, divisor) == 0, do: first_monkey, else: second_monkey
end
end
defmodule KeepAwayGame do
defstruct monkeys: [], inventory: Inventory.new(), inspects: %{}, throws: %{}
def new(), do: %KeepAwayGame{}
def add(
%KeepAwayGame{} = game,
%Monkey{} = monkey,
items,
inspect_behaviour,
%ThrowBehaviour{} = throw_behaviour
) do
%{
game
| monkeys: game.monkeys ++ [monkey],
inventory: Inventory.set(game.inventory, monkey, items),
inspects: Map.put(game.inspects, monkey, inspect_behaviour),
throws: Map.put(game.throws, monkey, throw_behaviour)
}
end
def round(%KeepAwayGame{} = game) do
Enum.reduce(game.monkeys, game, fn monkey, game ->
monkey_move(game, monkey)
end)
end
def monkey_move(%KeepAwayGame{} = game, %Monkey{} = monkey) do
items = Inventory.items(game.inventory, monkey)
case items do
[] ->
game
items ->
inspect_behaviour = Map.fetch!(game.inspects, monkey)
throw_behaviour = Map.fetch!(game.throws, monkey)
Enum.reduce(items, game, fn item, game ->
%mod{} = inspect_behaviour
item = apply(mod, :act, [inspect_behaviour, item])
to_monkey = ThrowBehaviour.act(throw_behaviour, item)
inventory = Inventory.hand(game.inventory, item, monkey, to_monkey)
%{game | inventory: inventory}
end)
end
end
def monkey_business_level(%KeepAwayGame{} = game) do
game.inventory.transactions
|> Stream.map(&elem(&1, 0))
|> Enum.frequencies()
|> Map.values()
|> Enum.sort_by(& &1, :desc)
|> Enum.take(2)
|> Enum.product()
end
end
input = puzzle_input
parse_operation = fn line ->
[_, operator, operator_value] = Regex.run(~r/(\+|\*)\s*(\d+|old)/, line)
operator =
case operator do
"*" -> :multiply
"+" -> :add
end
operation_value =
case operator_value do
"old" -> :self
value -> value |> Integer.parse() |> elem(0)
end
{operator, operation_value}
end
parse_test = fn test_line, first_case_line, second_case_line ->
test_value = Regex.run(~r/\d+/, test_line) |> hd |> Integer.parse() |> elem(0)
first_branch_monkey =
Regex.run(~r/\d+/, first_case_line) |> hd |> Integer.parse() |> elem(0) |> Monkey.new()
second_branch_monkey =
Regex.run(~r/\d+/, second_case_line) |> hd |> Integer.parse() |> elem(0) |> Monkey.new()
{test_value, first_branch_monkey, second_branch_monkey}
end
monkeys =
input
|> String.split("\n\n")
|> Enum.map(fn monkey ->
[monkey_line, items_line, operation_line, test_line, first_case_line, second_case_line] =
String.split(monkey, "\n", trim: true)
monkey = Regex.run(~r/\d+/, monkey_line) |> hd |> Integer.parse() |> elem(0) |> Monkey.new()
items =
Regex.scan(~r/\d+/, items_line)
|> Enum.map(fn [value] -> value |> Integer.parse() |> elem(0) |> Item.new() end)
operation = parse_operation.(operation_line)
test = parse_test.(test_line, first_case_line, second_case_line)
{monkey, items, operation, test}
end)
Part One
game =
Enum.reduce(monkeys, KeepAwayGame.new(), fn monkey, game ->
{monkey, items, operation, test} = monkey
KeepAwayGame.add(
game,
monkey,
items,
InspectBehaviour.new(operation),
ThrowBehaviour.new(test)
)
end)
game = Enum.reduce(1..20, game, fn _, game -> KeepAwayGame.round(game) end)
KeepAwayGame.monkey_business_level(game)
Part Two
defmodule UnmanagableInspectBehaviour do
defstruct [:operation, :divisor]
def new(operation, divisor),
do: %UnmanagableInspectBehaviour{operation: operation, divisor: divisor}
def act(%UnmanagableInspectBehaviour{operation: operation, divisor: divisor}, %Item{} = item) do
worry =
case operation do
{:add, :self} -> item.worry + item.worry
{:add, value} -> item.worry + value
{:multiply, :self} -> item.worry * item.worry
{:multiply, value} -> item.worry * value
end
%{item | worry: rem(worry, divisor)}
end
end
round_divisor =
monkeys
|> Stream.map(fn monkey ->
{_monkey, _items, _operation, {divisor, _, _}} = monkey
divisor
end)
|> Enum.product()
game =
Enum.reduce(monkeys, KeepAwayGame.new(), fn monkey, game ->
{monkey, items, operation, test} = monkey
KeepAwayGame.add(
game,
monkey,
items,
UnmanagableInspectBehaviour.new(operation, round_divisor),
ThrowBehaviour.new(test)
)
end)
game = Enum.reduce(1..10000, game, fn _, game -> KeepAwayGame.round(game) end)
KeepAwayGame.monkey_business_level(game)