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

Day 17

2022/day17.livemd

Day 17

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

Setup

input = Kino.Input.textarea("input file")

Common

defmodule Day17 do
  def move_piece_until_settled(map, piece, jet_streams) do
    %{:map => new_map, :piece => settled_piece, :jet_streams => remaining_jet_streams} =
      do_move_piece_until_settled(map, piece, jet_streams)

    %{
      map: new_map,
      piece_settled_position: settled_piece,
      remaining_jet_streams: remaining_jet_streams
    }
  end

  def normalize_map(map) do
    # Get max y for each x
    map
    |> Enum.group_by(fn {x, _y} -> x end)
    |> Enum.map(fn {k, v} ->
      {k,
       v
       |> Enum.max_by(fn {_x, y} -> y end)}
    end)
    |> Map.new()
    |> Map.new()
    |> Enum.map(fn {_k, v} -> v end)
    |> then(fn map ->
      floor = 1..7 |> Enum.map(fn x -> {x, 0} end)
      map ++ floor
    end)
  end

  # Private methods
  defp do_move_piece_until_settled(map, piece, jet_streams) do
    {settled, {new_map, new_piece, new_jet_streams}} =
      {map, piece, jet_streams}
      |> do_move_piece_for_jet_stream()
      |> do_move_piece_down()

    case settled do
      true ->
        %{map: new_map, piece: new_piece, jet_streams: new_jet_streams}

      false ->
        do_move_piece_until_settled(new_map, new_piece, new_jet_streams)
    end
  end

  defp do_move_piece_for_jet_stream({map, piece, jet_streams}) do
    {:next, movement, remaining_jet_streams} = StreamIterator.Suspension.next(jet_streams)
    # [movement | remaining_jet_streams] = jet_streams

    new_piece_pos = do_move_piece(movement, map, piece)

    {map, new_piece_pos, remaining_jet_streams}
  end

  defp do_move_piece_down({map, piece, jet_streams}) do
    new_piece =
      piece
      |> Enum.map(fn {x, y} ->
        {x, y - 1}
      end)

    # Check for collision
    collision = has_intersection(map, new_piece)

    case collision do
      true ->
        {true, {map ++ piece, piece, jet_streams}}

      false ->
        {false, {map, new_piece, jet_streams}}
    end
  end

  defp do_move_piece(">", map, piece) do
    new_piece =
      piece
      |> Enum.map(fn {x, y} ->
        {x + 1, y}
      end)

    max_x = new_piece |> Enum.map(fn {x, _} -> x end) |> Enum.max()
    # Check for collision
    collision = has_intersection(map, new_piece)

    case max_x >= 8 || collision do
      true ->
        piece

      false ->
        new_piece
    end
  end

  defp do_move_piece("<", map, piece) do
    new_piece =
      piece
      |> Enum.map(fn {x, y} ->
        {x - 1, y}
      end)

    min_x = new_piece |> Enum.map(fn {x, _} -> x end) |> Enum.min()
    # Check for collision
    collision = has_intersection(map, new_piece)

    case min_x <= 0 || collision do
      true ->
        piece

      false ->
        new_piece
    end
  end

  def has_intersection(list_1, list_2) do
    list_3 = list_1 -- list_2
    final_list = list_1 -- list_3

    length(final_list) > 0
  end
end

# Good abstraction to get stream elements one by one.
# Found here: https://elixirforum.com/t/best-way-to-iterate-a-stream/24382/17
defmodule StreamIterator.Suspension do
  defstruct continuation: nil

  def start(stream) do
    reduce_fun = fn item, _acc -> {:suspend, item} end
    {:suspended, nil, continuation} = Enumerable.reduce(stream, {:suspend, nil}, reduce_fun)
    %__MODULE__{continuation: continuation}
  end

  def next(state)

  def next(%{continuation: nil} = state) do
    {:eof, state}
  end

  def next(state) do
    case state.continuation.({:cont, nil}) do
      {:suspended, item, continuation} -> {:next, item, %{state | continuation: continuation}}
      {:halted, item} -> {item, %{state | continuation: nil}}
      {:done, nil} -> {:eof, %{state | continuation: nil}}
    end
  end
end

Part 1

movements =
  input
  |> Kino.Input.read()
  |> String.split("", trim: true)

shape_1 = fn init_pos_x, init_pos_y ->
  # Shape
  # ####
  # 
  # init_pos_x is the leftmost position
  # init_post_y is the lowest position
  [
    {init_pos_x, init_pos_y},
    {init_pos_x + 1, init_pos_y},
    {init_pos_x + 2, init_pos_y},
    {init_pos_x + 3, init_pos_y}
  ]
end

shape_2 = fn init_pos_x, init_pos_y ->
  # Shape
  # .#.
  # ###
  # .#.
  #
  # init_pos_x is the leftmost position
  # init_post_y is the lowest position
  [
    {init_pos_x, init_pos_y + 1},
    {init_pos_x + 1, init_pos_y + 1},
    {init_pos_x + 2, init_pos_y + 1},
    {init_pos_x + 1, init_pos_y},
    {init_pos_x + 1, init_pos_y + 2}
  ]
end

shape_3 = fn init_pos_x, init_pos_y ->
  # Shape
  # ..#
  # ..#
  # ###
  # init_pos_x is the leftmost position
  # init_post_y is the lowest position
  [
    {init_pos_x, init_pos_y},
    {init_pos_x + 1, init_pos_y},
    {init_pos_x + 2, init_pos_y},
    {init_pos_x + 2, init_pos_y + 1},
    {init_pos_x + 2, init_pos_y + 2}
  ]
end

shape_4 = fn init_pos_x, init_pos_y ->
  # Shape
  # #
  # #
  # #
  # #
  # init_pos_x is the leftmost position
  # init_post_y is the lowest position
  [
    {init_pos_x, init_pos_y},
    {init_pos_x, init_pos_y + 1},
    {init_pos_x, init_pos_y + 2},
    {init_pos_x, init_pos_y + 3}
  ]
end

shape_5 = fn init_pos_x, init_pos_y ->
  # Shape
  # ##
  # ##
  # init_pos_x is the leftmost position
  # init_post_y is the lowest position
  [
    {init_pos_x, init_pos_y},
    {init_pos_x + 1, init_pos_y},
    {init_pos_x, init_pos_y + 1},
    {init_pos_x + 1, init_pos_y + 1}
  ]
end

shapes = [shape_1, shape_2, shape_3, shape_4, shape_5]

movements_stream = StreamIterator.Suspension.start(Stream.cycle(movements))
shapes_stream = StreamIterator.Suspension.start(Stream.cycle(shapes))

rocks_to_watch = 2022
# rocks_to_watch = 1_000_000_000_000
units_wide = 7
y_wall_left = 0
y_wall_right = 8
starts_units_above = 3

# (rocks_to_watch)
# IO.inspect("before rocks")
# rocks = shapes_stream |> Enum.take(1_000_000_000_000)
# IO.inspect("after rocks")
# jet_streams = movements_stream |> Enum.take(1_000_000_000_000_000)
# IO.inspect("after jet streams")

# This is the floor, {0,0} and {0,8} are walls
map = 1..units_wide |> Enum.map(fn x -> {x, 0} end)
highest_y = map |> Enum.map(fn {_x, y} -> y end) |> Enum.max()

1..1_000_000_000_000
|> Enum.reduce(
  %{
    count: 1,
    map: map,
    highest_y: highest_y,
    jet_streams: movements_stream,
    shapes_stream: shapes_stream
  },
  fn _, acc ->
    IO.inspect(acc[:count])
    {:next, rock_fn, new_shapes_stream} = StreamIterator.Suspension.next(acc[:shapes_stream])

    rock_initial_pos = rock_fn.(3, acc[:highest_y] + 4)

    %{
      :map => new_map,
      :piece_settled_position => settled_piece,
      :remaining_jet_streams => remaining_jet_streams
    } = Day17.move_piece_until_settled(acc[:map], rock_initial_pos, acc[:jet_streams])

    normalized_map = Day17.normalize_map(new_map)

    max_y_settled_piece = settled_piece |> Enum.map(fn {_, y} -> y end) |> Enum.max()

    new_highest_y = [acc[:highest_y], max_y_settled_piece] |> Enum.max()

    %{
      count: acc[:count] + 1,
      map: normalized_map,
      highest_y: new_highest_y,
      shapes_stream: new_shapes_stream,
      jet_streams: remaining_jet_streams
    }
  end
)