Monkeys fitted squarely as GenServer
s in my head. My initial problem was using cast
instead of call
; I imagine impolite monkeys slinging bananas at each other as fast as they can. (At the end I’m still not sure whether this {:global, ".."})
naming strategy is idiomatic.)
For part 2, I hard-coded the @monkey_factor
to assuage my worry level, and then it is otherwise exactly the same as part 1. There must be some less literal way of going about this, so I’m looking forward to seeing other solutions!
Main module
defmodule Day11Monkeys do
@moduledoc “”"
Documentation for Day11Monkeys
.
“”"
def part_2(input \\ "test") do
notes =
input
|> load_notes()
|> Enum.map(&parse_note/1)
monkeys =
for note <- notes do
monkey_name = note[:monkey] |> Integer.to_string()
{:ok, _pid} = Monkey.start_link(note, {:global, monkey_name})
monkey_name
end
# simulate rounds
for round <- 1..10_000 do
IO.inspect "round #{round}"
for monkey <- monkeys do
for _item <- Monkey.get_items(monkey) do
Monkey.inspect_item(monkey)
end
end
end
[top, second] =
for monkey <- monkeys do
Monkey.get_inspections(monkey)
end
|> Enum.sort(:desc)
|> Enum.slice(0..1)
top * second
end
def data_path(input), do:
Path.expand "./priv/#{input}.txt"
def load_notes(input) do
input
|> data_path()
|> File.stream!()
|> Stream.map(&String.trim/1)
|> Enum.chunk_every(7)
|> Enum.map(fn lines ->
if lines |> Enum.at(-1) == "" do
Enum.drop(lines, -1)
else
lines
end
end)
end
def parse_note(raw_note) do
[
"Monkey " <> monkey,
"Starting items:" <> items,
"Operation: new = old " <> operation,
"Test: divisible by " <> test,
"If true: throw to monkey " <> true_throw,
"If false: throw to monkey " <> false_throw
] = raw_note
%{
monkey: monkey |> String.slice(0..-2) |> String.to_integer(),
items: items
|> String.split(",")
|> Enum.map(fn item ->
item |> String.trim() |> String.to_integer
end),
operation: to_function(operation),
test_divisibility: test |> String.to_integer,
throw_if_true: true_throw,
throw_if_false: false_throw,
inspections: 0
}
end
def to_function("+" <> num_as_string) do
number = num_as_string |> String.trim() |> String.to_integer()
fn value -> (value + number) end
end
def to_function("* old") do
fn value -> (value * value) end
end
def to_function("*" <> num_as_string) do
number = num_as_string |> String.trim() |> String.to_integer()
fn value -> (value * number) end
end
end
Monkey GenServer
defmodule Monkey do
use GenServer
@monkey_factor 17 * 5 * 11 * 13 * 3 * 19 * 2 * 7
# Sample monkey:
# [
# %{
# items: [54, 65, 75, 74],
# monkey: 1,
# operation: #Function<3.79865354/1 in Day11Monkeys.to_function/1>,
# test_divisibility: 19,
# throw_if_false: 0,
# throw_if_true: 2
# }
# ]
# CLIENT
def start_link(monkey, name \\ __MODULE__) do
# you may want to register your server with `name: __MODULE__`
# as a third argument to `start_link`
GenServer.start_link(__MODULE__, monkey, name: name)
end
def get_state(server), do:
GenServer.call({:global, server}, :state)
def get_items(server), do:
GenServer.call({:global, server}, :items)
def get_inspections(server), do:
GenServer.call({:global, server}, :inspections)
def inspect_item(server), do:
GenServer.call({:global, server}, :inspect_item, 200_000)
def throw_to(server, item), do:
GenServer.call({:global, server}, {:add_item_to_end, item})
# SERVER
@impl true
def init(monkey) do
{:ok, monkey}
end
@impl true
def handle_call(:state, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call(:items, _from, state) do
items = Map.get(state, :items)
{:reply, items, state}
end
@impl true
def handle_call(:inspections, _from, state) do
inspections = Map.get(state, :inspections)
{:reply, inspections, state}
end
@impl true
def handle_call(:inspect_item, _from, state) do
%{
monkey: monkey,
items: items,
operation: operation,
test_divisibility: test_divisibility,
throw_if_true: throw_if_true,
throw_if_false: throw_if_false,
inspections: inspections
} = state
[first | remainder] = items
new_worry = first |> operation.() |> rem(@monkey_factor) # |> Kernel.div(3) # part 1
IO.inspect new_worry
if rem(new_worry, test_divisibility) == 0 do
throw_to(throw_if_true, new_worry)
else
throw_to(throw_if_false, new_worry)
end
{
:reply,
"inspected",
%{ state |
items: remainder,
inspections: inspections + 1
}
}
end
@impl true
def handle_call({:add_item_to_end, item}, _from, state) do
items = Map.get(state, :items)
{
:reply,
"item thrown",
%{ state |
items: items ++ [item]
}
}
end
end