|
| 1 | +"""Day 10: Cathode-Ray Tube |
| 2 | +
|
| 3 | +This module provides the solution for Advent of Code 2022 - Day 10. |
| 4 | +
|
| 5 | +It simulates a simple CPU with register operations (addx, noop) and tracks |
| 6 | +signal strengths at specific cycles. Part 2 renders a CRT display based on |
| 7 | +the register value during each cycle. |
| 8 | +
|
| 9 | +The module contains a CPU class for simulation and a Solution class that |
| 10 | +inherits from SolutionBase. |
| 11 | +""" |
| 12 | + |
| 13 | +import re |
| 14 | +from typing import ClassVar |
| 15 | + |
| 16 | +from loguru import logger |
| 17 | + |
| 18 | +from aoc.models.base import SolutionBase |
| 19 | + |
| 20 | + |
| 21 | +class CPU: |
| 22 | + """Simulate a simple CPU with register X and cycle-based operations. |
| 23 | +
|
| 24 | + The CPU executes two instructions: addx (takes 2 cycles, adds value to X) |
| 25 | + and noop (takes 1 cycle, does nothing). Register X starts at 1 and is used |
| 26 | + to control both signal strength calculations and CRT sprite positioning. |
| 27 | + """ |
| 28 | + |
| 29 | + REGEX: ClassVar[re.Pattern] = re.compile(r"^(addx) (-?\d+)$|^(noop)$") |
| 30 | + |
| 31 | + def __init__(self) -> None: |
| 32 | + """Initialize CPU with register X at 1 and cycle counter at 1.""" |
| 33 | + self.register_X = 1 |
| 34 | + self.cycle = 1 |
| 35 | + self.waiting_add = 0 |
| 36 | + self.wait = 0 |
| 37 | + self.sum_of_registers = 0 |
| 38 | + self.display: str = "" |
| 39 | + |
| 40 | + def run(self, command: str) -> None: |
| 41 | + """Parse and queue a command for execution. |
| 42 | +
|
| 43 | + Args: |
| 44 | + command: Either "addx V" or "noop" instruction |
| 45 | + """ |
| 46 | + match = self.REGEX.match(command) |
| 47 | + |
| 48 | + if match and match.group(1) == "addx": |
| 49 | + self.wait = 2 |
| 50 | + self.waiting_add = int(match.group(2)) |
| 51 | + |
| 52 | + else: # noop |
| 53 | + self.wait = 1 |
| 54 | + self.waiting_add = 0 |
| 55 | + |
| 56 | + def advance_cycle(self, part_2: bool | None = None) -> None: |
| 57 | + """Advance one CPU cycle, updating signal strength and register. |
| 58 | +
|
| 59 | + Signal strength is calculated at cycles 20, 60, 100, 140, 180, 220. |
| 60 | + Register X is updated after an instruction completes its wait cycles. |
| 61 | +
|
| 62 | + Args: |
| 63 | + part_2: If True, also render CRT pixel for this cycle |
| 64 | + """ |
| 65 | + if self.cycle in [20, 60, 100, 140, 180, 220]: |
| 66 | + self.sum_of_registers += self.cycle * self.register_X |
| 67 | + |
| 68 | + if part_2: |
| 69 | + self.draw_pixel() |
| 70 | + |
| 71 | + # Advance cycle |
| 72 | + self.cycle += 1 |
| 73 | + self.wait -= 1 |
| 74 | + |
| 75 | + # Update register when instruction completes |
| 76 | + if self.wait == 0: |
| 77 | + self.register_X += self.waiting_add |
| 78 | + |
| 79 | + def draw_pixel(self) -> None: |
| 80 | + """Draw one CRT pixel based on sprite position and current cycle. |
| 81 | +
|
| 82 | + The CRT is 40 pixels wide. A 3-pixel wide sprite is centered at |
| 83 | + register X position. If the current pixel position overlaps with |
| 84 | + the sprite, draw '#', otherwise draw '.'. |
| 85 | + """ |
| 86 | + sprite_positions = [self.register_X - 1, self.register_X, self.register_X + 1] |
| 87 | + pixel_position = (self.cycle - 1) % 40 |
| 88 | + |
| 89 | + if pixel_position in sprite_positions: |
| 90 | + self.display += "#" |
| 91 | + |
| 92 | + else: |
| 93 | + self.display += "." |
| 94 | + |
| 95 | + if len(self.display) == 40: |
| 96 | + logger.info(f"{self.display}") |
| 97 | + self.display = "" |
| 98 | + |
| 99 | + |
| 100 | +class Solution(SolutionBase): |
| 101 | + """Simulate CPU operations and render CRT display. |
| 102 | +
|
| 103 | + This solution implements a simple CPU simulator that executes addx and noop |
| 104 | + instructions over multiple cycles. Part 1 calculates signal strengths at |
| 105 | + specific cycles (20, 60, 100, 140, 180, 220). Part 2 renders a 40-pixel |
| 106 | + wide CRT display where pixels are lit based on sprite position. |
| 107 | + """ |
| 108 | + |
| 109 | + def solve_part(self, data: list[str], *, part_2: bool = False) -> CPU: |
| 110 | + """Run CPU simulation through all instructions. |
| 111 | +
|
| 112 | + Processes instructions sequentially, advancing the CPU cycle-by-cycle |
| 113 | + until all instructions complete and the CPU becomes idle. |
| 114 | +
|
| 115 | + Args: |
| 116 | + data: List of CPU instructions (addx or noop) |
| 117 | + part_2: If True, enable CRT display rendering |
| 118 | +
|
| 119 | + Returns |
| 120 | + ------- |
| 121 | + CPU: CPU instance after all instructions complete with accumulated |
| 122 | + signal strengths and display output |
| 123 | + """ |
| 124 | + cpu = CPU() |
| 125 | + instructions = data.copy() |
| 126 | + |
| 127 | + while len(instructions) > 0 or cpu.wait > 0: |
| 128 | + # If CPU is idle, load next instruction |
| 129 | + if cpu.wait == 0 and len(instructions) > 0: |
| 130 | + line = instructions.pop(0) |
| 131 | + cpu.run(line) |
| 132 | + |
| 133 | + # Advance one cycle |
| 134 | + cpu.advance_cycle(part_2=part_2) |
| 135 | + |
| 136 | + return cpu |
| 137 | + |
| 138 | + def part1(self, data: list[str]) -> int: |
| 139 | + """Calculate sum of signal strengths at cycles 20, 60, 100, 140, 180, 220. |
| 140 | +
|
| 141 | + Signal strength is the cycle number multiplied by register X value during |
| 142 | + that cycle. The sum provides insight into CPU behavior during execution. |
| 143 | +
|
| 144 | + Args: |
| 145 | + data: List of CPU instructions |
| 146 | +
|
| 147 | + Returns |
| 148 | + ------- |
| 149 | + int: Sum of signal strengths at the six specified cycles |
| 150 | + """ |
| 151 | + cpu = self.solve_part(data) |
| 152 | + return cpu.sum_of_registers |
| 153 | + |
| 154 | + def part2(self, data: list[str]) -> None: |
| 155 | + """Render CRT display output to console. |
| 156 | +
|
| 157 | + The CRT draws 40x6 pixels, rendering one pixel per cycle. A 3-pixel |
| 158 | + sprite centered at register X determines if each pixel is lit ('#') |
| 159 | + or dark ('.'). The output spells an 8-letter message. |
| 160 | +
|
| 161 | + Args: |
| 162 | + data: List of CPU instructions |
| 163 | +
|
| 164 | + Returns |
| 165 | + ------- |
| 166 | + None: Display is printed to console during execution |
| 167 | + """ |
| 168 | + _ = self.solve_part(data, part_2=True) |
0 commit comments