diff --git a/day9.livemd b/day9.livemd new file mode 100644 index 0000000..ba34b58 --- /dev/null +++ b/day9.livemd @@ -0,0 +1,173 @@ +# AOC 2022 - Day 9 + +```elixir +Mix.install([ + {:req, "~> 0.3.3"}, + {:vega_lite, "~> 0.1.6"}, + {:kino_vega_lite, "~> 0.1.7"} +]) + +alias VegaLite, as: Vl +``` + +## Puzzle description + +[Day 9: Rope Bridge](https://adventofcode.com/2022/day/9). + +## Input + +```elixir +defmodule Load do + def input do + aoc_session = System.fetch_env!("LB_AOC_SESSION") + input_url = "https://adventofcode.com/2022/day/9/input" + Req.get!(input_url, headers: [cookie: "session=#{aoc_session}"]).body + end +end +``` + +## Solution + +```elixir +defmodule Util do + def process(input) do + input + |> String.split() + |> Enum.chunk_every(2) + |> Enum.map(&parse_motion/1) + end + + defp parse_motion(["U", amount]), do: {{0, 1}, String.to_integer(amount)} + defp parse_motion(["D", amount]), do: {{0, -1}, String.to_integer(amount)} + defp parse_motion(["L", amount]), do: {{-1, 0}, String.to_integer(amount)} + defp parse_motion(["R", amount]), do: {{1, 0}, String.to_integer(amount)} + + def plot_locations(positions) do + data = + positions + |> Enum.map(fn {x, y} -> %{x: x, y: y} end) + + chart = + Vl.new(width: 500, height: 400) + |> Vl.mark(:square) + |> Vl.encode_field(:x, "x", type: :ordinal, title: "X-coordinate", axis: [label_angle: 0]) + |> Vl.encode_field(:y, "y", type: :ordinal, title: "Y-coordinate", sort: :descending) + |> Vl.config(view: [stroke: nil]) + |> Kino.VegaLite.new() + |> Kino.render() + + for position <- data do + Kino.VegaLite.push(chart, position) + Process.sleep(1) + end + + positions + end +end +``` + +```elixir +defmodule RopeSimulator do + def simulate(instructions, knots \\ 2) do + {_final_pos, trail} = + for i <- instructions, reduce: {{0, 0}, []} do + {pos, trail} -> + new_trail = move(pos, i, []) + new_pos = hd(new_trail) + {new_pos, new_trail ++ trail} + end + + trail = Enum.reverse(trail) + + for _ <- 2..knots, reduce: trail do + trail -> Enum.scan(trail, {0, 0}, fn head, tail -> move_tail(head, tail) end) + end + |> MapSet.new() + end + + def move({x, y}, {_dir, 0}, trail), do: [{x, y} | trail] + + def move(pos, {dir, n}, trail) do + new_pos = translate(pos, dir) + move(new_pos, {dir, n - 1}, [new_pos | trail]) + end + + # overlap, do nothing + def move_tail({x, y}, {x, y}), do: {x, y} + # on same row + def move_tail({hx, y}, {tx, y}) when abs(hx - tx) == 1, do: {tx, y} + def move_tail({hx, y}, {tx, y}) when hx > tx, do: {tx + 1, y} + def move_tail({hx, y}, {tx, y}) when hx < tx, do: {tx - 1, y} + # on same column + def move_tail({x, hy}, {x, ty}) when abs(hy - ty) == 1, do: {x, ty} + def move_tail({x, hy}, {x, ty}) when hy > ty, do: {x, ty + 1} + def move_tail({x, hy}, {x, ty}) when hy < ty, do: {x, ty - 1} + # somewhere diagonally => do nothing + def move_tail({hx, hy}, {tx, ty}) when abs(hx - tx) + abs(hy - ty) == 2, do: {tx, ty} + + # move tail closer diagonally + def move_tail({hx, hy}, {tx, ty}) do + {dx, dy} = {hx - tx, hy - ty} + translate({tx, ty}, {round(dx / abs(dx)), round(dy / abs(dy))}) + end + + defp translate({x, y}, {dx, dy}), do: {x + dx, y + dy} +end +``` + +```elixir +defmodule Part1 do + def run(input) do + input + |> Util.process() + |> RopeSimulator.simulate() + |> Enum.count() + end +end + +defmodule Part2 do + def run(input) do + input + |> Util.process() + |> RopeSimulator.simulate(10) + |> Enum.count() + end +end + +ExUnit.start(autorun: false) + +defmodule Test do + use ExUnit.Case, async: true + @example_input ~s(R 5 +U 8 +L 8 +D 3 +R 17 +D 10 +L 25 +U 20) + @input Load.input() + + test "it loads the input" do + assert String.length(@input) > 0 + end + + test "part 1 example" do + assert Part1.run(@example_input) === 88 + end + + test "part 1" do + assert Part1.run(@input) === 5683 + end + + test "part 2 example" do + assert Part2.run(@example_input) === 36 + end + + test "part 2" do + assert Part2.run(@input) === 2372 + end +end + +ExUnit.run() +```