• # En Python, modélisé

    Posté par (site web personnel) . En réponse au message Avent du Code, jour 14. Évalué à 4. Dernière modification le 14 décembre 2022 à 11:40.

    from __future__ import annotations
    import enum
    import io
    import itertools
    from typing import Iterable, List, Tuple
    import numpy as np
    import numpy.typing as npt
    Coords = Tuple[int, int]
    class Material(enum.Enum):
     AIR = enum.auto()
     ROCK = enum.auto()
     SAND = enum.auto()
    class Slice:
     def __init__(self, array: npt.ArrayLike, leak: Coords) -> None:
     self.matrix: npt.NDArray = np.array(array)
     self.leak = leak
     def pour(self) -> bool:
     """Inject a unit of sand.
     Return False if it was blocked, True if it fell into the void."""
     y, x = self.leak
     while y < self.matrix.shape[0] - 1:
     y_ = y + 1
     for x_ in (x, x - 1, x + 1):
     if self.matrix[y_, x_] is Material.AIR:
     # Sand can flow down
     y = y_
     x = x_
     break
     else:
     # Blocked
     self.matrix[y, x] = Material.SAND
     return False
     # Sand fell all the way down into the void
     return True
     def __str__(self) -> str:
     result = io.StringIO()
     for line in self.matrix:
     for point in line:
     if point is Material.AIR:
     result.write(' ')
     elif point is Material.ROCK:
     result.write('█')
     elif point is Material.SAND:
     result.write('░')
     else:
     assert False # we covered all cases
     result.write('\n')
     return result.getvalue()
     def add_segment(self, start: Coords, end: Coords) -> None:
     y1, x1 = start
     y2, x2 = end
     if y1 == y2:
     x1, x2 = min(x1, x2), max(x1, x2)
     ys = (y1 for _ in range(x1, x2 + 1))
     xs = (x for x in range(x1, x2 + 1))
     elif x1 == x2:
     y1, y2 = min(y1, y2), max(y1, y2)
     ys = (y for y in range(y1, y2 + 1))
     xs = (x1 for _ in range(y1, y2 + 1))
     for y, x in zip(ys, xs):
     self.matrix[y, x] = Material.ROCK
    def import_segments(
     lines: Iterable[str]
     ) -> Tuple[List[Coords], List[Tuple[Coords, Coords]]]:
     points: List[Coords] = []
     segments: List[Tuple[Coords, Coords]] = []
     def coords(word: str) -> Coords:
     parts = word.split(',')
     if len(parts) != 2:
     raise ValueError("unexpected number of coordinates")
     return int(parts[1]), int(parts[0])
     for line in lines:
     words = line.rstrip().split(' -> ')
     segment_points = [coords(word) for word in words]
     points.extend(segment_points)
     segments.extend(itertools.pairwise(segment_points))
     return points, segments
    def import_slice1(lines: Iterable[str]) -> Slice:
     points, segments = import_segments(lines)
     y_leak, x_leak = 0, 500
     points.append((y_leak, x_leak))
     xmin = min(x for _, x in points) - 1
     xmax = max(x for _, x in points) + 1
     ymin = min(y for y, _ in points)
     ymax = max(y for y, _ in points)
     if ymin < 0:
     raise ValueError("unexpected negative coordinate")
     xshift = xmin
     matrix = np.full(((ymax + 1), xmax - xshift + 1), Material.AIR)
     result = Slice(matrix, (0, 500 - xshift))
     for (y1, x1), (y2, x2) in segments:
     result.add_segment((y1, x1 - xshift), (y2, x2 - xshift))
     return result
    def import_slice2(lines: Iterable[str]) -> Slice:
     points, segments = import_segments(lines)
     y_leak, x_leak = 0, 500
     points.append((y_leak, x_leak))
     xmin = min(x for _, x in points)
     xmax = max(x for _, x in points)
     ymin = min(y for y, _ in points)
     ymax = max(y for y, _ in points) + 2 # including the floor
     # Update xmin and xmax to accomodate a pyramid of sand
     xmin = min(xmin, x_leak - (ymax - y_leak))
     xmax = max(xmax, x_leak + (ymax - y_leak))
     if ymin < 0:
     raise ValueError("unexpected negative coordinate")
     xshift = xmin
     matrix = np.full(((ymax + 1), xmax - xshift + 1), Material.AIR)
     result = Slice(matrix, (0, 500 - xshift))
     for (y1, x1), (y2, x2) in segments:
     result.add_segment((y1, x1 - xshift), (y2, x2 - xshift))
     result.add_segment((ymax, xmin - xshift), (ymax, xmax - xshift))
     return result
    def solve1(lines: Iterable[str]) -> int:
     """Solve part 1 of today's puzzle"""
     slice_ = import_slice1(lines)
     for i in range(10000):
     if slice_.pour():
     return i
     raise ValueError("Simulantion did not end within expected limit")
    def solve2(lines: Iterable[str]) -> int:
     """Solve part 2 of today's puzzle"""
     slice_ = import_slice2(lines)
     for i in range(100000):
     _ = slice_.pour()
     if slice_.matrix[slice_.leak] is Material.SAND:
     return i + 1
     raise ValueError("Simulantion did not end within expected limit")