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

Day 17 - part2

day17-part2.livemd

Day 17 - part2

Mix.install([{:kino, "~> 0.5.0"}])
use Bitwise

Section

input = Kino.Input.textarea("paste input here:")
jets = ">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>"
jets = input |> Kino.Input.read() |> String.trim()
blocks = [
  [{0, 0}, {1, 0}, {2, 0}, {3, 0}],
  [{1, 0}, {0, 1}, {1, 1}, {2, 1}, {1, 2}],
  [{0, 0}, {1, 0}, {2, 0}, {2, 1}, {2, 2}],
  [{0, 0}, {0, 1}, {0, 2}, {0, 3}],
  [{0, 0}, {1, 0}, {0, 1}, {1, 1}]
]
defmodule Tetris do
  def height(tower) do
    tower
    |> Enum.map(&amp;elem(&amp;1, 1))
    |> Enum.max(&amp;>=/2, fn -> 0 end)
    |> Kernel.+(1)
  end

  def pprint({bx, by}, block, tower) do
    for y <- max(height(tower), by + 4)..0 do
      for x <- 0..6 do
        cond do
          MapSet.member?(tower, {x, y}) -> ?#
          {x - bx, y - by} in block -> ?@
          true -> ?\s
        end
      end
      |> then(&amp;('|' ++ &amp;1 ++ '|'))
      |> IO.puts()
    end

    IO.puts('+-------+')
  end

  # can be case like -----
  #                   
  #                      -----
  # so can't just check lowest y in each column, 1000 below lowest y is hacky heuristic
  # works in non-pathological cases
  def wrong_elim(tower) do
    new_floor =
      0..6
      |> Enum.map(fn x ->
        for {^x, y} <- tower do
          y
        end
        |> Enum.max(&amp;>=/2, fn -> -1 end)
      end)
      |> Enum.min()

    new_floor = new_floor - 1000

    if new_floor >= 0 do
      # IO.puts("eliminating row up to:")
      # IO.inspect(new_floor)
      old_floor = Process.get(:floor, 0)
      Process.put(:floor, old_floor + new_floor + 1)

      for {x, y} <- tower,
          y > new_floor,
          into: MapSet.new() do
        {x, y - new_floor - 1}
      end
    else
      tower
    end
  end

  def elim_full_rows(tower) do
    new_floor =
      height(tower)..0
      |> Enum.find(fn y ->
        Enum.all?(0..6, &amp;MapSet.member?(tower, {&amp;1, y}))
      end)

    if new_floor do
      IO.puts("eliminating row up to:")
      IO.inspect(new_floor)
      old_floor = Process.get(:floor, 0)
      Process.put(:floor, old_floor + new_floor + 1)

      for {x, y} <- tower,
          y > new_floor,
          into: MapSet.new() do
        {x, y - new_floor - 1}
      end
    else
      tower
    end
  end

  def has_full_row?(tower) do
    for y <- 0..height(tower) do
      Enum.all?(0..6, &amp;MapSet.member?(tower, {&amp;1, y}))
    end
    |> Enum.any?()
  end

  def free?({x, y}, block, tower) do
    # IO.puts("free?")
    # IO.inspect([{x, y}, block, tower])

    for {dx, dy} <- block do
      {bx, by} = {x + dx, y + dy}

      not MapSet.member?(tower, {bx, by}) and
        bx >= 0 and bx < 7 and
        by >= 0
    end
    |> Enum.all?()
  end

  def add_block({x, y}, block, tower) do
    for {bx, by} <- block, reduce: tower do
      tower ->
        MapSet.put(tower, {x + bx, y + by})
    end
  end

  def move(_, {tower, coord, [block | _], counter}) when counter == 1_000_000_000_000 do
    pprint(coord, block, tower)
    IO.inspect(has_full_row?(tower))
    {:halt, nil}
  end

  def move({jet, i}, {tower, {x, y}, [block | blocks], counter}) do
    seen = Process.get(:seen, Map.new())
    hash = :erlang.phash2({i, tower, x, y, block}, 1 <<< 32)

    counter =
      if Map.has_key?(seen, hash) and counter <= 500_000 do
        IO.puts("cycle!")
        {old_counter, old_floor} = seen[hash]
        new_floor = Process.get(:floor, 0)

        extra_floor =
          (new_floor - old_floor) * div(1_000_000_000_000 - counter, counter - old_counter)

        Process.put(:floor, new_floor + extra_floor)
        1_000_000_000_000 - rem(1_000_000_000_000 - counter, counter - old_counter)
      else
        Process.put(:seen, Map.put(seen, hash, {counter, Process.get(:floor, 0)}))
        counter
      end

    dx = if jet == ?>, do: 1, else: -1

    {x, y} =
      if free?({x + dx, y}, block, tower) do
        {x + dx, y}
      else
        {x, y}
      end

    if(free?({x, y - 1}, block, tower)) do
      {
        [height(tower)],
        {tower, {x, y - 1}, [block | blocks], counter}
      }
    else
      tower = add_block({x, y}, block, tower)
      # tower = elim_full_rows(tower)
      tower = wrong_elim(tower)
      height = height(tower)

      {
        [height],
        {
          tower,
          {2, height + 3},
          blocks ++ [block],
          counter + 1
        }
      }
    end
  end
end
defmodule Fast do
  def fast(jets, blocks) do
    jets
    |> to_charlist()
    |> Stream.cycle()
    |> Stream.zip(Stream.cycle(1..length(to_charlist(jets))))
    |> Stream.transform({MapSet.new(), {2, 3}, blocks, 0}, &amp;Tetris.move/2)
    # |> Stream.take(100000)
    |> Enum.at(-1)

    # |> Enum.take(100)
    # |> Enum.to_list()
    # |> Enum.join(" ")
    # |> IO.puts()
  end
end
Process.delete(:floor)
Process.delete(:seen)
Fast.fast(jets, blocks) + Process.get(:floor)