Powered by AppSignal & Oban Pro

aoc 2019 day 7 - fork

2019/elixir/day-7-2.livemd

aoc 2019 day 7 - fork

Setup

Mix.install([{:kino, "~> 0.5.1"}])
input = Kino.Input.textarea("")
init_state =
  input
  |> Kino.Input.read()
  |> String.split(",", trim: true)
  |> Enum.map(&String.to_integer/1)
  |> Enum.with_index()
  |> Map.new(fn {v, k} -> {k, v} end)

Computer

defmodule Computer.Operator do
  defstruct [:params, :op_code]

  def new(instruction) do
    op_code = rem(instruction, 100)
    params = get_parameters(instruction)

    %__MODULE__{
      op_code: op_code(op_code),
      params: params
    }
  end

  defp get_parameters(instruction) do
    {
      instruction |> div(100) |> rem(10),
      instruction |> div(1_000) |> rem(10)
    }
  end

  defp op_code(1), do: :add
  defp op_code(2), do: :mul
  defp op_code(3), do: :read
  defp op_code(4), do: :print
  defp op_code(5), do: :jump_neq
  defp op_code(6), do: :jump_eq
  defp op_code(7), do: :comp_lt
  defp op_code(8), do: :comp_eq
  defp op_code(99), do: :halt
end
defmodule Computer.Memory do
  @inc_mode 1
  @addr_mode 0

  defstruct memory: %{}, inputs: [], outputs: [], pointer: 0

  def new(data: %__MODULE__{memory: map, inputs: prev_inputs, pointer: pointer}, inputs: inputs),
    do: %__MODULE__{memory: map, inputs: prev_inputs ++ inputs, pointer: pointer}

  def new(data: map, inputs: inputs) do
    %__MODULE__{memory: map, inputs: inputs}
  end

  def get_instruction(%__MODULE__{memory: map, pointer: pointer}), do: map[pointer]

  def get_value(%__MODULE__{memory: map}, position, @inc_mode), do: map[position]
  def get_value(%__MODULE__{memory: map}, position, @addr_mode), do: map[map[position]]

  def put_value(%__MODULE__{memory: map} = s, position, value),
    do: %{s | memory: Map.put(map, map[position], value)}

  def append_output(memory, output), do: %{memory | outputs: [output | memory.outputs]}

  def move_pointer(memory, next_ptr), do: %{memory | pointer: next_ptr}

  def pop_input(%__MODULE__{inputs: [input | rest]} = memory),
    do: {input, %{memory | inputs: rest}}

  def put_inputs(memory, inputs), do: %{memory | inputs: memory.inputs ++ inputs}
end

loop: compute_until_output output?:

cast next server

compute_until_output

defmodule Computer do
  alias Computer.{Memory, Operator}

  @spec init_state(map(), list()) :: %Memory{}
  def init_state(data, inputs \\ []) do
    Memory.new(data: data, inputs: inputs)
  end

  @spec process(%Memory{}, list()) ::
          {%Memory{}, :halt} | {%Memory{}, :hold} | {%Memory{}, list()}
  def process(memory, inputs) do
    Memory.new(data: memory, inputs: inputs)
    |> compute_until_output()
  end

  @spec compute(%Memory{}) :: %Memory{}
  def compute(memory) do
    Memory.get_instruction(memory)
    |> Operator.new()
    |> case do
      %Operator{op_code: :halt} ->
        memory

      op ->
        move_operation(op, memory)
        |> compute()
    end
  end

  @spec compute_until_output(%Memory{}) ::
          {%Memory{}, :halt} | {%Memory{}, :hold} | {%Memory{}, list()}
  def compute_until_output(memory) do
    Memory.get_instruction(memory)
    |> Operator.new()
    |> case do
      %Operator{op_code: :halt} ->
        {memory, :halt}

      op ->
        move_operation(op, memory)
        |> case do
          {memory, :hold} ->
            {memory, :hold}

          {memory, outputs} ->
            {memory, outputs}

          memory ->
            compute_until_output(memory)
        end
    end
  end

  defp move_operation(%Operator{op_code: op_code, params: {p1, p2}}, memory)
       when op_code in [:add, :mul] do
    v1 = Memory.get_value(memory, memory.pointer + 1, p1)
    v2 = Memory.get_value(memory, memory.pointer + 2, p2)
    res = calc(op_code).(v1, v2)

    next_pointer = memory.pointer + 4

    Memory.put_value(memory, memory.pointer + 3, res)
    |> Memory.move_pointer(next_pointer)
  end

  defp move_operation(%Operator{op_code: :read}, %{inputs: []} = memory) do
    {memory, :hold}
  end

  defp move_operation(%Operator{op_code: :read}, memory) do
    {input, memory} = Memory.pop_input(memory)

    next_pointer = memory.pointer + 2

    Memory.put_value(memory, memory.pointer + 1, input)
    |> Memory.move_pointer(next_pointer)
  end

  defp move_operation(%Operator{op_code: :print, params: {p1, _}}, memory) do
    output = Memory.get_value(memory, memory.pointer + 1, p1)
    next_pointer = memory.pointer + 2

    Memory.append_output(memory, output)
    |> Memory.move_pointer(next_pointer)
    |> then(&{&1, &1.outputs})
  end

  defp move_operation(%Operator{op_code: :jump_neq, params: {p1, p2}}, memory) do
    predicate = Memory.get_value(memory, memory.pointer + 1, p1)
    goto = Memory.get_value(memory, memory.pointer + 2, p2)

    next_pointer = if predicate != 0, do: goto, else: memory.pointer + 3
    Memory.move_pointer(memory, next_pointer)
  end

  defp move_operation(%Operator{op_code: :jump_eq, params: {p1, p2}}, memory) do
    predicate = Memory.get_value(memory, memory.pointer + 1, p1)
    goto = Memory.get_value(memory, memory.pointer + 2, p2)

    next_pointer = if predicate == 0, do: goto, else: memory.pointer + 3
    Memory.move_pointer(memory, next_pointer)
  end

  defp move_operation(%Operator{op_code: :comp_lt, params: {p1, p2}}, memory) do
    v1 = Memory.get_value(memory, memory.pointer + 1, p1)
    v2 = Memory.get_value(memory, memory.pointer + 2, p2)
    value = if v1 < v2, do: 1, else: 0
    next_pointer = memory.pointer + 4

    Memory.put_value(memory, memory.pointer + 3, value)
    |> Memory.move_pointer(next_pointer)
  end

  defp move_operation(%Operator{op_code: :comp_eq, params: {p1, p2}}, memory) do
    v1 = Memory.get_value(memory, memory.pointer + 1, p1)
    v2 = Memory.get_value(memory, memory.pointer + 2, p2)
    value = if v1 == v2, do: 1, else: 0
    next_pointer = memory.pointer + 4

    Memory.put_value(memory, memory.pointer + 3, value)
    |> Memory.move_pointer(next_pointer)
  end

  defp calc(:add), do: &Kernel.+/2
  defp calc(:mul), do: &Kernel.*/2
end
defmodule ComputerServer do
  use GenServer, restart: :transient

  require Computer

  def start_link(args) do
    GenServer.start_link(__MODULE__, args)
  end

  @impl true
  def init(data: data, inputs: inputs, amp: amp, callback: callback) do
    IO.puts("server start! amp: #{amp}")

    {:ok,
     %{
       comp_memory: Computer.init_state(data, inputs),
       next: nil,
       outputs: [],
       amp: amp,
       callback: callback
     }}
  end

  def handle_cast({:register_next, next}, state) do
    {:noreply, %{state | next: next}}
  end

  @impl true
  def handle_cast({:start, inputs}, state) do
    IO.puts("this is process start!")
    go(state, inputs)
  end

  @impl true
  def handle_cast({:push, inputs}, state) do
    go(state, inputs)
  end

  @impl true
  def handle_continue(:process, state) do
    go(state)
  end

  defp go(%{comp_memory: memory, next: next, outputs: outputs} = s, inputs \\ []) do
    case Computer.process(memory, inputs) do
      {new_memory, :hold} ->
        {:noreply, %{s | comp_memory: new_memory}}

      {_state, :halt} ->
        IO.puts("#{s.amp}: #{inspect(outputs)}")
        if s.callback, do: outputs |> hd() |> s.callback.()
        {:stop, :normal, outputs}

      {new_memory, [new_output]} ->
        GenServer.cast(next, {:push, [new_output]})

        {:noreply, %{s | comp_memory: new_memory, outputs: [new_output | outputs]},
         {:continue, :process}}
    end
  end
end
defmodule ComputerManager do
  use GenServer

  # def start_link(args) do
  #   GenServer.start_link(__MODULE__, args)
  # end

  def run_amplifiers(args) do
    args = args |> Map.new() |> Map.put(:callback, self())
    {:ok, _pid} = GenServer.start_link(__MODULE__, args)

    receive do
      {:output, output} ->
        output
    after
      5000 ->
        {:error, :timeout}
    end
  end

  #########################################

  def init(args) do
    Process.flag(:trap_exit, true)
    init_data = args[:init_data]
    phases = args[:phases]
    callback_fn = fn output -> send(args[:callback], {:output, output}) end

    children =
      phases
      |> Enum.zip([:A, :B, :C, :D, :E])
      |> Enum.map(fn {phase, amp} ->
        {:ok, pid} =
          ComputerServer.start_link(
            data: init_data,
            inputs: [phase],
            amp: amp,
            callback: if(amp == :E, do: callback_fn)
          )

        {amp, pid}
      end)
      |> Map.new()

    register_all_next(children)
    {:ok, children, {:continue, {:process, :A}}}
  end

  @impl true
  def handle_continue({:process, amp}, children) do
    IO.puts("amp start : #{amp}")
    # IO.inspect(children)
    GenServer.cast(children[amp], {:start, [0]})
    {:noreply, children}
  end

  def handle_info({:EXIT, from, :normal}, children) do
    amp = children |> Enum.find_value(fn {amp, pid} -> pid == from && amp end)
    IO.puts("Amp end #{amp}(#{inspect(from)})")

    remain = children |> Enum.reject(fn {_amp, pid} -> pid == from end)

    if remain == [] do
      IO.puts("all amps ends")
      {:stop, :normal, []}
    else
      {:noreply, remain}
    end
  end

  defp register_all_next(children) do
    [:A, :B, :C, :D, :E]
    |> Enum.chunk_every(2, 1, [:A])
    |> Enum.each(fn [cur, next] ->
      GenServer.cast(children[cur], {:register_next, children[next]})
    end)
  end
end
ComputerManager.run_amplifiers(init_data: init_state, phases: [9, 8, 7, 6, 5])

Permute

defmodule Permutator do
  def permute(range) do
    permute(Enum.to_list(range), Range.size(range), [], [])
  end

  defp permute([], _len, acc, total), do: [acc | total]

  defp permute(list, len, acc, total) do
    for i <- 0..(len - 1), reduce: total do
      tt ->
        {n, rest} = List.pop_at(list, i)
        permute(rest, len - 1, [n | acc], tt)
    end
  end
end

Part 1

defmodule Part1.Solver do
  def solve(init_data) do
    Permutator.permute(0..4)
    |> Enum.map(fn phases ->
      phases
      |> Enum.reduce(init_data, fn phase, state ->
        output = state |> Map.get(:outputs, [0]) |> List.first()

        Computer.init_state(init_data, [phase, output])
        |> Computer.compute()
      end)
    end)
    |> Enum.max()
  end
end
# Part1.Solver.solve(init_state)

Part 2

defmodule Part2.Solver do
  def solve(init_data) do
    Permutator.permute(5..9)
    |> Task.async_stream(fn phases ->
      computer_map = for p <- phases, into: %{}, do: {p, Computer.init_state(init_data, [p])}

      phases
      |> Stream.cycle()
      # |> Enum.take(300)
      |> Enum.reduce_while({computer_map, [0]}, fn
        phase, {cpt_map, :halt} ->
          {:halt, cpt_map[List.last(phases)].outputs |> List.first()}

        phase, {cpt_map, outputs} ->
          state = cpt_map[phase]
          {state, outputs} = Computer.process(state, outputs)
          {:cont, {cpt_map |> Map.put(phase, state), outputs}}
      end)
    end)
    |> Enum.max_by(&elem(&1, 1))
  end
end
Part2.Solver.solve(init_state)
defmodule Part2.ServerSolver do
  def solve(init_data) do
    Permutator.permute(5..9)
    |> Task.async_stream(fn phases ->
      ComputerManager.run_amplifiers(init_data: init_data, phases: phases)
    end)
    |> Enum.reduce(0, fn {:ok, cur}, prev -> max(cur, prev) end)
  end
end
Part2.ServerSolver.solve(init_state)