Advent of Code 2022 day 14

Relatively straightforward. Decided to keep track of things with classes.

# with open("14_test.txt") as f:
with open("14.txt") as f:
    data = f.read()

def sign(x):
    if x < 0:
        return -1
    return 1

class Node:
    def __init__(self, data):
        self.x, self.y = [int(c) for c in data.split(",")]

    def __str__(self):
        return f"{self.x}, {self.y}"

class Line:
    def __init__(self, data):
        self.line = [Node(n) for n in data.split(" -> ")]

    @property
    def min_w(self):
        return min(node.x for node in self.line)

    @property
    def max_w(self):
        return max(node.x for node in self.line)

    @property
    def h(self):
        return max(node.y for node in self.line)

    def __iter__(self):
        return iter(self.line)

    def __getitem__(self, i):
        return self.line[i]

class Grid:
    def __init__(self, data):
        self.lines = [Line(l) for l in data.splitlines()]
        self.min_w = min(line.min_w for line in self.lines)
        self.max_w = max(line.max_w for line in self.lines)
        self.h = max(line.h for line in self.lines)
        self.grid = [
            ["." for _ in range(self.max_w-self.min_w+1)]
            for _ in range(self.h+1)
        ]
        for line in self.lines:
            for node, old_node in zip(line[1:], line):
                self.fill_line(node, old_node)
        self.comes_to_rest = True
        self.sand_start = Node("500, 0")

    def __str__(self):
        return "\n".join([
            "".join(
                row
            )
            for row in self.grid
        ])


    def block(self, node, char="#"):
        self.grid[node.y][node.x-self.min_w] = char

    def between(self, start, end):
        if start.x != end.x:
            return [
                Node(f"{i}, {end.y}")
                for i in range(start.x, end.x, sign(end.x-start.x))
            ]
        return [
            Node(f"{end.x}, {j}")
            for j in range(start.y, end.y, sign(end.y-start.y))
        ]

    def fill_line(self, end, start):
        self.block(start)
        self.block(end)
        for node in self.between(start, end):
            self.block(node)

    def at(self, x, y):
        if x >= self.min_w and y >= 0 and x < self.max_w+1 and y < self.h+1:
            return self.grid[y][x-self.min_w]
        return False

    def speck_movable(self, speck):
        if (
                self.at(x=speck.x, y=speck.y+1) == "." or
                self.at(x=speck.x-1, y=speck.y+1) == "." or
                self.at(x=speck.x+1, y=speck.y+1) == "."
        ):
            return True
        return False

    def move_speck(self, speck):
        if self.at(x=speck.x, y=speck.y+1) == ".":
            return Node(f"{speck.x}, {speck.y+1}")
        elif self.at(x=speck.x-1, y=speck.y+1) == ".":
            return Node(f"{speck.x-1}, {speck.y+1}")
        elif self.at(x=speck.x+1, y=speck.y+1) == ".":
            return Node(f"{speck.x+1}, {speck.y+1}")

    def sand(self):
        speck = self.sand_start
        while self.speck_movable(speck):
            speck = self.move_speck(speck)
        if speck.x == self.min_w or speck.x == self.max_w or speck.y == self.h:
            self.comes_to_rest = False
        else:
            self.block(speck, char="o")

    def answer(self):
        for i in range(100000):
            self.sand()
            print(self)
            if not self.comes_to_rest:
                return i

grid = Grid(data)
print(grid.answer())

# part 2
class Grid2:
    def __init__(self, data):
        self.lines = [Line(l) for l in data.splitlines()]
        self.h = max(line.h for line in self.lines)+2
        self.max_w = max(line.max_w for line in self.lines)
        self.N = 2*self.max_w+1
        self.grid = [
            ["." for _ in range(self.N)]
            for _ in range(self.h+1)
        ]
        for x in range(self.N):
            self.grid[-1][x] = "#"
        for line in self.lines:
            for node, old_node in zip(line[1:], line):
                self.fill_line(node, old_node)
        self.sand_start = Node("500, 0")
        self.completed = False

    def __str__(self):
        return "\n".join([
            "".join(
                row
            )
            for row in self.grid
        ])


    def block(self, node, char="#"):
        self.grid[node.y][node.x] = char

    def between(self, start, end):
        if start.x != end.x:
            return [
                Node(f"{i}, {end.y}")
                for i in range(start.x, end.x, sign(end.x-start.x))
            ]
        return [
            Node(f"{end.x}, {j}")
            for j in range(start.y, end.y, sign(end.y-start.y))
        ]

    def fill_line(self, end, start):
        self.block(start)
        self.block(end)
        for node in self.between(start, end):
            self.block(node)

    def at(self, x, y):
        if x >= 0 and y >= 0 and x < self.N and y < self.h+1:
            return self.grid[y][x]
        return False

    def speck_movable(self, speck):
        if (
                self.at(x=speck.x, y=speck.y+1) == "." or
                self.at(x=speck.x-1, y=speck.y+1) == "." or
                self.at(x=speck.x+1, y=speck.y+1) == "."
        ):
            return True
        return False

    def move_speck(self, speck):
        if self.at(x=speck.x, y=speck.y+1) == ".":
            return Node(f"{speck.x}, {speck.y+1}")
        elif self.at(x=speck.x-1, y=speck.y+1) == ".":
            return Node(f"{speck.x-1}, {speck.y+1}")
        elif self.at(x=speck.x+1, y=speck.y+1) == ".":
            return Node(f"{speck.x+1}, {speck.y+1}")

    def sand(self):
        speck = self.sand_start
        while self.speck_movable(speck):
            speck = self.move_speck(speck)
        if speck.x == self.sand_start.x and speck.y == self.sand_start.y:
            self.completed = True
        else:
            self.block(speck, char="o")

    def answer(self):
        for i in range(1, 100000):
            self.sand()
            if self.completed:
                return i

grid = Grid2(data)
print(grid.answer())