Advent of Code 2022 - Day 10

This one was really fun contrary to other “implement something CPU like” puzzles in the past. I used a bunch of Stream API and a module for scoping the CRT logic. I really liked the visual component of part 2.

Solution
defmodule Day10 do
  defmodule CRT do
    defstruct pixels: [], index: 0

    def render_pixel(state, x) do
      pixel = if state.index in (x - 1)..(x + 1), do: "#", else: "."

      %__MODULE__{
        pixels: [pixel | state.pixels],
        index: rem(state.index + 1, 40)
      }
    end

    def render(state) do
      state.pixels
      |> Enum.reverse()
      |> Enum.chunk_every(40)
      |> Enum.join("\n")
    end
  end

  def run(text) do
    text
    |> program()
    |> Stream.filter(fn {_, cycle} -> cycle in [20, 60, 100, 140, 180, 220] end)
    |> Stream.map(fn {x, cycle} -> x * cycle end)
    |> Enum.take(6)
    |> Enum.sum()
  end

  def render_to_crt(text) do
    text
    |> program()
    |> Stream.take_while(fn {_, cycle} -> cycle <= 240 end)
    |> Enum.reduce(%CRT{}, fn {x, _}, crt ->
      CRT.render_pixel(crt, x)
    end)
    |> CRT.render()
  end

  defp program(text) do
    text
    |> String.split("\n", trim: true)
    |> Stream.transform(1, fn
      "noop", x -> {[x], x}
      "addx " <> num, x -> {[x, x], x + String.to_integer(num)}
    end)
    |> Stream.with_index(1)
  end
end
1 Like

I had a very clunky version that got me the gold stars, but after some coffee I ended up with a super neat version using Stream.transform.

  input
  |> Stream.transform({1, 1}, fn 
    :noop, {cycle, x} ->
      {[{cycle, x}], {cycle + 1, x}}
    {:addx, value}, {cycle, x} ->
      {[{cycle, x}, {cycle + 1, x}], {cycle + 2, x + value}}
  end)

That will give you a stream of {cycle_no, x} tuples that you could use to find the solution to the other parts. input is parsed in to a list of tuples, the small example would look like the [:noop, {:addx, 3}, {:addx, -5}].

Just pipe that stream into this for the second part :slight_smile:

cycle_stream # from above
|> Stream.map(fn 
  {pc, x} when rem(pc-1, 40) in (x-1)..(x+1) -> ?#
  _ -> ?\s
end)
|> Stream.chunk_every(40)
|> Enum.intersperse(?\n)
|> IO.puts

Elixir standard library is so awesomely good.

2 Likes

No standard library for me, just a couple of recursive functions.

Both sub-millisecond. Part 1 especially completes in less than 20 microseconds.

You’re convincing me to look into Stream more for these recursive answers though, 5 parameter 6 clause functions are not the height of readability :see_no_evil:

Newbie programmer here, I really enjoyed today’s puzzle. Here’s my attempt.

Day 10

I don’t know Elixir that well, but IMHO quite nice, nothing special though: AdventOfCode22/aoc.ex at main · pistelak/AdventOfCode22 · GitHub

In the end I’m reasonably pleased with my solution but I had a lot of frustration getting there. Had an off by one error in my initial attempt at part 1 that only showed up with the real input and not the sample data. Part 2 was actually much easier but when I pasted the sample data output into vim for testing one of my plugins appended a # to each line after the first thinking they were meant to be comments. I didn’t realize it and couldn’t understand why my solution was not passing the test. Classic bad input is going to give bad output.

def execute(cmds, cycles) do
    cmds
    |> Enum.reduce_while({%{0 => 1, 1 => 1}, 1}, fn cmd, {register, cycle} ->
      if cycle > cycles do
        {:halt, {register, cycle}}
      else
        {:cont, exec(cmd, register, cycle)}
      end
    end)
    |> elem(0)
  end

  defp exec(cmd, register, cycle) do
    case cmd do
      "noop" ->
        register = Map.put(register, cycle + 1, register[cycle])
        {register, cycle + 1}

      <<"addx ", val::binary>> ->
        last_val = register[cycle]

        register =
          register
          |> Map.put(cycle + 1, last_val)
          |> Map.put(cycle + 2, last_val + String.to_integer(val))

        {register, cycle + 2}
    end
  end

  defp signal_strengths(signal_map, cycles) do
    cycles |> Enum.map(&signal_strength(&1, signal_map))
  end

  defp signal_strength(cycle, signal_map) do
    signal_map[cycle] * cycle
  end

  def part1(input) do
    input
    |> execute(220)
    |> signal_strengths([20, 60, 100, 140, 180, 220])
    |> Enum.sum()
  end

  def part2(input) do
    input
    |> execute(:to_completion)
    |> render()
  end

  defp render(map) do
    0..(map_size(map) - 2)
    |> Enum.reduce("", fn cycle, render ->
      sprite_pos = map[cycle + 1]
      cycle = rem(cycle, 40)

      case {cycle == 0, abs(cycle - sprite_pos) < 2} do
        {true, true} -> render <> "\n" <> "#"
        {true, false} -> render <> "\n" <> "."
        {false, true} -> render <> "#"
        {false, false} -> render <> "."
      end
    end)
    |> String.split("\n", trim: true)
  end

It is probably faster with handrolled recursive functions, but it can be quite dense to read :sweat_smile:

There are a lot of gems hidden in the standard library:)

1 Like

Just curious if this really works:
if Enum.member?(i.x..i.x+2, rem(i.cycle, 40))
because I would have thought it needs to be (i.x - 1)..(i.x + 1) since the sprite position is given by it’s middle pixel position.

You are right, there is something wrong. :sweat_smile: I tried to someone’s else solution and the result is slightly different: Screenshot 2022-12-10 at 18.56.23 2022-12-10 at 7.00.50 PM … Will take a look.

1 Like

Error by one :sweat: - cycles are indexed from 1 but CRT starts drawing at 0. So
Enum.member?(i.x-1..i.x+1, rem(i.cycle - 1, 40)) is correct.

1 Like

Nothing special/similar to others. Reminded me of 2021-13 which also had you print out letters with .'s and #'s. So had some fun and added some IO.ANSI to “pretty” print, e.g.

3 Likes

Nice touch with the ANSI stuff :slight_smile:

1 Like

This one was definitely interesting but I thought the second part was very unclear. I though for sure that the sprite position would work the same as the pixel position where the second row was indicated by x values over 40 and I was stumped when it never went over 40.

The divmod function is plagiarized from stack overflow :grimacing:

1 Like

December was a busy month so just today I was able to finish this one pensandoemelixir/day102022_v2.ex at main · adolfont/pensandoemelixir · GitHub