Published November 26, 2022

Another year, another 25 curious code challenges from Advent of Code! This year, I'll be attempting to make as many solutions as possible something you can run right here in your browser window via PyScript.

## Day 0 - Testing the Machinery

In preparation for this year's AoC, I've set up a Hugo templating system to allow me to quickly write and share each day's code. The setup will look much like this, with a brief explanation here, the code below, and an option to run live demos. The `get_input` function handles getting input from the textarea or file upload.

• day0.py
 ``````1 2 3 4 5 6 `````` ``````from utils import get_input def main_day0(): display(f"Input given: {get_input('day0')}", target="day0-output", append=False)``````

## Day 1: Calorie Counting (Part 1)

And we're off! As common for day 1 of AoC, this puzzle is is about making sure you can read input and identify line breaks, and do some very simple parsing.

One way to solve this problem would be to calculate the sum of calories in every elf's pack, then find the max of those. Astute coders will notice that you don't need to actually hold all the packs in memory at once; you can calculate them one at a time and retain the highest value seen so far, which avoids undue memory usage. Python more-or-less does this for us if we use generator expressions for everything.

• day1_1.py
• day1_1-viz.py
 ``````1 2 3 4 5 6 `````` ``````def main_day1_1(): elf_packs = (get_input('day1_1').split('\n\n')) elf_calories = (sum(int(line) for line in pack.split('\n')) for pack in elf_packs) display(f"{max(elf_calories)= }", target="day1_1-output", append=False)``````
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 `````` ``````from js import Chart, document, Object from pyodide.ffi import to_js import asyncio def j(obj): return to_js(obj, dict_converter=Object.fromEntries) def viz_day1_1(): asyncio.ensure_future(viz_day1_1_coro()) async def viz_day1_1_coro(): if ctx:= document.getElementById("day1_1-viz-canvas") is None: ctx = document.createElement("canvas") ctx.id = "day1_1-viz-canvas" parent = document.getElementById("day1_1-viz") parent.appendChild(ctx) parent.style.width="48rem" parent.style.height="24rem" parent.style.position = 'relative' elf_packs = (get_input('day1_1').split('\n\n')) elf_calories = [sum(int(line) for line in pack.split('\n')) for pack in elf_packs] most = max(elf_calories) most_index = elf_calories.index(most) display(f"{max(elf_calories)= }", target="day1_1-output", append=False) my_chart = Chart.new(ctx, j({ "type": "bar", "data": j({ "labels": [f"Elf {i}" for i in range(len(elf_packs))], "datasets": [j({ "label": "data", "data": elf_calories, "stack": 1, "backgroundColor": ['rgba(75,192,192,0.4)' if cal != most else 'red' for cal in elf_calories ] })] }), "options": j({ "animation": False, "responsive": True, "plugins": j({ "legend": j({ "display": False }), }), "scales": j({ "x": j({ "stacked": True }), "y": j({ "beginAtZero": True }) }) }) }))``````
Scroll to see complete code

## Day 1: Calorie Counting (Part 2)

Another common theme with Advent of Code - part 2 on a given day will try to subvert the optimizations you may have made in part 1!

For a quick-and-dirty solution here, I'll use the `sorted` function to convert our generator into a sorted list, then sum the last (largest) three elements. If this were a larger list of elements, we could come up with our own generator that injested elements from an Iterable one by one, and retained the largest three.

• day1_2.py
 ``````1 2 3 4 5 6 `````` ``````def main_day1_2(): elf_packs = (get_input('day1_2').split('\n\n')) elf_calories = sorted(sum(int(line) for line in pack.split('\n')) for pack in elf_packs) display(f"{sum(elf_calories[-3:])= }", target="day1_2-output", append=False)``````

## Day 2: Rock Paper Scissors (Part 1)

Some slightly more complicated input handling today, with some slightly more involved conditional logic to accumulate a score

It's also quite useful to me to be able to run these examples from the terminal, as well as in PyScript. If you look at the end of today's code, you'll see a use of checking whether we're running in pyodide (`if 'pyodide' in sys.modules`), and chosing where to snag the input from based on that determination.

• day2_1.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 `````` ``````import sys def translateLine(s): return s.translate(str.maketrans({"A": "X", "B": "Y", "C": "Z"})) def scoreFromStrategy(theirs, mine): selectedShapePoints = {"X": 1, "Y": 2, "Z": 3} points = selectedShapePoints[mine] if theirs == mine: #draw points += 3 elif ((theirs, mine) == ("X", "Y") or (theirs, mine) == ("Y", "Z") or (theirs, mine) == ("Z", "X")): #win points += 6 return points def scoreFromInput(data): return sum(scoreFromStrategy(*translateLine(line).split(" ")) for line in data) if 'pyodide' in sys.modules: def main_day2_1(): data = get_input('day2_1').split('\n') display(f"{scoreFromInput(data)= }", target="day2_1-output", append=False) else: if __name__ == "__main__": with open('input.txt', 'r') as fp: data = fp.read().split('\n') print(f"{scoreFromInput(data)= }")``````
Scroll to see complete code

## Day 2: Rock Paper Scissors (Part 2)

I fully admit to my solution here trying to be way too clever. The speediest way to solve this problem (both in execution time and in writing) would almost certainly be to create a looking table of the 9 possible input lines with their resultant scores, and just loop over the input and sum according to those scores.

That said, this is a good chance to start stirring the brain cells on another common theme in Advnent of Code challenges - using one part of the input to determine how to interpret another part of the input.

• day2_2.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 `````` ``````from itertools import cycle import sys def translateLine(s): return s.translate(str.maketrans({"A": "X", "B": "Y", "C": "Z"})) def scoreFromStrategy(theirs, result): theirsIndex = ["X", "Y", "Z"].index(theirs) # X,Y,Z => 0,1,2 relativeIndex = {"X": 2, "Y": 0, "Z": 1} #Offset by whether we lose, win, or draw selectedShape = ["X", "Y", "Z"][(theirsIndex + relativeIndex[result]) % 3] #Find our choice selectedShapePoints = {"X": 1, "Y": 2, "Z": 3} #Points for our choice resultPoints = {"X": 0, "Y": 3, "Z": 6} score = selectedShapePoints[selectedShape] + resultPoints[result] return score def scoreFromInput(data): return sum(scoreFromStrategy(*translateLine(line).split(" ")) for line in data) if 'pyodide' in sys.modules: def main_day2_2(): data = get_input('day2_2').split('\n') display(f"{scoreFromInput(data)= }", target="day2_2-output", append=False) else: if __name__ == "__main__": with open('input.txt', 'r') as fp: data = fp.read().split('\n') print(f"{scoreFromInput(data)= }")``````
Scroll to see complete code

## Day 3: Rucksack Reorganization (Part 1)

This is one of those neat days where one can use a neat feature of Python - set operations - to make finding common elements between two iterables fast and easy.

On a whim, I hopped on a livestream and whipped up a visualization of this part of the solution, which you can check out if you run the live examples on this page.

• day3_1.py
• day3_1-viz.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 `````` ``````import string import sys charValue = {s: index + 1 for index, s in enumerate(string.ascii_lowercase)} |\ {s: index + 27 for index, s in enumerate(string.ascii_uppercase)} def prioritySum(data): sum = 0 for line in data: midpoint = int(len(line)/2) first = set(line[:midpoint]) second = set(line[midpoint:]) sum += charValue[(first & second).pop()] return sum if 'pyodide' in sys.modules: def main_day3_1(): data = get_input('day3_1').split('\n') display(f"{prioritySum(data)= }", target="day3_1-output", append=False) elif __name__ == "__main__": with open("input.txt", "r") as fp: data = fp.read() result = prioritySum(data.split('\n')) print(f"{result= }") ``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 `````` ``````import string import sys charValue = {s: index + 1 for index, s in enumerate(string.ascii_lowercase)} |\ {s: index + 27 for index, s in enumerate(string.ascii_uppercase)} def prioritySum(data): sum = 0 for line in data: midpoint = int(len(line)/2) first = set(line[:midpoint]) second = set(line[midpoint:]) sum += charValue[(first & second).pop()] return sum def visualizeProcessing(data, tbody): tbody.style.fontFamily = 'monospace' tbody.classList.add("unformatted-table") for line in data: row = js.document.createElement('tr') scoreElement = js.document.createElement('td') firstElement = js.document.createElement('td') secondElement = js.document.createElement('td') #styling row.classList.add("bg-gray-900", "text-gray-300") scoreElement.style.paddingRight = "2rem" firstElement.style.textAlign = "right" firstElement.style.paddingRight = "0.5rem" #Actually add the content midpoint = int(len(line)/2) first = line[:midpoint] second = line[midpoint:] commonChar = (set(first) & set(second)).pop() score = charValue[commonChar] first = first.replace(commonChar, f'{commonChar}') second = second.replace(commonChar, f'{commonChar}') #Format and insert text firstElement.innerHTML = "" + first + "" secondElement.innerHTML = "" + second + "" scoreElement.innerHTML = f"{commonChar}={score}" row.appendChild(scoreElement) row.appendChild(firstElement) row.appendChild(secondElement) tbody.appendChild(row) #grayRows = js.document.querySelectorAll("tbody tr:nth-child(2n-1)") grayRows = js.document.querySelectorAll("tr") for row in grayRows: js.console.log(row.style) row.style.removeProperty("background-color") def setupTable(): import js table = js.document.createElement('table') table.classList.add("w-full") tbody = js.document.createElement('tbody') table.appendChild(tbody) vizelem = js.document.getElementById("day3_1-viz") vizelem.classList.add("w-full") vizelem.classList.add("h-76") vizelem.classList.add("overflow-y-scroll") vizelem.appendChild(table) return tbody if 'pyodide' in sys.modules: import js def viz_day3_1(): tbody = setupTable() data = get_input('day3_1').split('\n') visualizeProcessing(data, tbody) display(f"{prioritySum(data)= }", target="day3_1-output", append=False) elif __name__ == "__main__": with open("input.txt", "r") as fp: data = fp.read() result = prioritySum(data.split('\n')) print(f"{result= }") ``````
Scroll to see complete code

## Day 3: Rucksack Reorganization (Part 2)

Similar to part 1, part 2 is much easier if you use your chosen language's set operations to quickly narrow down the given elements to only the ones common between each trio of elves. I suppose the "gotcha" in this part is meant to catch out anyone who implemented a nested-for-loop, check-each-element-one-by-one solution to part 1.

• day3_2.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 `````` ``````import string import sys from typing import Collection charValue = {s: index + 1 for index, s in enumerate(string.ascii_lowercase)} |\ {s: index + 27 for index, s in enumerate(string.ascii_uppercase)} def scoreBy3(data): sum = 0 for index, a in enumerate(data[::3]): b = data[index*3 + 1] c = data[index*3 + 2] common = (set(a) & set(b) & set(c)) assert len(common) == 1 common = common.pop() score = charValue[common] sum += score return sum if 'pyodide' in sys.modules: def main_day3_2(): data = get_input('day3_2').split('\n') display(f"{scoreBy3(data)= }", target="day3_2-output", append=False) elif __name__ == "__main__": with open("input.txt", "r") as fp: data = fp.read() result = scoreBy3(data.split('\n')) print(f"{result= }") ``````
Scroll to see complete code

## Day 4: Camp Cleanup (Part 1)

Some year, somewhere, I shant be tricked by forgetting to convert input strings to integers.

This is not that year apparently. The provided example input works if you only compair the inputs alphanumerically, since it only uses single-digit numbers, but the real input only yields the correct solution if you remember to convert the inputs to integers.

• day4_1.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 `````` ``````import sys def fullyContains(a: tuple[int, int] , b: tuple[int, int]) -> bool: return a <= b and a >= b def solutionFromInput(data): pairs = (line.split(',') for line in data) pairs = ((p.split('-'), p.split('-')) for p in pairs) pairs = (((int(p), int(p)), (int(p), int(p))) for p in pairs) matches = (fullyContains(pair, pair) or fullyContains(pair,pair) for pair in pairs) return sum(1 if m else 0 for m in matches) if 'pyodide' in sys.modules: def main_day4_1(): data = get_input('day4_1').split('\n') display(f"{solutionFromInput(data)= }", target="day4_1-output", append=False) else: if __name__ == "__main__": with open("input.txt", "r") as fp: data = fp.read().split("\n") print(solutionFromInput(data)) ``````
Scroll to see complete code

## Day 4: Camp Cleanup (Part 2)

I'm certain there's a more clever way to determine whether two ranges overlap; I've used the brute-force method to check if either of the endpoints of each pair lies within (or equals) the endpoints of the other pair. I have a feeling, from the symmetry of the boolean logic, that it could be simplified somehow, but this is functional.

• day4_2.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 `````` ``````import sys def overlaps(a: tuple[int, int] , b: tuple[int, int]) -> bool: return (a <= b <= a) or (a <= b <= a) or \ (b <= a <= b) or (b <= a <= b) def solutionFromInput(data): pairs = (line.split(',') for line in data) pairs = ((p.split('-'), p.split('-')) for p in pairs) pairs = (((int(p), int(p)), (int(p), int(p))) for p in pairs) matches = (overlaps(pair, pair) for pair in pairs) return sum(1 if m else 0 for m in matches) if 'pyodide' in sys.modules: def main_day4_2(): data = get_input('day4_2').split('\n') print(data) display(f"{solutionFromInput(data)= }", target="day4_2-output", append=False) else: if __name__ == "__main__": with open("input.txt", "r") as fp: data = fp.read().split("\n") print(solutionFromInput(data)) ``````
Scroll to see complete code

## Day 5: Supply Stacks (Part 1)

As I've experienced in previous years, the process of identifying a strategy or algorithm to solve a problem, and the creating the data structure for that algorithm, go hand in hand.

In today's case, the fact that the input is presented row-by-row, but the data is relevant column-by-column, means that a cerain amount of input processessing is necessary to make the data useful. But once it is, the solution is relatively straightfoward.

• day5_1.py
• day5_1-viz.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 `````` ``````from typing import NamedTuple import re import sys from typing import NewType # The 'yard' is the collection of all the stacks of crates yardType = NewType("yardType", dict[str:list]) class Instruction(NamedTuple): quantity: int from_stack: str to_stack: str #("Instruction", ('quantity', 'from_stack', 'to_stack')) def solutionFromInput(data: str) -> str: data = data.split("\n") stacks, instructions = parseInput(data) for ins in instructions: stacks = operateOn(stacks, ins) return topCrates(stacks) def parseInput(data: list[str]) -> tuple[yardType, list[Instruction]]: firstBlankLine = data.index("") cratePositions = data[:firstBlankLine] cratePositions.reverse() #ordered bottom to top instructionLines = data[firstBlankLine+1:] # Create crate data struction crateNameLine = cratePositions #get crate labels from line crates = {name: [] for name in crateNameLine.split()} #create dicts per line for line in cratePositions[1:]: for index, crateName in enumerate(line[1::4]): if crateName != " ": crates[str(crateNameLine[index*4 + 1])].append(crateName) #Add crate to list # Parse instructions instructions = [] for ins in instructionLines: match = re.match(r'move (?P\d+) from (?P\d+) to (?P\d+)', ins) num, from_stack, to_stack = match.group('num'), match.group('from_stack'), match.group('to_stack') instructions.append(Instruction( quantity = int(match.group('num')), from_stack = match.group('from_stack'), to_stack = match.group('to_stack') )) return (crates, instructions) def operateOn(crates: yardType, ins: Instruction) -> yardType: #printCrates(crates) for _ in range(ins.quantity): crates[ins.to_stack].append(crates[ins.from_stack].pop()) return crates def topCrates(crates: yardType): return ''.join([stack.pop() for stack in crates.values()]) if 'pyodide' in sys.modules: def main_day5_1(): data = get_input('day5_1') display(f"{solutionFromInput(data)= }", target="day5_1-output", append=False) else: with open("inputtest.txt", "r") as fp: data = fp.read() print(solutionFromInput(data))``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 `````` ``````def viz_day5_1(): from typing import NamedTuple from dataclasses import dataclass from functools import partial import re import js from js import anime, Object from pyodide.ffi import to_js, create_proxy from pyodide.ffi.wrappers import add_event_listener from typing import NewType # The 'yard' is the collection of all the stacks of crates yardType = NewType("yardType", dict[str:list]) class Instruction(NamedTuple): quantity: int from_stack: str to_stack: str def j(obj): return to_js(obj, dict_converter=Object.fromEntries) data = get_input('day5_1').split('\n') def parseInput(data: list[str]) -> tuple[yardType, list[Instruction]]: firstBlankLine = data.index("") cratePositions = data[:firstBlankLine] cratePositions.reverse() #ordered bottorstBlankLine = data.index("") cratePositions = data[:firstBlankLine] cratePositions.reverse() #ordered bottom to top instructionLines = data[firstBlankLine+1:] # Create crate data struction crateNameLine = cratePositions #get crate labels from line crates = {name: [] for name in crateNameLine.split()} #create dicts per line for line in cratePositions[1:]: for index, crateName in enumerate(line[1::4]): if crateName != " ": crates[str(crateNameLine[index*4 + 1])].append(crateName) #Add crate to list # Parse instructions instructions = [] for ins in instructionLines: match = re.match(r'move (?P\d+) from (?P\d+) to (?P\d+)', ins) num, from_stack, to_stack = match.group('num'), match.group('from_stack'), match.group('to_stack') instructions.append(Instruction( quantity = int(match.group('num')), from_stack = match.group('from_stack'), to_stack = match.group('to_stack') )) return (crates, instructions) yard, instructions = parseInput(data) @dataclass class Crate(): label:str element:object def __str__(self): return self.label @dataclass class Stack(): crates: list x: int overall = js.document.getElementById("day5_1-viz") overall.style.margin = "50px 50px 50px 50px" overall.style.position = "relative" div = js.document.createElement('div') overall.appendChild(div) divHeight = 800 divWidth = 600 startColor = "Aquamarine" endColor = "green" div.style.height = f"{divHeight}px" div.style.width = f"{divWidth}px" div.style.backgroundColor = "#eee" floorY = divHeight - 100 floor = js.document.createElement('div') floor.id = "floor" floor.style.backgroundColor = "#777" floor.style.position = "absolute" floor.style.height = "10px" floor.style.width = f"{divWidth}px" floor.style.bottom = f"{divHeight-floorY-10}px" floor.style.left = "0px" overall.appendChild(floor) textOutput = js.document.createElement("div") textOutput.id = "day5_1_text_output" overall.parentNode.insertBefore(textOutput, overall.nextSibling) # #js.startAnimation = myTimeline.play playButton = js.document.createElement("button") playButton.innerText = "Play" playButton.style.padding = ".5rem 1rem .5rem 1rem" playButton.style.border = "2px solid #2c2e34" playButton.style.backgroundColor = "#bbf7d0" # #js.stopAnimation = myTimeline.pause pauseButton = js.document.createElement("button") pauseButton.innerText = "Pause" pauseButton.style.padding = ".5rem 1rem .5rem 1rem" pauseButton.style.border = "2px solid #2c2e34" pauseButton.style.backgroundColor = "#fef08a" # seekbar = js.document.createElement("input") seekbar.type = "range" seekbar.id = "seekbar" seekbar.min = "0" seekbar.max = "100" seekbar.value = "0" seekbar.style.width = "400px" controlHolder = js.document.createElement("div") controlHolder.id = "day5_2_controls" controlHolder.appendChild(playButton) controlHolder.appendChild(pauseButton) controlHolder.appendChild(seekbar) controlHolder.style.position = "absolute" controlHolder.style.bottom = "10px" controlHolder.style.left = "20px" controlHolder.style.width = "100%" div.appendChild(controlHolder) crateSize = 25 def indexToBottom(index): return divHeight - floorY + (crateSize+5) * index def makeDisplay(yard): for index, stack in enumerate(yard): newStackList = [] leftEdge = index * (crateSize * 2) + 25 stackLabel = js.document.createElement('div') stackLabel.style.width = f"{crateSize}px" stackLabel.style.height = f"{crateSize}px" stackLabel.style.position = "absolute" stackLabel.style.left = f"{leftEdge}px" stackLabel.style.top = f"{floorY + 15}px" stackLabel.style.textAlign = "center" label = js.document.createElement("span") label.innerText = stack stackLabel.appendChild(label) div.appendChild(stackLabel) for stackLevel, crate in enumerate(yard[stack]): bottom = indexToBottom(stackLevel) container = js.document.createElement('div') container.style.backgroundColor = startColor container.style.width = f"{crateSize}px" container.style.border = "2px solid #2c2e34" container.style.position = "absolute" container.style.bottom = f"{bottom}px" container.style.left = f"{leftEdge}px" container.style.textAlign = "center" label = js.document.createElement("span") label.innerText = str(crate) container.appendChild(label) newStackList.append(Crate(crate, container)) div.appendChild(container) yard[stack] = Stack(crates= newStackList, x = leftEdge) makeDisplay(yard) myTimeline = anime.timeline(j({ "duration": 500, "easing": "easeInOutSine", "autoplay": True })) def seekbar_input(evt): myTimeline.pause() myTimeline.seek(myTimeline.duration * (float(evt.srcElement.value)/100)) add_event_listener(seekbar, 'input', seekbar_input) add_event_listener(playButton, 'click', lambda _: myTimeline.play()) add_event_listener(pauseButton, 'click', lambda _: myTimeline.pause()) progressElement = js.document.getElementById("seekbar") prevProgress = 0 def updateSeekbar(yard, *args): nonlocal prevProgress progressElement.value = myTimeline.progress if myTimeline.progress == 100: for stack in yard: if len(yard[stack].crates): topCrate = yard[stack].crates[-1] topCrate.element.style.backgroundColor = endColor elif prevProgress == 100: for stack in yard: for crate in yard[stack].crates: crate.element.style.backgroundColor = startColor prevProgress = myTimeline.progress def displayOnMove(from_stack, to_stack, quantity, instructionCount, instructionIndex, *args): display(f"Instruction {instructionIndex}: Move {quantity} crate{'s' if quantity > 1 else ''} from stack {from_stack} to stack {to_stack}. {instructionCount+1} of {quantity} complete", target="day5_1_text_output", append=False) def doFinalOutput(yard, *args): solution = "" for stack in yard: if len(yard[stack].crates): topCrate = yard[stack].crates[-1] topCrate.element.style.backgroundColor = "green" solution += topCrate.label display(f"SOLUTION: {solution}", target="day5_1_text_output", append=False) def moveOneCrate(yard: yardType, from_stack: str, to_stack: str, quantity: int, timeline, instructionCount, instructionIndex, final): js.console.log("moveOneCrate") crate = yard[from_stack].crates.pop() yard[to_stack].crates.append(crate) newBottom = indexToBottom(yard[to_stack].crates.index(crate)) timeline.add(j({ "targets": crate.element, "keyframes": [ j({"bottom": f"{divHeight - 20 - crateSize}px"}), j({"left": yard[to_stack].x}), j({"bottom": f"{newBottom}px"}) ], "begin": partial(displayOnMove, from_stack, to_stack, quantity, instructionCount, instructionIndex), "update": partial(updateSeekbar, yard), "complete": partial(doFinalOutput, yard) if final else (lambda _: None) })) def operateOn(yard: yardType, ins: Instruction, instructionIndex: int, final:bool) -> yard: for instructionCount in range(ins.quantity): moveOneCrate(yard, ins.from_stack, ins.to_stack, ins.quantity, myTimeline, instructionCount, instructionIndex, final = final and instructionCount == ins.quantity - 1) def doAllInstructions(instructions): for instructionIndex, ins in enumerate(instructions): operateOn(yard, ins, instructionIndex, final = True if instructionIndex == len(instructions) - 1 else False) doAllInstructions(instructions) js.console.log(js.window)``````
Scroll to see complete code

## Day 5: Supply Stacks (Part 2)

I was wondering when I would get bit by one of PyScript's core limitations (currently) - all `<py-script>` tags are executed in the same global namespace. Meaning if you have two functions with the same name in two separate files/script tags, any objects whos names overlap previous tags overwrite those objects. Hence names like `operateOn5_2()` to ensure the functions are unique to this day/part.

• day5_2.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 `````` ``````from typing import NamedTuple import re import sys from typing import NewType # The 'yard' is the collection of all the stacks of crates yardType = NewType("yardType", dict[str:list]) class Instruction(NamedTuple): quantity: int from_stack: str to_stack: str def solutionWithClumpedStacks(data: str) -> str: data = data.split("\n") stacks, instructions = parseInput5_2(data) for ins in instructions: stacks = operateOn5_2(stacks, ins) return topCrates5_2(stacks) def parseInput5_2(data: list[str]) -> tuple[yardType, list[Instruction]]: firstBlankLine = data.index("") cratePositions = data[:firstBlankLine] cratePositions.reverse() #ordered bottom to top instructionLines = data[firstBlankLine+1:] # Create crate data struction crateNameLine = cratePositions #get crate labels from line crates = {name: [] for name in crateNameLine.split()} #create dicts per line for line in cratePositions[1:]: for index, crateName in enumerate(line[1::4]): if crateName != " ": crates[str(crateNameLine[index*4 + 1])].append(crateName) #Add crate to list # Parse instructions instructions = [] for ins in instructionLines: match = re.match(r'move (?P\d+) from (?P\d+) to (?P\d+)', ins) num, from_stack, to_stack = match.group('num'), match.group('from_stack'), match.group('to_stack') instructions.append(Instruction( quantity = int(match.group('num')), from_stack = match.group('from_stack'), to_stack = match.group('to_stack') )) return (crates, instructions) def operateOn5_2(crates: yardType, ins: Instruction) -> yardType: to_move = crates[ins.from_stack][-ins.quantity:] crates[ins.from_stack] = crates[ins.from_stack][:-ins.quantity] crates[ins.to_stack].extend(to_move) return crates def topCrates5_2(crates: yardType): return ''.join([stack.pop() for stack in crates.values()]) if 'pyodide' in sys.modules: def main_day5_2(): data = get_input('day5_2') display(f"{solutionWithClumpedStacks(data)= }", target="day5_2-output", append=False) else: with open("input.txt", "r") as fp: data = fp.read() print(solutionWithClumpedStacks(data))``````
Scroll to see complete code

## Day 6: Tuning Trouble (Part 1)

Part of the fun of Advent of Code is trying to guess what things in Part 1 of each day are going to get turned topsy-turvy in Part 2. Today's question, involving finding when elements in a sliding window are unique, lead me to a few guesses. Would there be some other criteria for determining success? Only one duplicated letter perhaps? Perhaps the window would need to ignore only its center element, or the window would jump by twos, or something.

• day6_1.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 `````` ``````from collections import deque import sys def findIndexAllDifferent(input_stream, n): window = deque(maxlen=n) for index, token in enumerate(input_stream): window.append(token) if index + 1 >= n and len(set(window)) >= n: return index + 1 return -1 if 'pyodide' in sys.modules: def main_day6_1(): data = get_input('day6_1') display(f"{findIndexAllDifferent(data, 4)= }", target="day6_1-output", append=False) else: with open("input.txt", "r") as fp: data = fp.read().strip('\n') print(findIndexAllDifferent(data, 4)) ``````
Scroll to see complete code

## Day 6: Tuning Trouble (Part 2)

Thankfully, it turned out part 2 made the simplest possible adjustment - the length of the window. Hence, the code for the two parts looks almost identical. I suppose the objective was to catch out anyone who "manually" checked each element of the sliding window for uniqueness against the other three.

• day6_2.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 `````` ``````from collections import deque import sys def findIndexAllDifferent_6_2(input_stream, n): window = deque(maxlen=n) for index, token in enumerate(input_stream): window.append(token) if index + 1 >= n and len(set(window)) >= n: return index + 1 return -1 if 'pyodide' in sys.modules: def main_day6_2(): data = get_input('day6_2') display(f"{findIndexAllDifferent_6_2(data, 14)= }", target="day6_2-output", append=False) else: with open("input.txt", "r") as fp: data = fp.read().strip('\n') print(findIndexAllDifferent_6_2(data, 14))``````
Scroll to see complete code

## Day 7: No Space Left on Device (Part 1)

When running into a challenge like today's, the question is always: "Should I implement my own data structure, or make use of a built-in/pre-existing module?" Today I opted for the later, and discovered the anytree package for the first time. It has all the functionality I could way - children/parent tracking, arbitrary attributes on Nodes, provision for walking/tranversing/searching the tree, importing/exporting dictionaries/JSON, symlinks... I suspect I'm going to get a lot of use out of this.

• day7_1.py
• day7_1-viz.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 `````` ``````import sys from anytree import Node, RenderTree, PreOrderIter from anytree.search import findall def solution_7_1(data): if (root_cmd := data) == "\$ cd /": root = Node('/') data = data[1:] else: raise ValueError(f"First command something other than cd / : {root_cmd}") currentNode = root for line in data: currentNode = processLine_7_1(line, root, currentNode) # After constructing tree, pre-calculate folder sizes. # A bit inefficient, but fine for node in PreOrderIter(root): if isFolder_7_1(node) and not hasattr(node, "folder_size"): node.folder_size = containedFileSize_7_1(node) small_folders = findall(root, lambda n: hasattr(n, "folder_size") and n.folder_size <= 100_000) return sum(f.folder_size for f in small_folders) def processLine_7_1(line: str, rootNode: Node, currentNode: Node) -> Node: """ Processes one line of input; mutates the tree pointed to by rootNode, returns the new currentNode """ nextCurrentNode = currentNode match line.split(): case ["\$", "cd", "/"]: # Move out to root nextCurrentNode = rootNode case ["\$", "cd", ".."]: # Move out one level nextCurrentNode = currentNode.parent case ["\$", "cd", dir]: # Move to directory {dir} nextCurrentNode = [child for child in currentNode.children if child.name == dir] case ["\$", "ls"]: #list files pass case ["dir", dirname]: # New directory {dirname} newDir = Node(name=dirname, parent = currentNode) case [size, filename]: # New file {filename} newFile = Node(name=filename, parent=currentNode, size=int(size)) case _: raise ValueError(f"Somehow unmatched??") return nextCurrentNode def printTree_7_1(root:Node) -> None: for pre, fill, node in RenderTree(root): print(f"{pre}{node.name}{' - ' + str(node.size) if hasattr(node, 'size') else ''}") def isFile_7_1(node:Node) -> bool: return len(node.children) == 0 def isFolder_7_1(node:Node) -> bool: return len(node.children) > 0 def containedFileSize_7_1(node:Node) -> bool: if isFile_7_1(node): return node.size return sum(containedFileSize_7_1(n) for n in node.children) if 'pyodide' in sys.modules: def main_day7_1(): data = get_input('day7_1').split('\n') display(f"{solution_7_1(data)=}", target="day7_1-output", append=False) else: with open("input.txt", "r") as fp: data = fp.read().split('\n') print(solution_7_1(data))``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 `````` ``````from contextlib import redirect_stdout from io import StringIO import sys from anytree import Node, RenderTree, PreOrderIter from anytree.search import findall from anytree.render import ContStyle def viz_day7_1(): def local_main(data): import js if (root_cmd := data) == "\$ cd /": root = Node('/') data = data[1:] else: raise ValueError(f"First command something other than cd / : {root_cmd}") currentNode = root for line in data: currentNode = processLine_7_1(line, root, currentNode) # After constructing tree, pre-calculate folder sizes. # A bit inefficient, but fine for node in PreOrderIter(root): if isFolder_7_1(node) and not hasattr(node, "folder_size"): node.folder_size = containedFileSize_7_1(node) small_folders = findall(root, lambda n: hasattr(n, "folder_size") and n.folder_size <= 100_000) display(sum(f.folder_size for f in small_folders), target = "day7_1-output", append=False) if (pre:= js.document.getElementById("day7_1-pre")) is None: # Make pre tag for output pre = js.document.createElement("pre") pre.id = "day7_1-pre" pre.classList.add("bg-gray-900", "text-gray-300") pre.style.lineHeight = '1.1' container = js.document.getElementById("day7_1-viz") container.classList.add('max-h-124', 'overflow-y-auto', 'my-4') container.appendChild(pre) else: pre.innerHTML = "" #clear existing output for pre, fill, node in RenderTree(root, style=ContStyle()): #if node in small_folders: nameSegment = f"{pre}{node.name}" if hasattr(node, 'size'): displaySize = f"{'size:': >{len('folder size:')}} {str(node.size)}" elif hasattr(node, 'folder_size'): displaySize = f"{'folder size:'} {str(node.folder_size)}" else: displaySize = '' contents = f"{nameSegment: <60}{displaySize}" #Highlight solution lines if hasattr(node, "folder_size") and node.folder_size <= 100_000: contents = f'{contents}' else: contents = f'{contents}' display(HTML(contents), target = "day7_1-pre") def processLine_7_1(line: str, rootNode: Node, currentNode: Node) -> Node: """ Processes one line of input; mutates the tree pointed to by rootNode, returns the new currentNode """ nextCurrentNode = currentNode match line.split(): case ["\$", "cd", "/"]: # Move out to root nextCurrentNode = rootNode case ["\$", "cd", ".."]: # Move out one level nextCurrentNode = currentNode.parent case ["\$", "cd", dir]: # Move to directory {dir} nextCurrentNode = [child for child in currentNode.children if child.name == dir] case ["\$", "ls"]: #list files pass case ["dir", dirname]: # New directory {dirname} newDir = Node(name=dirname, parent = currentNode) case [size, filename]: # New file {filename} newFile = Node(name=filename, parent=currentNode, size=int(size)) case _: raise ValueError(f"Somehow unmatched??") return nextCurrentNode def printTree_7_1(root:Node) -> None: for pre, fill, node in RenderTree(root): print(f"{pre}{node.name}{' - ' + str(node.size) if hasattr(node, 'size') else ''}") def isFile_7_1(node:Node) -> bool: return len(node.children) == 0 def isFolder_7_1(node:Node) -> bool: return len(node.children) > 0 def containedFileSize_7_1(node:Node) -> bool: if isFile_7_1(node): return node.size return sum(containedFileSize_7_1(n) for n in node.children) data = get_input('day7_1').split('\n') local_main(data)``````
Scroll to see complete code

## Day 7: No Space Left on Device (Part 2)

Another part-2 problem which requires taking the data discovered in part 1, sorting it (by some key), and finding the smallest (largest) value, possibly beyond some threshhold. Using `anytree.search.findall` makes it easy to find the folders, and `sorted(key = lamabda node: node.folder_size)` allows us to sort by the relevant key.

• day7_2.py
• day7_2-viz.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 `````` ``````import sys from anytree import Node, RenderTree, PreOrderIter from anytree.search import findall def solution_7_2(data): if (root_cmd := data) == "\$ cd /": root = Node('/') data = data[1:] else: raise ValueError(f"First command something other than cd / : {root_cmd}") currentNode = root for line in data: currentNode = processLine_7_2(line, root, currentNode) for node in PreOrderIter(root): if isFolder_7_2(node) and not hasattr(node, "folder_size"): node.folder_size = containedFileSize_7_2(node) size_used = root.folder_size total_size = 70_000_000 size_available = total_size - size_used size_needed_for_update = 30_000_000 minimum_delete = size_needed_for_update - size_available # Sort folders by size, find the smallest one larger than the needed size for folder in sorted(findall(root, lambda n: hasattr(n, "folder_size")), key= lambda n: n.folder_size): if folder.folder_size > minimum_delete: return folder.folder_size def processLine_7_2(line: str, rootNode: Node, currentNode: Node) -> Node: """ Processes one line of input; mutates the tree pointed to by rootNode, returns the new currentNode """ nextCurrentNode = currentNode match line.split(): case ["\$", "cd", "/"]: # Move out to root nextCurrentNode = rootNode case ["\$", "cd", ".."]: # Move out one level nextCurrentNode = currentNode.parent case ["\$", "cd", dir]: # Move to directory {dir} nextCurrentNode = [child for child in currentNode.children if child.name == dir] case ["\$", "ls"]: # List Files pass case ["dir", dirname]: # New directory {dirname} newDir = Node(name=dirname, parent = currentNode) case [size, filename]: # New file {filename} newFile = Node(name=filename, parent=currentNode, size=int(size)) case _: raise ValueError(f"Somehow unmatched??") # Somehow unmatched?? return nextCurrentNode def printTree_7_2(root:Node) -> None: for pre, fill, node in RenderTree(root): print(f"{pre}{node.name}{' - ' + str(node.size) if hasattr(node, 'size') else ''}") def isFile_7_2(node:Node) -> bool: return len(node.children) == 0 def isFolder_7_2(node:Node) -> bool: return len(node.children) > 0 def containedFileSize_7_2(node:Node) -> bool: if isFile_7_2(node): return node.size return sum(containedFileSize_7_2(n) for n in node.children) if 'pyodide' in sys.modules: def main_day7_2(): data = get_input('day7_2').split('\n') display(f"{solution_7_2(data)=}", target="day7_2-output", append=False) else: with open("input.txt", "r") as fp: data = fp.read().split('\n') print(solution_7_2(data))``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 `````` ``````from contextlib import redirect_stdout from io import StringIO import sys from anytree import Node, RenderTree, PreOrderIter from anytree.search import findall from anytree.render import ContStyle def viz_day7_2(): def local_main(data): import js if (root_cmd := data) == "\$ cd /": root = Node('/') data = data[1:] else: raise ValueError(f"First command something other than cd / : {root_cmd}") currentNode = root for line in data: currentNode = processLine_7_2(line, root, currentNode) # After constructing tree, pre-calculate folder sizes. # A bit inefficient, but fine for node in PreOrderIter(root): if isFolder_7_2(node) and not hasattr(node, "folder_size"): node.folder_size = containedFileSize_7_2(node) size_used = root.folder_size total_size = 70_000_000 size_available = total_size - size_used size_needed_for_update = 30_000_000 minimum_delete = size_needed_for_update - size_available # Sort folders by size, find the smallest one larger than the needed size for folder in sorted(findall(root, lambda n: hasattr(n, "folder_size")), key= lambda n: n.folder_size): if folder.folder_size > minimum_delete: selectedFolder = folder display(folder.folder_size, target="day7_2-output", append=False) break if (pre:= js.document.getElementById("day7_2-pre")) is None: # Make pre tag for output pre = js.document.createElement("pre") pre.id = "day7_2-pre" pre.classList.add("bg-gray-900", "text-gray-300") pre.style.lineHeight = '1.1' container = js.document.getElementById("day7_2-viz") container.classList.add('max-h-124', 'overflow-y-auto', 'my-4') container.appendChild(pre) else: pre.innerHTML = "" #clear existing output runningTotal = 0 for pre, fill, node in RenderTree(root, style=ContStyle()): #if node in small_folders: nameSegment = f"{pre}{node.name}" if hasattr(node, 'size'): displaySize = f"{'size:': >{len('folder size:')}} {str(node.size)}" elif hasattr(node, 'folder_size'): displaySize = f"{'folder size:'} {str(node.folder_size)}" else: displaySize = '' contents = f"{nameSegment: <60}{displaySize}" #Highlight solution lines if node == selectedFolder or (node in selectedFolder.descendants and isFile_7_2(node)): if node == selectedFolder: contents = f"{contents: <85} <<<<< SOLUTION" if isFile_7_2(node): runningTotal += node.size contents = f"{contents: <85} Running Total: {runningTotal}" contents = f'{contents}' else: contents = f'{contents}' display(HTML(contents), target = "day7_2-pre") def processLine_7_2(line: str, rootNode: Node, currentNode: Node) -> Node: """ Processes one line of input; mutates the tree pointed to by rootNode, returns the new currentNode """ nextCurrentNode = currentNode match line.split(): case ["\$", "cd", "/"]: # Move out to root nextCurrentNode = rootNode case ["\$", "cd", ".."]: # Move out one level nextCurrentNode = currentNode.parent case ["\$", "cd", dir]: # Move to directory {dir} nextCurrentNode = [child for child in currentNode.children if child.name == dir] case ["\$", "ls"]: #list files pass case ["dir", dirname]: # New directory {dirname} newDir = Node(name=dirname, parent = currentNode) case [size, filename]: # New file {filename} newFile = Node(name=filename, parent=currentNode, size=int(size)) case _: raise ValueError(f"Somehow unmatched??") return nextCurrentNode def printTree_7_2(root:Node) -> None: for pre, fill, node in RenderTree(root): print(f"{pre}{node.name}{' - ' + str(node.size) if hasattr(node, 'size') else ''}") def isFile_7_2(node:Node) -> bool: return len(node.children) == 0 def isFolder_7_2(node:Node) -> bool: return len(node.children) > 0 def containedFileSize_7_2(node:Node) -> bool: if isFile_7_2(node): return node.size return sum(containedFileSize_7_2(n) for n in node.children) data = get_input('day7_2').split('\n') local_main(data)``````
Scroll to see complete code

## Day 8: Treetop Tree House (Part 1)

More parsing, more fun! I suspect there's some data structure that makes it simpler to iterate over both the rows and columns of a grid... or perhaps I should create my own, as that's the kind of thing that seems to come up often in Advent of Code.

• day8_1.py
• day8_1-viz.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 `````` ``````import sys def solution_8_1(data: list[str]): # (rowIndex, columnIndex) visible_trees: set[tuple[int, int]] = set() #Rows left to right for row_index, row in enumerate(data): visible_trees |= {(row_index, line_index) for line_index in find_visible_in_line(row)} pass #Rows right to left for row_index, row in enumerate([row[::-1] for row in data]): visible_trees |= {(row_index, len(row) - line_index - 1) for line_index in find_visible_in_line(row)} pass #Columns Top to Bottom: for column_index in range(len(data)): column = [row[column_index] for row in data] visible_trees |= {(row_index, column_index) for row_index in find_visible_in_line(column)} #Columns Bottom to Top: for column_index in range(len(data)): column = [row[column_index] for row in data][::-1] visible_trees |= {(len(column) - row_index - 1, column_index) for row_index in find_visible_in_line(column)} #printVisibleTrees(data, visible_trees) return(f"{len(visible_trees)= }") def printVisibleTrees(data: list[str], visbile_trees: set[tuple[int, int]]): for row_index, row in enumerate(data): for column_index, char in enumerate(row): if (row_index, column_index) in visbile_trees: print(f"[green on dark_red]{char}[/]", end = "") else: print(f"[bright_black]{char}[/]", end = "") print("") def find_visible_in_line(line: str) -> set[int]: max_height_seen = -1 visible_in_line = set() for line_index, tree in enumerate(line): if int(tree) > max_height_seen: visible_in_line.add(line_index) max_height_seen = int(tree) if max_height_seen == 9: break return visible_in_line if 'pyodide' in sys.modules: def main_day8_1(): data = get_input('day8_1').split('\n') display(f"{solution_8_1(data)=}", target="day8_1-output", append=False) else: from rich import print with open("input.txt", "r") as fp: data = fp.read().split('\n') print(f"{solution_8_1(data)=}")``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 `````` ``````from itertools import repeat from typing import Iterable from sys import stdout from pyscript import display, HTML from rich import get_console from rich.segment import Segment from rich.console import NewLine from rich.text import Text import rich.jupyter # Patch the rich library to enable output c = get_console() c.is_jupyter = lambda: True def get_rich_printer(target): def display_pyscript(segments: Iterable[Segment], text: str) -> None: """Allow output of raw HTML within pyscript/pyodide""" html = rich.jupyter._render_segments(segments) display(HTML(html), target=target, append=True) rich.jupyter.display = display_pyscript return get_console().print # Solution code: import js def viz_day8_1(): prepare_day8_1_element() js.document.getElementById("day8_1-viz").innerHTML = "" data = get_input('day8_1').split('\n') display(f"{solve_viz8_1(data)=}", target="day8_1-output", append=False) post_day8_1_element() def prepare_day8_1_element(): viz_div = js.document.getElementById("day8_1-viz") viz_div.classList.add("overflow-x-auto", "overflow-y-auto", "w-full", "whitespace-nowrap") def post_day8_1_element(): from pyodide.ffi import to_js viz_div = js.document.getElementById("day8_1-viz") for pretag in viz_div.getElementsByTagName('pre'): pretag.style.whiteSpace = "nowrap" pretag.style.display = "inline-block" def solve_viz8_1(data: list[str]): # (rowIndex, columnIndex) visible_trees: set[tuple[int, int]] = set() #Rows left to right for row_index, row in enumerate(data): visible_trees |= {(row_index, line_index) for line_index in find_visible_in_line_viz(row)} pass #Rows right to left for row_index, row in enumerate([row[::-1] for row in data]): visible_trees |= {(row_index, len(row) - line_index - 1) for line_index in find_visible_in_line_viz(row)} pass #Columns Top to Bottom: for column_index in range(len(data)): column = [row[column_index] for row in data] visible_trees |= {(row_index, column_index) for row_index in find_visible_in_line_viz(column)} #Columns Bottom to Top: for column_index in range(len(data)): column = [row[column_index] for row in data][::-1] visible_trees |= {(len(column) - row_index - 1, column_index) for row_index in find_visible_in_line_viz(column)} printVisibleTrees_viz(data, visible_trees) return(len(visible_trees)) def printVisibleTrees_viz(data: list[str], visbile_trees: set[tuple[int, int]]): row_strings = [] for row_index, row in enumerate(data): line_elements = [] for column_index, char in enumerate(row): if (row_index, column_index) in visbile_trees: line_elements.append(f"[bright_white on dark_green]{char}[/]") else: line_elements.append(f"[aquamarine3]{char}[/]") row_strings.append(''.join(line_elements)) for r in row_strings: get_rich_printer("day8_1-viz")(r) #rprint_8_1_viz(r) def find_visible_in_line_viz(line: str) -> set[int]: max_height_seen = -1 visible_in_line = set() for line_index, tree in enumerate(line): if int(tree) > max_height_seen: visible_in_line.add(line_index) max_height_seen = int(tree) if max_height_seen == 9: break return visible_in_line ``````
Scroll to see complete code

## Day 8: Treetop Tree House (Part 2)

Figuring out all the indices, ranges, and exactly what was being asked was a bit hairy in this second part, but the ultimate stumbling block for me ended up being multiplying the running score by itself, rather than by the new trees seen in a given direction. Oops!

• day8_2.py
• day8_2-viz.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 `````` ``````import sys def solution_8_2(data: list[str]): max_trees_visible = 0 high_score_location = None visible_from_high_score = set() for house_row_index, treehouse_row in enumerate(data): for house_col_index, treehouse_height in enumerate(treehouse_row): visible_trees = set() score = 1 #Row left to right row_to_right = data[house_row_index][house_col_index+1:] visbile_from_func = find_visible_in_line_8_2(row_to_right, max_height=int(treehouse_height)) visible_trees |= {(house_row_index, line_index + house_col_index + 1) for line_index in visbile_from_func} score *= len(visbile_from_func) #Rows right to left row_to_left = list(reversed(data[house_row_index][:house_col_index])) visbile_from_func = find_visible_in_line_8_2(row_to_left, max_height=int(treehouse_height)) visible_trees |= {(house_row_index, house_col_index - 1 - line_index) for line_index in visbile_from_func} score *= len(visbile_from_func) #Columns Top to Bottom: column_down = [row[house_col_index] for row in data[house_row_index+1:]] visbile_from_func = find_visible_in_line_8_2(column_down, max_height=int(treehouse_height)) visible_trees |= {(house_row_index + line_index + 1, house_col_index) for line_index in visbile_from_func} score *= len(visbile_from_func) #Columns bottom to top: column_up = list(reversed([row[house_col_index] for row in data[:house_row_index]])) visbile_from_func = find_visible_in_line_8_2(column_up, max_height=int(treehouse_height)) visible_trees |= {(house_row_index - line_index - 1, house_col_index) for line_index in visbile_from_func} score *= len(visbile_from_func) if score > max_trees_visible: max_trees_visible = score high_score_location = (house_row_index, house_col_index) visible_from_high_score = visible_trees #printVisibleTrees(data, visible_from_high_score, special=high_score_location) return max_trees_visible def printVisibleTrees(data: list[str], visible_trees: set[tuple[int, int]], special = None): for row_index, row in enumerate(data): for column_index, char in enumerate(row): if (row_index, column_index) == special: print(f"[black dark_slate_gray_1]{char}[/]", end = "") elif (row_index, column_index) in visible_trees: print(f"[green on dark_red]{char}[/]", end = "") else: print(f"[bright_black]{char}[/]", end = "") print("") def find_visible_in_line_8_2(line: str, max_height = 9) -> int: #print(f"Visible in line {list(line)} from height {max_height}; ", end = "") visible_trees = set() if not line: #print("Nothing here, score 0") return visible_trees for line_index, tree in enumerate(line): if int(tree) >= max_height: #print(f"Score: {line_index+1}") return range(0, line_index+1) else: #print(f"Max length, score {len(line)}") return range(0, len(line)) if 'pyodide' in sys.modules: def main_day8_2(): data = get_input('day8_2').split('\n') display(f"{solution_8_2(data)=}", target="day8_2-output", append=False) else: from rich import print with open("input.txt", "r") as fp: data = fp.read().split('\n') print(f"{solution_8_2(data)=}")``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 `````` ``````from itertools import repeat from typing import Iterable from sys import stdout from pyscript import display, HTML from rich import get_console from rich.segment import Segment from rich.console import NewLine from rich.text import Text import rich.jupyter # Patch the rich library to enable output c = get_console() c.is_jupyter = lambda: True def get_rich_printer(target): def display_pyscript(segments: Iterable[Segment], text: str) -> None: """Allow output of raw HTML within pyscript/pyodide""" html = rich.jupyter._render_segments(segments) display(HTML(html), target=target, append=True) rich.jupyter.display = display_pyscript return get_console().print #Solution code def viz_day8_2(): prepare_day8_2_element() js.document.getElementById("day8_2-viz").innerHTML = "" data = get_input('day8_2').split('\n') display(f"{solution_viz8_2(data)=}", target="day8_2-output", append=False) post_day8_2_element() def prepare_day8_2_element(): viz_div = js.document.getElementById("day8_2-viz") viz_div.classList.add("overflow-x-auto", "overflow-y-auto", "w-full", "whitespace-nowrap") def post_day8_2_element(): from pyodide.ffi import to_js viz_div = js.document.getElementById("day8_2-viz") for pretag in viz_div.getElementsByTagName('pre'): pretag.style.whiteSpace = "nowrap" pretag.style.display = "inline-block" def solution_viz8_2(data: list[str]): max_trees_visible = 0 high_score_location = None visible_from_high_score = set() for house_row_index, treehouse_row in enumerate(data): for house_col_index, treehouse_height in enumerate(treehouse_row): visible_trees = set() score = 1 #Row left to right row_to_right = data[house_row_index][house_col_index+1:] visbile_from_func = find_visible_in_line_viz8_2(row_to_right, max_height=int(treehouse_height)) visible_trees |= {(house_row_index, line_index + house_col_index + 1) for line_index in visbile_from_func} score *= len(visbile_from_func) #Rows right to left row_to_left = list(reversed(data[house_row_index][:house_col_index])) visbile_from_func = find_visible_in_line_viz8_2(row_to_left, max_height=int(treehouse_height)) visible_trees |= {(house_row_index, house_col_index - 1 - line_index) for line_index in visbile_from_func} score *= len(visbile_from_func) #Columns Top to Bottom: column_down = [row[house_col_index] for row in data[house_row_index+1:]] visbile_from_func = find_visible_in_line_viz8_2(column_down, max_height=int(treehouse_height)) visible_trees |= {(house_row_index + line_index + 1, house_col_index) for line_index in visbile_from_func} score *= len(visbile_from_func) #Columns bottom to top: column_up = list(reversed([row[house_col_index] for row in data[:house_row_index]])) visbile_from_func = find_visible_in_line_viz8_2(column_up, max_height=int(treehouse_height)) visible_trees |= {(house_row_index - line_index - 1, house_col_index) for line_index in visbile_from_func} score *= len(visbile_from_func) if score > max_trees_visible: max_trees_visible = score high_score_location = (house_row_index, house_col_index) visible_from_high_score = visible_trees printVisibleTrees_viz_8_2(data, visible_from_high_score, special=high_score_location) return max_trees_visible def printVisibleTrees_viz_8_2(data: list[str], visbile_trees: set[tuple[int, int]], special = None): row_strings = [] for row_index, row in enumerate(data): line_elements = [] for column_index, char in enumerate(row): if (row_index, column_index) == special: line_elements.append(f"[bright_white on dark_violet]{char}[/]") elif (row_index, column_index) in visbile_trees: line_elements.append(f"[bright_white on dark_green]{char}[/]") else: line_elements.append(f"[aquamarine3]{char}[/]") row_strings.append(''.join(line_elements)) for r in row_strings: get_rich_printer("day8_2-viz")(r) def find_visible_in_line_viz8_2(line: str, max_height = 9) -> int: #print(f"Visible in line {list(line)} from height {max_height}; ", end = "") visible_trees = set() if not line: #print("Nothing here, score 0") return visible_trees for line_index, tree in enumerate(line): if int(tree) >= max_height: #print(f"Score: {line_index+1}") return range(0, line_index+1) else: #print(f"Max length, score {len(line)}") return range(0, len(line))``````
Scroll to see complete code

## Day 9: Rope Bridge (Part 1)

I felt quite clever during this first part of today's problem, when I noticed that, when the tail of the rope has to move, it always moves to where the head was in the previous step. This saves a fair amount of figuring out the logic of exactly where the tail moves to in each step - it can just reuse the previous position of the head, if necessary.

• day9_1.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 `````` ``````from typing import NamedTuple import sys class Vector(NamedTuple): x: int y: int direction_to_vector = { "R": Vector(1, 0), "L": Vector(-1, 0), "U": Vector(0, 1), "D": Vector(0, -1), } def solve_9_1(data): head = Vector(0, 0) tail = Vector(0, 0) tail_visited = {tail} for line in data.split('\n'): direction, quantity = line.split(" ") diff = direction_to_vector[direction] for _ in range(int(quantity)): prev_head = head head = Vector(head.x + diff.x, head.y + diff.y) if too_far_9_1(head, tail): tail = prev_head tail_visited.add(tail) return len(tail_visited) def too_far_9_1(head, tail): return abs(head.x - tail.x) > 1 or abs(head.y - tail.y) > 1 if 'pyodide' in sys.modules: def main_day9_1(): data = get_input('day9_1') display(f"{solve_9_1(data)=}", target="day9_1-output", append=False) elif __name__ == '__main__': with open ("input.txt", "r") as fp: data = fp.read() print(f"{solve_9_1(data)}")``````
Scroll to see complete code

## Day 9: Rope Bridge (Part 2)

Of course, I was being too clever by half, and the logic in part 1 doesn't hold in part two; I ended up chasing a nasty typo in the logic of determining where each tail segment moves for quite awhile. I've left my rudimentary testing code and print statements in place and commented out for illustrative purposes.

• day9_2.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 `````` ``````from typing import NamedTuple import sys class Point(NamedTuple): x: int y: int previous: any class Vector(NamedTuple): x: int y: int direction_to_vector = { "R": Vector(1, 0), "L": Vector(-1, 0), "U": Vector(0, 1), "D": Vector(0, -1), } def solve_9_2(data): head = Point(0, 0, Vector(0, 0)) tails = [Point(0,0, Vector(0, 0)) for _ in range(9)] tail_visited = {(tails[-1].x, tails[-1].y)} for line in data.split('\n'): #print(f"\n{line=}") direction, quantity = line.split(" ") head_move = direction_to_vector[direction] for _ in range(int(quantity)): #print(f"Segments: {''.join([f'({str(s.x): >2},{str(s.y): >2})' for s in [head, *tails]])}") head = Point(x = head.x + head_move.x, y = head.y + head_move.y, previous=Vector(head.x, head.y)) #print(f"Head moves by ({head_move.x},{head_move.y}) to ({head.x}, {head.y})") #move first tail diff = catchup_step(head, tails) tails = Point(tails.x + diff.x, tails.y + diff.y, Vector(tails.x, tails.y)) #Move other tails for index, following_tail in enumerate(tails[1:]): local_index = index + 1 diff = catchup_step(tails[local_index-1], following_tail) #print(f'Moving tail at index {local_index} by {diff}') #print(f"({str(diff.x): >2},{str(diff.y): >2})", end = "") tails[local_index] = Point(following_tail.x + diff.x, following_tail.y + diff.y, Vector(following_tail.x, following_tail.y)) tail_visited.add((tails[-1].x, tails[-1].y)) #print(f"Segments: {''.join([f'({str(s.x): >2},{str(s.y): >2})' for s in [head, *tails]])}") #print("") #input("") return len(tail_visited) def catchup_step(head: Point, tail: Point) -> Vector: if abs(head.x - tail.x) > 1 or abs(head.y - tail.y) > 1: xdiff = 0 ydiff = 0 if head.x > tail.x: xdiff = 1 elif head.x < tail.x: xdiff = -1 if head.y > tail.y: ydiff = 1 elif head.y < tail.y: ydiff = -1 return Vector(xdiff, ydiff) return Vector(0, 0) def test_catchup_step(): #Identity zero_vector = Vector(0, 0) a = Vector(x = 0, y = 0) assert catchup_step(a, zero_vector) == Vector(0, 0) #1 off for x in range(-1, 1+1): for y in range(-1, 1+1): assert catchup_step(Vector(x, y), zero_vector) == Vector(0, 0) #2 off a = Vector(x = 2, y = 0) assert catchup_step(a, zero_vector) == Vector(1, 0) a = Vector(x = -2, y = 0) assert catchup_step(a, zero_vector) == Vector(-1, 0) a = Vector(x = 0, y = 2) assert catchup_step(a, zero_vector) == Vector(0, 1) a = Vector(x = 0, y = -2) assert catchup_step(a, zero_vector) == Vector(0, -1) #2, 1 off a = Vector(x = 2, y = 1) #print(c:= catchup_step(a, zero_vector)) assert catchup_step(a, zero_vector) == Vector(1, 1) a = Vector(x = 2, y = -1) assert catchup_step(a, zero_vector) == Vector(1, -1) a = Vector(x = -2, y = 1) assert catchup_step(a, zero_vector) == Vector(-1, 1) a = Vector(x = -2, y = -1) assert catchup_step(a, zero_vector) == Vector(-1, -1) a = Vector(x = 1, y = 2) #print(c:= catchup_step(a, zero_vector)) assert catchup_step(a, zero_vector) == Vector(1, 1) a = Vector(x = 1, y = -2) assert catchup_step(a, zero_vector) == Vector(1, -1) a = Vector(x = -1, y = 2) assert catchup_step(a, zero_vector) == Vector(-1, 1) a = Vector(x = -1, y = -2) assert catchup_step(a, zero_vector) == Vector(-1, -1) # 2, 2 off a = Vector(x = 2, y = 2) assert catchup_step(a, zero_vector) == Vector(1, 1) a = Vector(x = -2, y = 2) assert catchup_step(a, zero_vector) == Vector(-1, 1) a = Vector(x = 2, y = -2) assert catchup_step(a, zero_vector) == Vector(1, -1) a = Vector(x = -2, y = -2) assert catchup_step(a, zero_vector) == Vector(-1, -1) print("Tests pass") if 'pyodide' in sys.modules: def main_day9_2(): data = get_input('day9_2') display(f"{solve_9_2(data)=}", target="day9_2-output", append=False) elif __name__ == '__main__': with open ("input.txt", "r") as fp: data = fp.read() print(f"{solve_9_2(data)}") #test_catchup_step()``````
Scroll to see complete code

## Day 10: Cathode Ray Tube (Part 1)

I had a very long dog walk this morning, during which I chose to vastly over-engineer today's problem. Partly for the fun of visualizing the solution on a quiet walk, and partly because this is the kind of problem that tends to come back in later days.

I also thoroughly type-hinted my solution, which I find tremendously helpful in keeping new data structures straight when developing them.

• day10_1.py
• day10/computer.py
• day10/parser_10_1.py
• day10/register.py
• day10/instruction.py
• day10/noop.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 `````` ``````import sys if 'pyodide' in sys.modules: import js import os js.console.log(os.listdir('.')) from typing import Iterable from computer import Computer from parser_10_1 import Parser_10_1 def solve_10_1(data): comp = Computer(instructions=iter(data), parser=Parser_10_1()) signal_strength = 0 for breakpoint in (20 + i * 40 for i in range(6)): comp.run_until_clock(breakpoint) #print(f"Current instruction is {comp._current_instruction} with {comp._current_instruction._ellapsed_ticks} ellapsed ticks at index {comp._instruction_index}") x_register = comp.reg_x.get() #print(f"{x_register=} ") signal_value = breakpoint * comp.reg_x.get() #print(f"At {breakpoint}, signal is {signal_value}") signal_strength += signal_value #print(f"Running total {signal_strength= }\n") return(signal_strength) if 'pyodide' in sys.modules: def main_day10_1(): data = get_input('day10_1').split("\n") display(f"{solve_10_1(data)=}", target="day10_1-output", append=False) else: with open('input.txt', 'r') as fp: data = fp.read().split('\n') print(solve_10_1(data))``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 `````` ``````from typing import Iterable from instructionparser import InstructionParser from register import Register class Computer(): def __init__(self, instructions: Iterable[str], parser: InstructionParser = None): self.instructions = instructions self.parser = parser self.reg_x = Register(1) self.clock_counter = 1 self._instruction_index = -1 self.advance_instruction() def run(self) -> None: print("Computer running") while self._current_instruction is not None: self.next() def run_single_step(self) -> bool: """ Returns True if successfully completed a step, false if halted""" if self._current_instruction is not None: self.next() return True else: return False def run_until_clock(self, cycles) -> None: print(f"Computer running until clock is {cycles}") while self.clock_counter < cycles and self._current_instruction is not None: self.next() def next(self) -> None: if self._current_instruction == None: self._current_instruction = self.parser.parse(next(self.instructions)) instruction_complete = self._current_instruction.tick() if (instruction_complete): self._current_instruction.at_complete() self.clock_counter += 1 def advance_instruction(self) -> None: try: self._current_instruction = self.parser.parse(next(self.instructions), self) self._instruction_index += 1 #print(f"Instruction advanced to {self._current_instruction}") except StopIteration: self._current_instruction = None ``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 `````` ``````from instructionparser import InstructionParser, InstructionParseException from instruction import Instruction from computer import Computer from addition import Addition from noop import Noop class Parser_10_1(): def parse(self, instruction: str, computer: Computer) -> Instruction: str_instruction = instruction.split() match str_instruction: case ["noop"]: return Noop(1, computer) case ["addx", value] : return Addition(duration=2, computer=computer, register = computer.reg_x, addend = int(value)) case _: raise InstructionParseException(f"No instruction matching '{instruction}'") if __name__ == "__main__": with open('inputtest_long.txt', 'r') as fp: data = fp.read().split('\n') p = Parser_10_1() computer = Computer(None) for line in data: print(p.parse(instruction=line, computer=computer))``````
Scroll to see complete code
 ``````1 2 3 4 5 6 7 8 9 `````` ``````class Register(): def __init__(self, data): self._data = data def set(self, data): self._data = data def get(self): return self._data``````
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 `````` ``````from abc import ABC, abstractmethod #from computer import Computer class Instruction(ABC): def __init__(self, duration: int, computer): self.duration = duration self.computer = computer self._ellapsed_ticks = 0 @abstractmethod def tick(self) -> bool: ... @abstractmethod def at_complete(self) -> None: ... ``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 `````` ``````from computer import Computer from instruction import Instruction from register import Register class Addition(Instruction): def __init__(self, duration: int, computer: Computer, register: Register, addend: int): super().__init__(duration, computer) self.register: Register = register self.addend: int = addend def tick(self) -> bool: self._ellapsed_ticks += 1 return self._ellapsed_ticks == 2 def at_complete(self) -> None: self.register.set(self.register.get() + self.addend) self.computer.advance_instruction()``````
Scroll to see complete code
 ``````1 2 3 4 5 6 7 8 9 `````` ``````from instruction import Instruction class Noop(Instruction): def tick(self) -> bool: self._ellapsed_ticks += 1 return True #Only 1 tick def at_complete(self) -> None: self.computer.advance_instruction()``````

## Day 10: Cathode Ray Tube (Part 2)

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Vero recusandae sint illo ab ullam deserunt. Fugiat debitis, harum velit corporis facilis modi perferendis consequuntur et eaque libero rem minima voluptatum?

• day10_2.py
• day10/computer.py
• day10/screen.py
• day10/parser_10_1.py
• day10/register.py
• day10/instruction.py
• day10/noop.py
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 `````` ``````import sys from typing import Iterable from computer import Computer from screen import Screen from parser_10_1 import Parser_10_1 def solve_10_2(data): screen = Screen() comp = Computer(instructions=iter(data), parser=Parser_10_1()) while True: input_from_computer = comp.reg_x.get() screen.tick(input_from_computer) should_continue = comp.run_single_step() if not should_continue: break return str(screen) if 'pyodide' in sys.modules: def prepare_10_2(): import js viz_div = js.document.getElementById("day10_2-viz") pre = js.document.createElement("pre") pre.id = "day10_1-pre" pre.classList.add("bg-gray-900", "text-gray-300") pre.style.lineHeight = '1.1' viz_div.appendChild(pre) def main_day10_2(): data = get_input('day10_2').split("\n") prepare_10_2() display(f"{solve_10_2(data)}", target="day10_1-pre", append=False) else: with open('input.txt', 'r') as fp: data = fp.read().split('\n') print(solve_10_2(data))``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 `````` ``````from typing import Iterable from instructionparser import InstructionParser from register import Register class Computer(): def __init__(self, instructions: Iterable[str], parser: InstructionParser = None): self.instructions = instructions self.parser = parser self.reg_x = Register(1) self.clock_counter = 1 self._instruction_index = -1 self.advance_instruction() def run(self) -> None: print("Computer running") while self._current_instruction is not None: self.next() def run_single_step(self) -> bool: """ Returns True if successfully completed a step, false if halted""" if self._current_instruction is not None: self.next() return True else: return False def run_until_clock(self, cycles) -> None: print(f"Computer running until clock is {cycles}") while self.clock_counter < cycles and self._current_instruction is not None: self.next() def next(self) -> None: if self._current_instruction == None: self._current_instruction = self.parser.parse(next(self.instructions)) instruction_complete = self._current_instruction.tick() if (instruction_complete): self._current_instruction.at_complete() self.clock_counter += 1 def advance_instruction(self) -> None: try: self._current_instruction = self.parser.parse(next(self.instructions), self) self._instruction_index += 1 #print(f"Instruction advanced to {self._current_instruction}") except StopIteration: self._current_instruction = None ``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 `````` ``````from typing import NamedTuple class Screen(): def __init__(self): self.lines = [['.'] * 40 for _ in range(6)] self.beam_position = Position(row = 0, column = 0) def __str__(self): return '\n'.join(''.join(line) for line in self.lines) def tick(self, input_from_computer: int) -> None: if abs(self.beam_position.column - input_from_computer) <= 1: self.lines[self.beam_position.row][self.beam_position.column] = "#" next_column = self.beam_position.column + 1 if next_column >= 40: next_column = 0 next_row = self.beam_position.row + 1 else: next_row = self.beam_position.row self.beam_position = Position(row = next_row, column = next_column) class Position(NamedTuple): row: int column: int``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 `````` ``````from instructionparser import InstructionParser, InstructionParseException from instruction import Instruction from computer import Computer from addition import Addition from noop import Noop class Parser_10_1(): def parse(self, instruction: str, computer: Computer) -> Instruction: str_instruction = instruction.split() match str_instruction: case ["noop"]: return Noop(1, computer) case ["addx", value] : return Addition(duration=2, computer=computer, register = computer.reg_x, addend = int(value)) case _: raise InstructionParseException(f"No instruction matching '{instruction}'") if __name__ == "__main__": with open('inputtest_long.txt', 'r') as fp: data = fp.read().split('\n') p = Parser_10_1() computer = Computer(None) for line in data: print(p.parse(instruction=line, computer=computer))``````
Scroll to see complete code
 ``````1 2 3 4 5 6 7 8 9 `````` ``````class Register(): def __init__(self, data): self._data = data def set(self, data): self._data = data def get(self): return self._data``````
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 `````` ``````from abc import ABC, abstractmethod #from computer import Computer class Instruction(ABC): def __init__(self, duration: int, computer): self.duration = duration self.computer = computer self._ellapsed_ticks = 0 @abstractmethod def tick(self) -> bool: ... @abstractmethod def at_complete(self) -> None: ... ``````
Scroll to see complete code
 `````` 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 `````` ``````from computer import Computer from instruction import Instruction from register import Register class Addition(Instruction): def __init__(self, duration: int, computer: Computer, register: Register, addend: int): super().__init__(duration, computer) self.register: Register = register self.addend: int = addend def tick(self) -> bool: self._ellapsed_ticks += 1 return self._ellapsed_ticks == 2 def at_complete(self) -> None: self.register.set(self.register.get() + self.addend) self.computer.advance_instruction()``````
Scroll to see complete code
 ``````1 2 3 4 5 6 7 8 9 `````` ``````from instruction import Instruction class Noop(Instruction): def tick(self) -> bool: self._ellapsed_ticks += 1 return True #Only 1 tick def at_complete(self) -> None: self.computer.advance_instruction()``````