diff --git a/day6/solver.py b/day6/solver.py index 60b192d..2128fcc 100644 --- a/day6/solver.py +++ b/day6/solver.py @@ -1,10 +1,15 @@ import sys +from copy import copy, deepcopy +import operator as op from typing import Optional, Self, TypeAlias import logging logger = logging.getLogger() +class SimulationLooping(Exception): + ... + class Direction: steps = [(0, -1), (1, 0), (0, 1), (-1, 0)] @@ -25,9 +30,18 @@ class Direction: def __str__(self): return self.chars[self.angle] + def __repr__(self): + return f"" + def turn(self, amount=1): return Direction((self.angle + amount) % 4) + def __hash__(self): + return hash(self.angle) + + def __eq__(self, other): + return self.angle == other.angle + class Position: __slots__ = ('x', 'y') @@ -68,30 +82,43 @@ class LabMap: """ return pos in self.obstacles + def __deepcopy__(self, memo): + return LabMap(self.width, self.height, copy(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() + self.visited: set[tuple[Position, Direction]] = set() if pos is not None: - self.visited.add(pos) + self.visited.add((pos, direction)) def visit(self, pos: Position): self.pos = pos - self.visited.add(pos) + key = (pos, self.direction) + seen_before = key in self.visited + self.visited.add(key) + return seen_before + + def get_path(self): + return set(map(op.itemgetter(0), self.visited)) def leave(self): self.pos = None def total_visited(self): - return len(self.visited) + return len(self.get_path()) def turn(self): self.direction = self.direction.turn() + def __deepcopy__(self, memo): + guard = Guard(self.pos, self.direction) + guard.visited = copy(self.visited) + return guard + class Simulation: def __init__(self, map: LabMap, guards: list[Guard]): @@ -100,6 +127,12 @@ class Simulation: self.ticker = 0 + def __deepcopy__(self, memo): + sim = Simulation(deepcopy(self.map, memo), deepcopy(self.guards, memo)) + sim.ticker = self.ticker + return sim + + @classmethod def from_string(cls, simstring: str) -> Self: """ @@ -164,6 +197,7 @@ class Simulation: """ Tick the simulation. """ + looping = None for guard in self.guards: if (pos := guard.pos) is not None: pos = pos.move(guard.direction) @@ -171,11 +205,15 @@ class Simulation: if self.map.is_obstacle(pos): guard.turn() else: - guard.visit(pos) + looper = guard.visit(pos) + looping = looper if looping is None else looping & looper else: guard.leave() self.ticker += 1 + if looping: + raise SimulationLooping("All guards are looping!") + # print(f"Tick {self.ticker}") # print(self.to_string()) @@ -186,6 +224,33 @@ class Simulation: self.tick() +def find_good_obstacles(sim: Simulation): + """ + Examine alternate universes to compute a list of good obstacle locations. + + NOTE: This is far from the most efficient approach. + We could also memoise guard paths from each alt universe simulation + to 'fast-forward' time between changes. + """ + guard = sim.guards[0] + good_obstacles = [] + tried_obstacles = set() + + while any(guard.pos is not None for guard in sim.guards): + sim_alt = deepcopy(sim) + sim.tick() + pos = guard.pos + if pos is not None and pos not in tried_obstacles: + sim_alt.map.obstacles.add(pos) + try: + sim_alt.run() + except SimulationLooping: + good_obstacles.append(pos) + tried_obstacles.add(pos) + + return good_obstacles + + def main(): with open(sys.argv[1]) as file: map = file.read().strip() @@ -196,6 +261,12 @@ def main(): print(f"Simulation complete after {simulation.ticker} steps.") print(f"Total nodes visited: {simulation.total_visited()}") + print("Loading Simulation again") + simulation = Simulation.from_string(map) + print("Searching alternate universes for good obtacle placement.") + result = find_good_obstacles(simulation) + print(f"Found {len(result)} good placements for obstacles!") + if __name__ == '__main__': main() diff --git a/day6/test.py b/day6/test.py index 1af96d5..dbba10c 100644 --- a/day6/test.py +++ b/day6/test.py @@ -1,4 +1,4 @@ -from solver import Simulation +from solver import Simulation, find_good_obstacles test_data = r""" @@ -22,6 +22,14 @@ def test_visited(): assert visited == 41 print("Visited test passed") +def test_loop_obstacles(): + print("Beginning loop inducing obstacle placement test") + sim = Simulation.from_string(test_data) + places = find_good_obstacles(sim) + assert len(places) == 6 + print(f"Loop inducing obstacle placement test complete") + if __name__ == '__main__': test_visited() + test_loop_obstacles()