From baf45b78d1590aa232121ffc6b22c3cff930b41a Mon Sep 17 00:00:00 2001 From: Interitio Date: Sat, 7 Dec 2024 00:43:14 +1000 Subject: [PATCH] Day 6 part 1 solution. --- day6/solver.py | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ day6/test.py | 27 +++++++ 2 files changed, 228 insertions(+) create mode 100644 day6/solver.py create mode 100644 day6/test.py diff --git a/day6/solver.py b/day6/solver.py new file mode 100644 index 0000000..60b192d --- /dev/null +++ b/day6/solver.py @@ -0,0 +1,201 @@ +import sys +from typing import Optional, Self, TypeAlias +import logging + + +logger = logging.getLogger() + + +class Direction: + steps = [(0, -1), (1, 0), (0, 1), (-1, 0)] + chars = ['^', '>', 'v', '<'] + + def __init__(self, angle: int): + self.angle = angle + + @classmethod + def parse(cls, char: str): + angle = cls.chars.index(char) + return cls(angle) + + @property + def step(self): + return self.steps[self.angle] + + def __str__(self): + return self.chars[self.angle] + + def turn(self, amount=1): + return Direction((self.angle + amount) % 4) + + +class Position: + __slots__ = ('x', 'y') + + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + def __repr__(self): + return f"" + + def move(self, direction: Direction): + xdiff, ydiff = direction.step + return Position(self.x + xdiff, self.y + ydiff) + + def __hash__(self): + return hash((self.x, self.y)) + + def __eq__(self, other): + return self.x == other.x and self.y == other.y + + +class LabMap: + def __init__(self, width: int, height: int, obstacles: set[Position]): + self.width = width + self.height = height + self.obstacles = obstacles + + def in_map(self, pos: Position): + """ + Test whether a given position is in the map. + """ + return (0 <= pos.x < self.width) and (0 <= pos.y < self.height) + + def is_obstacle(self, pos: Position): + """ + Test whether the position is an obstacle. + """ + return pos in self.obstacles + + +class Guard: + def __init__(self, pos: Optional[Position], direction: Direction): + self.pos = pos + self.direction = direction + + # TODO: If cycles are a concern, make this (pos, direction) + self.visited: set[Position] = set() + if pos is not None: + self.visited.add(pos) + + def visit(self, pos: Position): + self.pos = pos + self.visited.add(pos) + + def leave(self): + self.pos = None + + def total_visited(self): + return len(self.visited) + + def turn(self): + self.direction = self.direction.turn() + + +class Simulation: + def __init__(self, map: LabMap, guards: list[Guard]): + self.map = map + self.guards = guards + + self.ticker = 0 + + @classmethod + def from_string(cls, simstring: str) -> Self: + """ + Create a Simulation from a starting string. + """ + if not simstring: + raise ValueError("Empty simulation string provided") + + obstacles = [] + guards = [] + lines = simstring.splitlines() + width = len(lines[0]) + height = len(lines) + for i, line in enumerate(simstring.splitlines()): + line = line.strip() + if not line: + continue + for j, char in enumerate(line): + match char: + case '#': + pos = Position(j, i) + obstacles.append(pos) + case '>' | 'v' | '<' | '^': + pos = Position(j, i) + direction = Direction.parse(char) + guard = Guard(pos, direction) + guards.append(guard) + case '.': + pass + case _: + logger.warning(f"Unknown map char: '{char}'") + + map = LabMap(width, height, set(obstacles)) + return cls(map, guards) + + def to_string(self) -> str: + """ + Create a simulation map string. + Mainly for debugging. + """ + lines = [] + + # Initialise the list + for _ in range(self.map.height): + line = self.map.width * ['.'] + lines.append(line) + + for obstacle in self.map.obstacles: + lines[obstacle.y][obstacle.x] = '#' + + for guard in self.guards: + pos = guard.pos + if pos is not None: + lines[pos.y][pos.x] = str(guard.direction) + + return '\n'.join(''.join(line) for line in lines) + + def total_visited(self) -> int: + return sum(guard.total_visited() for guard in self.guards) + + def tick(self): + """ + Tick the simulation. + """ + for guard in self.guards: + if (pos := guard.pos) is not None: + pos = pos.move(guard.direction) + if self.map.in_map(pos): + if self.map.is_obstacle(pos): + guard.turn() + else: + guard.visit(pos) + else: + guard.leave() + self.ticker += 1 + + # print(f"Tick {self.ticker}") + # print(self.to_string()) + + def run(self): + if not self.guards: + raise ValueError("Simulation has no guards to move!") + while any(guard.pos is not None for guard in self.guards): + self.tick() + + +def main(): + with open(sys.argv[1]) as file: + map = file.read().strip() + print("Loading Simulation") + simulation = Simulation.from_string(map) + print("Running Simulation") + simulation.run() + print(f"Simulation complete after {simulation.ticker} steps.") + print(f"Total nodes visited: {simulation.total_visited()}") + + +if __name__ == '__main__': + main() diff --git a/day6/test.py b/day6/test.py new file mode 100644 index 0000000..1af96d5 --- /dev/null +++ b/day6/test.py @@ -0,0 +1,27 @@ +from solver import Simulation + + +test_data = r""" +....#..... +.........# +.......... +..#....... +.......#.. +.......... +.#..^..... +........#. +#......... +......#... +""".strip() + +def test_visited(): + print("Beginning visited test") + sim = Simulation.from_string(test_data) + sim.run() + visited = sim.total_visited() + assert visited == 41 + print("Visited test passed") + + +if __name__ == '__main__': + test_visited()