|
| 1 | +"""Day 14: Regolith Reservoir |
| 2 | +
|
| 3 | +This module provides the solution for Advent of Code 2022 - Day 14. |
| 4 | +
|
| 5 | +It simulates falling sand in a cave system with rock formations, tracking |
| 6 | +how sand accumulates as it falls from a source point following gravity rules. |
| 7 | +
|
| 8 | +The module contains a Solution class that inherits from SolutionBase for |
| 9 | +parsing rock formations and simulating sand physics. |
| 10 | +""" |
| 11 | + |
| 12 | +from itertools import pairwise |
| 13 | +import re |
| 14 | +from typing import ClassVar |
| 15 | + |
| 16 | +from aoc.models.base import SolutionBase |
| 17 | + |
| 18 | + |
| 19 | +class Solution(SolutionBase): |
| 20 | + """Simulate falling sand in cave system with rock formations. |
| 21 | +
|
| 22 | + This solution models sand falling from point (500, 0) following physics rules: |
| 23 | + sand tries to move down, then down-left, then down-right. Part 1 counts sand |
| 24 | + units that settle before falling into the abyss. Part 2 adds an infinite floor |
| 25 | + and counts units until the source is blocked. |
| 26 | +
|
| 27 | + The simulation uses set-based collision detection for efficient position tracking. |
| 28 | + """ |
| 29 | + |
| 30 | + REGEX: ClassVar[re.Pattern[str]] = re.compile(r"(\d+,\d+)") |
| 31 | + SAND_SOURCE: ClassVar[tuple[int, int]] = (500, 0) |
| 32 | + |
| 33 | + def parse_data(self, data: list[str]) -> set[tuple[int, int]]: |
| 34 | + """Parse rock formation coordinates into occupied positions set. |
| 35 | +
|
| 36 | + Converts line segment notation (e.g., "498,4 -> 498,6 -> 496,6") into |
| 37 | + individual coordinate points by drawing lines between each pair of points. |
| 38 | +
|
| 39 | + Args: |
| 40 | + data: List of strings describing rock paths as coordinate pairs |
| 41 | + separated by " -> " |
| 42 | +
|
| 43 | + Returns |
| 44 | + ------- |
| 45 | + set[tuple[int, int]]: Set of (x, y) coordinates occupied by rocks |
| 46 | + """ |
| 47 | + rocks = set() |
| 48 | + |
| 49 | + for line in data: |
| 50 | + matches = re.findall(self.REGEX, line) |
| 51 | + coords = [(int(x), int(y)) for x, y in [match.split(",") for match in matches]] |
| 52 | + |
| 53 | + for (x1, y1), (x2, y2) in pairwise(coords): |
| 54 | + # Vertical line |
| 55 | + if x1 == x2: |
| 56 | + for y in range(min(y1, y2), max(y1, y2) + 1): |
| 57 | + rocks.add((x1, y)) |
| 58 | + |
| 59 | + # Horizontal line |
| 60 | + else: |
| 61 | + for x in range(min(x1, x2), max(x1, x2) + 1): |
| 62 | + rocks.add((x, y1)) |
| 63 | + |
| 64 | + return rocks |
| 65 | + |
| 66 | + def simulate( |
| 67 | + self, rocks: set[tuple[int, int]], max_depth: int, *, has_floor: bool = False |
| 68 | + ) -> int: |
| 69 | + """Simulate sand falling and settling until termination condition met. |
| 70 | +
|
| 71 | + Each sand unit falls from source (500, 0) following movement rules: |
| 72 | + 1. Try to move down (0, +1) |
| 73 | + 2. Try to move down-left (-1, +1) |
| 74 | + 3. Try to move down-right (+1, +1) |
| 75 | + 4. If all blocked, settle at current position |
| 76 | +
|
| 77 | + Args: |
| 78 | + rocks: Set of rock positions that block sand |
| 79 | + max_depth: Maximum y-coordinate of rocks (for abyss detection) |
| 80 | + has_floor: If True, simulate infinite floor at max_depth + 2 |
| 81 | +
|
| 82 | + Returns |
| 83 | + ------- |
| 84 | + int: Number of sand units that settled before termination condition |
| 85 | + """ |
| 86 | + settled_sand = set() |
| 87 | + fall_offsets = [(0, 1), (-1, 1), (1, 1)] # down, down-left, down-right |
| 88 | + floor_level = max_depth + 2 if has_floor else None |
| 89 | + |
| 90 | + while True: |
| 91 | + sand_x, sand_y = self.SAND_SOURCE |
| 92 | + |
| 93 | + while True: |
| 94 | + if has_floor and sand_y + 1 == floor_level: |
| 95 | + settled_sand.add((sand_x, sand_y)) |
| 96 | + break |
| 97 | + |
| 98 | + if not has_floor and sand_y >= max_depth: |
| 99 | + return len(settled_sand) |
| 100 | + |
| 101 | + moved = False |
| 102 | + for dx, dy in fall_offsets: |
| 103 | + next_pos = (sand_x + dx, sand_y + dy) |
| 104 | + |
| 105 | + if next_pos not in rocks and next_pos not in settled_sand: |
| 106 | + sand_x, sand_y = next_pos |
| 107 | + moved = True |
| 108 | + break |
| 109 | + |
| 110 | + if not moved: |
| 111 | + settled_sand.add((sand_x, sand_y)) |
| 112 | + |
| 113 | + if has_floor and (sand_x, sand_y) == self.SAND_SOURCE: |
| 114 | + return len(settled_sand) |
| 115 | + |
| 116 | + break |
| 117 | + |
| 118 | + def part1(self, data: list[str]) -> int: |
| 119 | + """Count sand units that settle before falling into abyss. |
| 120 | +
|
| 121 | + Simulates sand falling until a unit falls past the lowest rock formation, |
| 122 | + indicating it would fall forever into the endless void below. |
| 123 | +
|
| 124 | + Args: |
| 125 | + data: List of strings describing rock formations |
| 126 | +
|
| 127 | + Returns |
| 128 | + ------- |
| 129 | + int: Number of sand units that came to rest before sand starts |
| 130 | + flowing into the abyss |
| 131 | + """ |
| 132 | + rocks = self.parse_data(data) |
| 133 | + max_depth = max(y for _, y in rocks) |
| 134 | + |
| 135 | + return self.simulate(rocks, max_depth, has_floor=False) |
| 136 | + |
| 137 | + def part2(self, data: list[str]) -> int: |
| 138 | + """Count sand units that settle before blocking the source. |
| 139 | +
|
| 140 | + Adds an infinite horizontal floor 2 units below the lowest rock. |
| 141 | + Simulates until sand accumulates to block the source point at (500, 0). |
| 142 | +
|
| 143 | + Args: |
| 144 | + data: List of strings describing rock formations |
| 145 | +
|
| 146 | + Returns |
| 147 | + ------- |
| 148 | + int: Number of sand units that came to rest before the source |
| 149 | + becomes blocked (including the unit that blocks it) |
| 150 | + """ |
| 151 | + rocks = self.parse_data(data) |
| 152 | + max_depth = max(y for _, y in rocks) |
| 153 | + |
| 154 | + return self.simulate(rocks, max_depth, has_floor=True) |
0 commit comments