--- /dev/null
+from dataclasses import dataclass
+from typing import List, Tuple
+
+LEFT = (0, -1)
+UP = (-1, 0)
+RIGHT = (0, 1)
+DOWN = (1, 0)
+
+
+@dataclass
+class Problem:
+ data: List[List[int]]
+
+ def trailheads(self):
+ for row, values in enumerate(self.data):
+ for col, value in enumerate(values):
+ if value == 0:
+ yield (row, col)
+
+ def score(self, head: Tuple[int, int]) -> List[Tuple[int, int]]:
+ score = []
+ rows, cols = len(self.data), len(self.data[0])
+ val = self.data[head[0]][head[1]]
+ if val < 9:
+ nextval = val + 1
+ for dir in (LEFT, UP, RIGHT, DOWN):
+ row = head[0] + dir[0]
+ col = head[1] + dir[1]
+ if row >= 0 and row < rows and col >= 0 and col < cols:
+ if self.data[row][col] == nextval:
+ score += self.score((row, col))
+ else:
+ return [head]
+ return score
+
+ def run(self, part2: bool = False, debug: bool = False) -> int:
+ result = {}
+ rows, cols = len(self.data), len(self.data[0])
+ for head in self.trailheads():
+ if part2:
+ result[head] = self.score(head)
+ else:
+ result[head] = set(self.score(head))
+
+ sum = 0
+ for k in result:
+ sum += len(result[k])
+ if debug:
+ print('for head', k, ' score ', len(result[k]), result[k])
+ return sum
+
+ @staticmethod
+ def from_stream(stream) -> 'Problem':
+ data = []
+ for line in stream:
+ data.append([int(x) for x in line.strip()])
+ return Problem(data)
--- /dev/null
+import sys
+import argparse
+from . import Problem
+
+
+def main(args=None):
+ parser = argparse.ArgumentParser(description="AOC 2024 day 10")
+ parser.add_argument('filename', type=str)
+ parser.add_argument('-d', '--debug', action='store_true')
+ parser.add_argument('-2', '--part2', action='store_true')
+ options = parser.parse_args(args)
+
+ with open(options.filename) as f:
+ problem = Problem.from_stream(f)
+ if options.debug:
+ for row in problem.data:
+ print(row)
+
+ print(f"result {problem.run(part2=options.part2, debug=options.debug)}")
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))