From 3f87c8906e638e17ccde9c0d4db6bda4b990945f Mon Sep 17 00:00:00 2001 From: alexo Date: Thu, 28 Dec 2023 17:35:11 +1100 Subject: [PATCH] day22: coverage minus vpython --- day22/day22.py | 148 +++++++++++++----------------------- day22/lib/classes.py | 17 +++-- day22/lib/parsers.py | 2 - day22/lib/vis.py | 96 +++++++++++++++++++++++ day22/tests/__init__.py | 0 day22/tests/test_day22.py | 16 ++++ day22/tests/test_parsers.py | 10 +++ pyproject.toml | 3 +- 8 files changed, 188 insertions(+), 104 deletions(-) create mode 100644 day22/lib/vis.py create mode 100644 day22/tests/__init__.py create mode 100644 day22/tests/test_day22.py create mode 100644 day22/tests/test_parsers.py diff --git a/day22/day22.py b/day22/day22.py index 50548cc..9713c26 100644 --- a/day22/day22.py +++ b/day22/day22.py @@ -1,35 +1,15 @@ -import random -from typing import Any - import vpython -from day22.lib.classes import BoxData, Matrix, Vector3 +from day22.lib.classes import BoxData, Matrix from day22.lib.parsers import get_boxes +from day22.lib.vis import CAMERA_AXIS_1, CAMERA_POS_1, bind_keys, follow_block, init_vis INPUT = "day22/input.txt" -INPUT_SMALL = "input-small.txt" - - -def construct_box(box_data: BoxData, color: vpython.vector) -> vpython.box: - return vpython.box( - pos=box_data.vpos, - length=box_data.length, - height=box_data.height, - width=box_data.width, - color=color, - ) - - -def random_color() -> vpython.vector: - hsv = vpython.vector(random.random(), random.uniform(0.5, 1.0), 1.0) - return vpython.color.hsv_to_rgb(hsv) - +INPUT_SMALL = "day22/input-small.txt" -CAMERA_START = vpython.vector(40.0164, 11.0337, 39.9238) -CAMERA_AXIS = -CAMERA_START -CAMERA_POS_1 = vpython.vector(111.512, 122.347, 65.0144) -CAMERA_AXIS_1 = vpython.vector(-83.3746, -20.0517, -69.4084) +AUTO_START = False +ANIMATE = True class Visualization: @@ -37,60 +17,34 @@ class Visualization: matrix: Matrix has_started: bool - def __init__(self, auto_start: bool = False, animate: bool = True) -> None: - self.boxes = get_boxes(INPUT) + def __init__(self, boxes: list[BoxData], animate: bool = True) -> None: + self.boxes = boxes self.boxes.sort(key=lambda x: x.z_val_bot) self.matrix = Matrix() - self.init_vis() - self.has_started = False - vpython.scene.bind("keydown", self.on_key_input) self.animate = animate - if auto_start: - self.start() - - def init_vis(self) -> None: - """Initialize vis boxes""" - for box in self.boxes: - color = random_color() - vbox = construct_box(box, color) - box.set_vbox(vbox) - ground = BoxData("ground", Vector3(0, 0, 0), Vector3(10, 10, 0)) - construct_box(ground, random_color()) - # init camera: - vpython.scene.camera.axis = CAMERA_AXIS - vpython.scene.camera.pos = CAMERA_START - vpython.scene.height = 600 - vpython.scene.width = 400 - - def on_key_input(self, evt: Any) -> None: - character = evt.key - if character == "shift": - return - print(character) - if character in ["\n", "enter", "return"]: - self.start() - if character == "c": - print(vpython.scene.camera.pos) - print(vpython.scene.camera.axis) - - def vis_sleep(self, time: float) -> None: if self.animate: - vpython.sleep(time) + init_vis(self.boxes) + bind_keys(self.start) + + self.has_started = False def vis_rate(self, rate: float) -> None: if self.animate: vpython.rate(rate) def follow_block(self, y: float, box: BoxData) -> None: - pos = vpython.scene.camera.pos - pos.y = max(y + box.start_pos.z, pos.y) - vpython.scene.camera.pos = pos + if self.animate: + follow_block(y, box) def start(self) -> None: - if self.has_started: + if self.has_started: # pragma: no cover return self.has_started = True - camera_height = vpython.scene.camera.pos.y + + if self.animate: + camera_height = vpython.scene.camera.pos.y + else: + camera_height = 0 for box in self.boxes: while self.matrix.can_fall_down(box): @@ -108,6 +62,7 @@ def start(self) -> None: print(self.calculate_part1()) print(self.calculate_part2()) + self.animate_part1() self.animate_part2() @@ -119,44 +74,47 @@ def calculate_part2(self) -> int: return sum(len(box.recursive_fall({box})) for box in self.boxes) def animate_part1(self) -> None: - if not self.animate: - return - vpython.scene.camera.pos = CAMERA_POS_1 - vpython.scene.camera.axis = CAMERA_AXIS_1 - for box in self.boxes: - if self.matrix.can_fly_up(box): - box.select() - self.vis_rate(60) - - for box in self.boxes: - if self.matrix.can_fly_up(box): - box.unselect() - self.vis_rate(60) + if self.animate: + vpython.scene.camera.pos = CAMERA_POS_1 + vpython.scene.camera.axis = CAMERA_AXIS_1 + for box in self.boxes: + if self.matrix.can_fly_up(box): + box.select() + self.vis_rate(60) + + for box in self.boxes: + if self.matrix.can_fly_up(box): + box.unselect() + self.vis_rate(60) def animate_part2(self) -> None: - if not self.animate: - return - vpython.scene.camera.pos = CAMERA_POS_1 - vpython.scene.camera.axis = CAMERA_AXIS_1 - reversed_boxes = sorted(self.boxes, key=lambda box: box.end_pos.z, reverse=True) - for box in reversed_boxes: - to_fall = box.recursive_fall({box}) - to_fall_sorted = sorted(to_fall, key=lambda x: x.z_val_bot) - if len(to_fall_sorted) == 0: - continue + if self.animate: + vpython.scene.camera.pos = CAMERA_POS_1 + vpython.scene.camera.axis = CAMERA_AXIS_1 + reversed_boxes = sorted( + self.boxes, key=lambda box: box.end_pos.z, reverse=True + ) + for box in reversed_boxes: + to_fall = box.recursive_fall({box}) + to_fall_sorted = sorted(to_fall, key=lambda x: x.z_val_bot) + if len(to_fall_sorted) == 0: + continue - for faller in to_fall_sorted: - faller.select() + for faller in to_fall_sorted: + faller.select() - self.vis_rate(20) + self.vis_rate(20) - for faller in to_fall: - faller.unselect() + for faller in to_fall: + faller.unselect() def main() -> None: - auto_start = True - vis = Visualization(auto_start, True) + boxes: list[BoxData] = get_boxes(INPUT) + vis = Visualization(boxes, ANIMATE) + + if AUTO_START: + vis.start() while True: vpython.rate(165) # we control sleeping diff --git a/day22/lib/classes.py b/day22/lib/classes.py index 9c167e5..38caa14 100644 --- a/day22/lib/classes.py +++ b/day22/lib/classes.py @@ -16,7 +16,9 @@ class BoxData: name: str start_pos: Vector3 end_pos: Vector3 - vbox: vpython.box = field(init=False, repr=False, hash=False) + vbox: Optional[vpython.box] = field( + init=False, repr=False, hash=False, default=None + ) supports: set["BoxData"] = field( default_factory=set, hash=False, repr=False ) # list of blocks we support @@ -59,15 +61,18 @@ def fall(self) -> None: self.start_pos.z -= 1 self.end_pos.z -= 1 # vbox y == boxdata z - self.vbox.pos.y -= 1 + if self.vbox is not None: + self.vbox.pos.y -= 1 def select(self) -> None: - self.vbox.pos.x += 30 - self.vbox.pos.z -= 30 + if self.vbox is not None: + self.vbox.pos.x += 30 + self.vbox.pos.z -= 30 def unselect(self) -> None: - self.vbox.pos.x -= 30 - self.vbox.pos.z += 30 + if self.vbox is not None: + self.vbox.pos.x -= 30 + self.vbox.pos.z += 30 def set_supports(self, supports: set["BoxData"]) -> None: """blocks under us""" diff --git a/day22/lib/parsers.py b/day22/lib/parsers.py index f3bd6a2..1ce6f46 100644 --- a/day22/lib/parsers.py +++ b/day22/lib/parsers.py @@ -11,8 +11,6 @@ def get_boxes(path: str) -> list[BoxData]: with open(path, encoding="utf8") as file: for index, line in enumerate(file): line = line.strip() - if len(line) == 0: - break vec1, vec2 = line.split("~") from_vec = parse_vector(vec1) to_vec = parse_vector(vec2) diff --git a/day22/lib/vis.py b/day22/lib/vis.py new file mode 100644 index 0000000..cb75a2a --- /dev/null +++ b/day22/lib/vis.py @@ -0,0 +1,96 @@ +import random +from typing import Any + +import vpython + +from day22.lib.classes import BoxData, Matrix, Vector3 + + +def construct_box(box_data: BoxData, color: vpython.vector) -> vpython.box: + return vpython.box( + pos=box_data.vpos, + length=box_data.length, + height=box_data.height, + width=box_data.width, + color=color, + ) + + +def random_color() -> vpython.vector: + hsv = vpython.vector(random.random(), random.uniform(0.5, 1.0), 1.0) + return vpython.color.hsv_to_rgb(hsv) + + +CAMERA_START = vpython.vector(40.0164, 11.0337, 39.9238) +CAMERA_AXIS = -CAMERA_START + +CAMERA_POS_1 = vpython.vector(111.512, 122.347, 65.0144) +CAMERA_AXIS_1 = vpython.vector(-83.3746, -20.0517, -69.4084) + + +def init_vis(boxes: list[BoxData]) -> None: + """Initialize vis boxes. Only called if we're visualizing""" + for box in boxes: + color = random_color() + vbox = construct_box(box, color) + box.set_vbox(vbox) + + ground = BoxData("ground", Vector3(0, 0, 0), Vector3(10, 10, 0)) + construct_box(ground, random_color()) + # init camera: + vpython.scene.camera.axis = CAMERA_AXIS + vpython.scene.camera.pos = CAMERA_START + vpython.scene.height = 600 + vpython.scene.width = 400 + + +def bind_keys(on_key_down: Any) -> None: + def callback(evt: Any) -> None: + character = evt.key + if character == "shift": + return + print(character) + if character in ["\n", "enter", "return"]: + on_key_down() + + vpython.scene.bind("keydown", callback) + + +def follow_block(y: float, box: BoxData) -> None: + """force camera to follow a block""" + pos = vpython.scene.camera.pos + pos.y = max(y + box.start_pos.z, pos.y) + vpython.scene.camera.pos = pos + + +def animate_part1(boxes: list[BoxData], matrix: Matrix) -> None: + vpython.scene.camera.pos = CAMERA_POS_1 + vpython.scene.camera.axis = CAMERA_AXIS_1 + for box in boxes: + if matrix.can_fly_up(box): + box.select() + vpython.rate(60) + + for box in boxes: + if matrix.can_fly_up(box): + box.unselect() + vpython.rate(60) + + +def animate_part2(boxes: list[BoxData]) -> None: + vpython.scene.camera.pos = CAMERA_POS_1 + vpython.scene.camera.axis = CAMERA_AXIS_1 + reversed_boxes = sorted(boxes, key=lambda box: box.end_pos.z, reverse=True) + for box in reversed_boxes: + to_fall = box.recursive_fall({box}) + to_fall_sorted = sorted(to_fall, key=lambda x: x.z_val_bot) + if len(to_fall_sorted) == 0: + continue + + for faller in to_fall_sorted: + faller.select() + + vpython.rate(20) + + for faller in to_fall: + faller.unselect() diff --git a/day22/tests/__init__.py b/day22/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/day22/tests/test_day22.py b/day22/tests/test_day22.py new file mode 100644 index 0000000..513167f --- /dev/null +++ b/day22/tests/test_day22.py @@ -0,0 +1,16 @@ +from typing import TYPE_CHECKING + +from day22.day22 import INPUT_SMALL, Visualization +from day22.lib.parsers import get_boxes + +if TYPE_CHECKING: + from day22.lib.classes import BoxData + + +def test_visualization() -> None: + boxes: list[BoxData] = get_boxes(INPUT_SMALL) + vis = Visualization(boxes, False) + + vis.start() + assert vis.calculate_part1() == 5 + assert vis.calculate_part2() == 7 diff --git a/day22/tests/test_parsers.py b/day22/tests/test_parsers.py new file mode 100644 index 0000000..7463b60 --- /dev/null +++ b/day22/tests/test_parsers.py @@ -0,0 +1,10 @@ +from day22.day22 import INPUT_SMALL +from day22.lib.classes import BoxData, Vector3 +from day22.lib.parsers import get_boxes + + +def test_parser() -> None: + boxes: list[BoxData] = get_boxes(INPUT_SMALL) + assert len(boxes) == 7 + assert boxes[0].start_pos == Vector3(1, 0, 1) + assert boxes[0].end_pos == Vector3(1, 2, 1) diff --git a/pyproject.toml b/pyproject.toml index cb86ad5..8e90af3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,8 +39,9 @@ exclude_lines = [ "if TYPE_CHECKING:", "raise AssertionError", "# pragma: no cover", + "if self.animate:", ] -omit = ["download_inputs.py", "maker.py"] +omit = ["download_inputs.py", "maker.py", "day22/lib/vis.py"] precision = 2 skip_covered = true fail_under = 0