You avoid the ropes, plunge into the river, and swim to shore.
The Elves yell something about meeting back up with them upriver, but the river is too loud to tell exactly what they're saying. They finish crossing the bridge and disappear from view.
Situations like this must be why the Elves prioritized getting the communication system on your handheld device working. You pull it out of your pack, but the amount of water slowly draining from a big crack in its screen tells you it probably won't be of much immediate use.
Unless, that is, you can design a replacement for the device's video system! It seems to be some kind of cathode-ray tube screen and simple CPU that are both driven by a precise clock circuit. The clock circuit ticks at a constant rate; each tick is called a cycle.
Start by figuring out the signal being sent by the CPU. The CPU has a single register, X
, which starts with the value 1
. It supports only two instructions:
addx V
takes two cycles to complete. After two cycles, the X
register is increased by the value V
. (V
can be negative.)noop
takes one cycle to complete. It has no other effect.The CPU uses these instructions in a program (your puzzle input) to, somehow, tell the screen what to draw.
Consider the following small program:
noop
addx 3
addx -5
Execution of this program proceeds as follows:
noop
instruction begins execution. During the first cycle, X
is 1
. After the first cycle, the noop
instruction finishes execution, doing nothing.addx 3
instruction begins execution. During the second cycle, X
is still 1
.X
is still 1
. After the third cycle, the addx 3
instruction finishes execution, setting X
to 4
.addx -5
instruction begins execution. During the fourth cycle, X
is still 4
.4
. After the fifth cycle, the addx -5
instruction finishes execution, setting X
to -1
.Maybe you can learn something by looking at the value of the X
register throughout execution. For now, consider the signal strength (the cycle number multiplied by the value of the X
register) during the 20th cycle and every 40 cycles after that (that is, during the 20th, 60th, 100th, 140th, 180th, and 220th cycles).
For example, consider this larger program:
addx 15
addx -11
addx 6
addx -3
addx 5
addx -1
addx -8
addx 13
addx 4
noop
addx -1
addx 5
addx -1
addx 5
addx -1
addx 5
addx -1
addx 5
addx -1
addx -35
addx 1
addx 24
addx -19
addx 1
addx 16
addx -11
noop
noop
addx 21
addx -15
noop
noop
addx -3
addx 9
addx 1
addx -3
addx 8
addx 1
addx 5
noop
noop
noop
noop
noop
addx -36
noop
addx 1
addx 7
noop
noop
noop
addx 2
addx 6
noop
noop
noop
noop
noop
addx 1
noop
noop
addx 7
addx 1
noop
addx -13
addx 13
addx 7
noop
addx 1
addx -33
noop
noop
noop
addx 2
noop
noop
noop
addx 8
noop
addx -1
addx 2
addx 1
noop
addx 17
addx -9
addx 1
addx 1
addx -3
addx 11
noop
noop
addx 1
noop
addx 1
noop
noop
addx -13
addx -19
addx 1
addx 3
addx 26
addx -30
addx 12
addx -1
addx 3
addx 1
noop
noop
noop
addx -9
addx 18
addx 1
addx 2
noop
noop
addx 9
noop
noop
noop
addx -1
addx 2
addx -37
addx 1
addx 3
noop
addx 15
addx -21
addx 22
addx -6
addx 1
noop
addx 2
addx 1
noop
addx -10
noop
noop
addx 20
addx 1
addx 2
addx 2
addx -6
addx -11
noop
noop
noop
The interesting signal strengths can be determined as follows:
X
has the value 21
, so the signal strength is 20 * 21 = 420. (The 20th cycle occurs in the middle of the second addx -1, so the value of register X is the starting value, 1, plus all of the other addx values up to that point: 1 + 15 - 11 + 6 - 3 + 5 - 1 - 8 + 13 + 4 = 21.)X
has the value 19
, so the signal strength is 60 * 19 = 1140.X
has the value 18
, so the signal strength is 100 * 18 = 1800.X
has the value 21
, so the signal strength is 140 * 21 = 2940.X
has the value 16
, so the signal strength is 180 * 16 = 2880.X
has the value 18
, so the signal strength is 220 * 18 = 3960.The sum of these signal strengths is 13140.
Find the signal strength during the 20th, 60th, 100th, 140th, 180th, and 220th cycles. What is the sum of these six signal strengths?
# Python imports
from __future__ import annotations
import numpy as np
from pathlib import Path
test = Path("data/day10_test.txt") # test data
test2 = Path("data/day10_test2.txt") # more test data
data = Path("data/day10_data.txt") # puzzle data
class CPU:
"""Represents the CPU of the elves' comunicator"""
def __init__(self, display: CRT=None) -> None:
"""Instantiate CPU for elves' communicator
:param display: CRT object for callback/display
"""
self.display = display # holds output display (for callback)
self.regx = 1 # register for X position
self.cycle_count = 0 # current clock cycle in CPU
self.addx_buffer = [] # two-cycle buffer for addX operation
self.states = [] # list of (cycle, X) states
# Distribution dictionary for instruction operations
self.instrs = {"noop": self.noop,
"addx": self.addx}
def cycle(self) -> None:
"""Run the CPU for a clock cycle"""
self.cycle_count += 1 # increment the cell cycle count
self.states.append((self.cycle_count, self.X)) # store value of X _during_ cycle
# If there's a display, update it with the X value during the cycle
if self.display is not None:
self.display.update(self.cycle_count, self.X)
# If the addX buffer is not empty, pop the next value off it
if len(self.addx_buffer):
self.regx += self.addx_buffer.pop(0)
def addx(self, val: int) -> None:
"""addX operation
:param val: the X to be added
To ensure that two cycles are occupied, we use a buffer that
is popped from the left at each cycle, and add two values:
0 first, then val.
"""
self.addx_buffer.append(0)
self.addx_buffer.append(val)
def noop(self) -> None:
"""noop"""
pass
def exec_instr(self, instr: list[str] | list[str, int]):
"""Execute a single instruction
:param instr: the instruction
Uses the internal distribution dictionary
"""
if instr[0] == "noop": # noop
self.instrs["noop"]()
else: # non-noop instructions
self.instrs[instr[0]](instr[1])
def load_instructions(self, instructions: list[list[str] | list[str, int]]) -> None:
"""Load instructions into the CPU
:param instructions: instructions for the CPU
"""
self.instructions = instructions
def execute(self, cycles: int) -> None:
"""Run CPU for a number of cycles
:param cycles: the number of cycles to run for
"""
for cycle in range(cycles):
if not len(self.addx_buffer): # wait to execute pending commands
if len(self.instructions): # run pending instruction
self.exec_instr(self.instructions.pop(0))
self.cycle() # cycle CPU
@property
def X(self) -> int:
"""Return current value in X register"""
return self.regx
def parse_input(fpath: Path):
"""Parse puzzle input into
:param fpath: path to puzzle input
"""
with fpath.open() as ifh:
instructions = [_.strip().split() for _ in ifh.readlines()]
for instr in instructions:
if len(instr) == 2: # if there are two values, cast second as int
instr[1] = int(instr[1])
return instructions
def part1(fpath: Path, cycles: int) -> int | list[tuple[int, int]]:
"""Solve the puzzle
:param fpath: path to puzzle input
:param cycles: number of cycles to run the CPU
"""
cpu = CPU()
cpu.load_instructions(parse_input(fpath))
cpu.execute(cycles)
if cycles > 5: # second test and puzzle
print(sum([cpu.states[idx][0] * cpu.states[idx][1] for idx in range(19, 220, 40)]))
else: # first test data
return cpu.states
part1(test, 5)
[(1, 1), (2, 1), (3, 1), (4, 4), (5, 4)]
part1(test2, 220)
13140
part1(data, 220)
14240
It seems like the X
register controls the horizontal position of a sprite. Specifically, the sprite is 3 pixels wide, and the X
register sets the horizontal position of the middle of that sprite. (In this system, there is no such thing as "vertical position": if the sprite's horizontal position puts its pixels where the CRT is currently drawing, then those pixels will be drawn.)
You count the pixels on the CRT: 40 wide and 6 high. This CRT screen draws the top row of pixels left-to-right, then the row below that, and so on. The left-most pixel in each row is in position 0, and the right-most pixel in each row is in position 39.
Like the CPU, the CRT is tied closely to the clock circuit: the CRT draws a single pixel during each cycle. Representing each pixel of the screen as a #, here are the cycles during which the first and last pixel in each row are drawn:
Cycle 1 -> ######################################## <- Cycle 40
Cycle 41 -> ######################################## <- Cycle 80
Cycle 81 -> ######################################## <- Cycle 120
Cycle 121 -> ######################################## <- Cycle 160
Cycle 161 -> ######################################## <- Cycle 200
Cycle 201 -> ######################################## <- Cycle 240
So, by carefully timing the CPU instructions and the CRT drawing operations, you should be able to determine whether the sprite is visible the instant each pixel is drawn. If the sprite is positioned such that one of its three pixels is the pixel currently being drawn, the screen produces a lit pixel (#); otherwise, the screen leaves the pixel dark (.).
The first few pixels from the larger example above are drawn as follows:
Sprite position: ###.....................................
Start cycle 1: begin executing addx 15
During cycle 1: CRT draws pixel in position 0
Current CRT row: #
During cycle 2: CRT draws pixel in position 1
Current CRT row: ##
End of cycle 2: finish executing addx 15 (Register X is now 16)
Sprite position: ...............###......................
Start cycle 3: begin executing addx -11
During cycle 3: CRT draws pixel in position 2
Current CRT row: ##.
During cycle 4: CRT draws pixel in position 3
Current CRT row: ##..
End of cycle 4: finish executing addx -11 (Register X is now 5)
Sprite position: ....###.................................
Start cycle 5: begin executing addx 6
During cycle 5: CRT draws pixel in position 4
Current CRT row: ##..#
During cycle 6: CRT draws pixel in position 5
Current CRT row: ##..##
End of cycle 6: finish executing addx 6 (Register X is now 11)
Sprite position: ..........###...........................
Start cycle 7: begin executing addx -3
During cycle 7: CRT draws pixel in position 6
Current CRT row: ##..##.
During cycle 8: CRT draws pixel in position 7
Current CRT row: ##..##..
End of cycle 8: finish executing addx -3 (Register X is now 8)
Sprite position: .......###..............................
Start cycle 9: begin executing addx 5
During cycle 9: CRT draws pixel in position 8
Current CRT row: ##..##..#
During cycle 10: CRT draws pixel in position 9
Current CRT row: ##..##..##
End of cycle 10: finish executing addx 5 (Register X is now 13)
Sprite position: ............###.........................
Start cycle 11: begin executing addx -1
During cycle 11: CRT draws pixel in position 10
Current CRT row: ##..##..##.
During cycle 12: CRT draws pixel in position 11
Current CRT row: ##..##..##..
End of cycle 12: finish executing addx -1 (Register X is now 12)
Sprite position: ...........###..........................
Start cycle 13: begin executing addx -8
During cycle 13: CRT draws pixel in position 12
Current CRT row: ##..##..##..#
During cycle 14: CRT draws pixel in position 13
Current CRT row: ##..##..##..##
End of cycle 14: finish executing addx -8 (Register X is now 4)
Sprite position: ...###..................................
Start cycle 15: begin executing addx 13
During cycle 15: CRT draws pixel in position 14
Current CRT row: ##..##..##..##.
During cycle 16: CRT draws pixel in position 15
Current CRT row: ##..##..##..##..
End of cycle 16: finish executing addx 13 (Register X is now 17)
Sprite position: ................###.....................
Start cycle 17: begin executing addx 4
During cycle 17: CRT draws pixel in position 16
Current CRT row: ##..##..##..##..#
During cycle 18: CRT draws pixel in position 17
Current CRT row: ##..##..##..##..##
End of cycle 18: finish executing addx 4 (Register X is now 21)
Sprite position: ....................###.................
Start cycle 19: begin executing noop
During cycle 19: CRT draws pixel in position 18
Current CRT row: ##..##..##..##..##.
End of cycle 19: finish executing noop
Start cycle 20: begin executing addx -1
During cycle 20: CRT draws pixel in position 19
Current CRT row: ##..##..##..##..##..
During cycle 21: CRT draws pixel in position 20
Current CRT row: ##..##..##..##..##..#
End of cycle 21: finish executing addx -1 (Register X is now 20)
Sprite position: ...................###..................
Allowing the program to run to completion causes the CRT to produce the following image:
##..##..##..##..##..##..##..##..##..##..
###...###...###...###...###...###...###.
####....####....####....####....####....
#####.....#####.....#####.....#####.....
######......######......######......####
#######.......#######.......#######.....
Render the image given by your program. What eight capital letters appear on your CRT?
class CRT:
"""Represents the CRT of the elves' communicator"""
def __init__(self) -> None:
"""Instantiate CRT with internal CPU"""
self.cpu = CPU(self) # CPU for executing instructions
self.display = np.zeros(240) # 1D array
def load_instructions(self, instructions: list[list[str] | list[str, int]]) -> None:
"""Load instructions for CRT into CPU
:param instructions: instructions for CRT
"""
self.cpu.load_instructions(instructions)
def run(self, cycles: int=240) -> None:
"""Run CPU for a set number of cycles
:param cycles: number of cycles to run CPU
"""
self.cpu.execute(cycles)
def update(self, cycle: int, spritepos: int) -> None:
"""Callback: CPU updates the CRT display at a named cycle
:param cycle: current CPU cycle
:param spritepos: position of sprite during this cycle
Pixel displays at position cycle (mod 40) in the display, if
the sprite centre (CPU X register value) is one pixel or less
from that position.
"""
if abs(spritepos - (cycle - 1) % 40) < 2:
self.display[cycle-1] = 1
def __str__(self) -> str:
"""Return string representation of CRT display"""
display = self.display.reshape(6,40) # reshape array for display
outstr = [] # hold display image
for row in display: # convert each row of the array to string
curstr = ""
for elem in row:
if elem == 0:
curstr += "."
else:
curstr += "#"
outstr.append(curstr)
return "\n".join(outstr)
def part2(fpath: Path):
"""Solve the puzzle
:param fpath: path to puzzle input
"""
instructions = parse_input(fpath)
crt = CRT()
crt.load_instructions(instructions)
crt.run()
print(crt)
part2(test2)
##..##..##..##..##..##..##..##..##..##.. ###...###...###...###...###...###...###. ####....####....####....####....####.... #####.....#####.....#####.....#####..... ######......######......######......#### #######.......#######.......#######.....
part2(data)
###..#....#..#.#....#..#.###..####.#..#. #..#.#....#..#.#....#.#..#..#....#.#..#. #..#.#....#..#.#....##...###....#..####. ###..#....#..#.#....#.#..#..#..#...#..#. #....#....#..#.#....#.#..#..#.#....#..#. #....####..##..####.#..#.###..####.#..#.