Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 22

2022/elixir/day22.livemd

Day 22

Mix.install([
  {:kino, "~> 0.8.0"}
])

Puzzle Input

area = Kino.Input.textarea("Puzzle Input")
puzzle_input = Kino.Input.read(area)
example_input = """
        ...#     
        .#..     
        #...     
        ....     
...#.......#    
........#...    
..#....#....    
..........#.    
        ...#....
        .....#..
        .#......
        ......#.

10R5L5R10L4R5L5
"""
input = example_input

Common

[map, navigation] = String.split(input, "\n\n", parts: 2)

tiles =
  for {line, row} <- map |> String.split("\n", trim: true) |> Stream.with_index(1),
      {tile, column} <- line |> String.codepoints() |> Stream.with_index(1) do
    {{column, row}, tile}
  end
  |> Map.new()
instructions =
  navigation
  |> String.split(~r{(L|R)}, include_captures: true, trim: true)
  |> Enum.map(fn instruction ->
    case instruction do
      "L" -> {:turn, "L"}
      "R" -> {:turn, "R"}
      steps -> {:step, steps |> Integer.parse() |> elem(0)}
    end
  end)
defmodule Player do
  defstruct [:position, :direction]

  def turn(%Player{direction: direction} = player, opts \\ [clockwise?: true]) do
    clockwise? = Keyword.fetch!(opts, :clockwise?)

    direction =
      case direction do
        :up -> if clockwise?, do: :right, else: :left
        :left -> if clockwise?, do: :up, else: :down
        :down -> if clockwise?, do: :left, else: :right
        :right -> if clockwise?, do: :down, else: :up
      end

    %{player | direction: direction}
  end

  def next_position(%Player{direction: direction, position: {x, y}}) do
    case direction do
      :left -> {x - 1, y}
      :right -> {x + 1, y}
      :up -> {x, y - 1}
      :down -> {x, y + 1}
    end
  end

  def password(%Player{direction: direction, position: {x, y}}) do
    directions = %{
      right: 0,
      down: 1,
      left: 2,
      up: 3
    }

    1000 * y + 4 * x + directions[direction]
  end
end

Part One

12512 is too low

defmodule Board do
  def start_position(tiles) do
    tiles
    |> Stream.filter(fn {{_x, y}, tile} -> tile == "." &amp;&amp; y == 1 end)
    |> Stream.map(&amp;elem(&amp;1, 0))
    |> Enum.min()
  end

  def next_tile(tiles, %Player{} = player) do
    next = Player.next_position(player)

    case Map.get(tiles, next, " ") do
      " " ->
        wrap(tiles, player)

      tile ->
        {next, tile}
    end
  end

  def wrap(tiles, %Player{} = player) do
    %{position: {x, y}, direction: direction} = player

    horizontal? = direction in [:left, :right]

    matcher? =
      if horizontal? do
        &amp;match?({_, ^y}, &amp;1)
      else
        &amp;match?({^x, _}, &amp;1)
      end

    axis_tiles =
      tiles
      |> Stream.filter(fn {position, tile} ->
        matcher?.(position) &amp;&amp; tile != " "
      end)

    max_side? = direction in [:left, :up]

    if max_side? do
      Enum.max_by(axis_tiles, &amp;elem(&amp;1, 0))
    else
      Enum.min_by(axis_tiles, &amp;elem(&amp;1, 0))
    end
  end

  def execute(tiles, %Player{} = player, instruction) do
    case instruction do
      {:turn, "L"} ->
        Player.turn(player, clockwise?: false)

      {:turn, "R"} ->
        Player.turn(player, clockwise?: true)

      {:step, steps} ->
        Enum.reduce_while(1..steps, player, fn _, player ->
          {next_position, next_tile} = next_tile(tiles, player)

          case next_tile do
            "#" -> {:halt, player}
            "." -> {:cont, %{player | position: next_position}}
          end
        end)
    end
  end
end
player = %Player{position: Board.start_position(tiles), direction: :right}
player = Enum.reduce(instructions, player, &amp;Board.execute(tiles, &amp;2, &amp;1))
Player.password(player)

Part Two

defmodule Cube do
  @side 4

  @sides %{
    {2, 0} => 1,
    {0, 1} => 2,
    {1, 1} => 3,
    {2, 1} => 4,
    {2, 2} => 5,
    {3, 2} => 6
  }

  def start_position(tiles) do
    tiles
    |> Stream.filter(fn {{_x, y}, tile} -> tile == "." &amp;&amp; y == 1 end)
    |> Stream.map(&amp;elem(&amp;1, 0))
    |> Enum.min()
  end

  def next_tile(tiles, %Player{} = player) do
    next = Player.next_position(player)

    case Map.get(tiles, next, " ") do
      " " ->
        wrap(tiles, player)

      tile ->
        {next, tile}
    end
  end

  def wrap(tiles, %Player{} = player) do
    %{position: {x, y}, direction: direction} = player

    horizontal? = direction in [:left, :right]

    matcher? =
      if horizontal? do
        &amp;match?({_, ^y}, &amp;1)
      else
        &amp;match?({^x, _}, &amp;1)
      end

    axis_tiles =
      tiles
      |> Stream.filter(fn {position, tile} ->
        matcher?.(position) &amp;&amp; tile != " "
      end)

    max_side? = direction in [:left, :up]

    if max_side? do
      Enum.max_by(axis_tiles, &amp;elem(&amp;1, 0))
    else
      Enum.min_by(axis_tiles, &amp;elem(&amp;1, 0))
    end
  end

  def side({x, y}) do
    u = floor((x - 1) / @side)
    v = floor((y - 1) / @side)

    Map.fetch!(@sides, {u, v})
  end

  def execute(tiles, %Player{} = player, instruction) do
    case instruction do
      {:turn, "L"} ->
        Player.turn(player, clockwise?: false)

      {:turn, "R"} ->
        Player.turn(player, clockwise?: true)

      {:step, steps} ->
        Enum.reduce_while(1..steps, player, fn _, player ->
          {next_position, next_tile} = next_tile(tiles, player)

          case next_tile do
            "#" -> {:halt, player}
            "." -> {:cont, %{player | position: next_position}}
          end
        end)
    end
  end
end
Cube.side({1, 9})