--- /dev/null
+#!/usr/bin/env pypy3.10
+
+import sys
+import argparse
+import math
+import itertools
+
+
+def parse(filename: str):
+ nodes = {}
+ with open(filename) as stream:
+ route = stream.readline().strip()
+ junk = stream.readline().strip()
+ for line in stream.readlines():
+ line = line.strip()\
+ .replace(" = ", ",")\
+ .replace("(", "")\
+ .replace(")", "")\
+ .replace(", ", ",")
+ key, left, right = line.split(",")
+ nodes[key] = (left, right)
+ return route, junk, nodes
+
+
+def step(route, node):
+ direction = next(route)
+ return node[0] if direction == 'L' else node[1]
+
+
+def part1(filename, debug=False) -> int:
+ route, junk, nodes = parse(filename)
+
+ steps = 0
+ route = itertools.cycle(route)
+ name = 'AAA'
+ while name != 'ZZZ':
+ steps += 1
+ name = step(route, nodes[name])
+ if debug:
+ print(f"{steps} {name}")
+
+ return steps
+
+
+class Path:
+ def __init__(self, route: str, nodes, start: str):
+ self.route = itertools.cycle(route)
+ self.nodes = nodes
+ self.name = start
+
+ def step(self):
+ direction = next(self.route)
+ node = self.nodes[self.name]
+ self.name = node[0] if direction == 'L' else node[1]
+
+ def is_end(self):
+ return self.name.endswith('Z')
+
+
+def part2(filename, debug=False):
+ route, junk, nodes = parse(filename)
+ paths = [Path(route, nodes, key) \
+ for key in nodes.keys() if key.endswith('A')]
+ loop = [0] * len(paths)
+ spans = [0] * len(paths)
+ steps = 0
+ while True:
+ for ndx, path in enumerate(paths):
+ path.step()
+ if path.name.endswith('Z') and spans[ndx] == 0:
+ if loop[ndx]:
+ spans[ndx] = steps - loop[ndx]
+ else:
+ loop[ndx] = steps
+ if all(spans):
+ break
+ steps += 1
+ if debug:
+ print(loop)
+ print(spans)
+ return math.lcm(*spans)
+
+
+def main(args=None):
+ parser = argparse.ArgumentParser(description="advent of code 2023 day 6")
+ parser.add_argument('filename')
+ parser.add_argument('--part1', action='store_true')
+ parser.add_argument('--part2', action='store_true')
+ parser.add_argument('--debug', action='store_true')
+ options = parser.parse_args(args)
+
+ if options.part1:
+ print(f"part 1: {part1(options.filename, debug=options.debug)}")
+ if options.part2:
+ print(f"part 2: {part2(options.filename, debug=options.debug)}")
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))