Advent of Code 2022

Published November 26, 2022

Tags: codeadvent python pyscript

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.

Table of Contents

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'<span style="text-shadow: 0 0 5px #ffffff; color: rgb(255, 255, 255)">{commonChar}</span>')
        second = second.replace(commonChar, f'<span style="text-shadow: 0 0 5px #ffffff; color: rgb(255, 255, 255)">{commonChar}</span>')

        #Format and insert text
        firstElement.innerHTML = "<span>" + first + "</span>"
        secondElement.innerHTML = "<span>" + second + "</span>"
        scoreElement.innerHTML = f"<span>{commonChar}={score}</span>"

        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[0] <= b[0] and a[1] >= b[1]

def solutionFromInput(data):
    pairs = (line.split(',') for line in data)
    pairs = ((p[0].split('-'), p[1].split('-')) for p in pairs)
    pairs = (((int(p[0][0]), int(p[0][1])), (int(p[1][0]), int(p[1][1]))) for p in pairs)
    matches = (fullyContains(pair[0], pair[1]) or fullyContains(pair[1],pair[0]) 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[0] <= b[0] <= a[1]) or (a[0] <= b[1] <= a[1]) or \
           (b[0] <= a[0] <= b[1]) or (b[0] <= a[1] <= b[1])

def solutionFromInput(data):
    pairs = (line.split(',') for line in data)
    pairs = ((p[0].split('-'), p[1].split('-')) for p in pairs)
    pairs = (((int(p[0][0]), int(p[0][1])), (int(p[1][0]), int(p[1][1]))) for p in pairs)
    matches = (overlaps(pair[0], pair[1]) 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[0] #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<num>\d+) from (?P<from_stack>\d+) to (?P<to_stack>\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[0] #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<num>\d+) from (?P<from_stack>\d+) to (?P<to_stack>\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)

    #<button onclick="startAnimation()">Play</button>
    #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"
    

    #<button onclick="stopAnimation()">Pause</button>
    #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"

    #<input type="range" id="seekbar" min="0" max="100" value="0" oninput="myTimeline.pause();myTimeline.seek(myTimeline.duration * (this.value/100))" style="width: 100%"></progress>
    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[0] #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<num>\d+) from (?P<from_stack>\d+) to (?P<to_stack>\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[0]) == "$ 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][0]
        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[0]) == "$ 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'<span style="text-shadow: 0 0 8px #ffffff; color: rgb(255, 255, 255)">{contents}</span>'
            else:
                contents = f'<span>{contents}</span>'
            
            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][0]
            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[0]) == "$ 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][0]
        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[0]) == "$ 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'<span style="text-shadow: 0 0 8px #ffffff; color: rgb(255, 255, 255)">{contents}</span>'
            else:
                contents = f'<span>{contents}</span>'
            
            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][0]
            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[0])):
        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[0])):
        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[0])):
        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[0])):
        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[0])
            tails[0] = Point(tails[0].x + diff.x, tails[0].y + diff.y, Vector(tails[0].x, tails[0].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/addition.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/addition.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()