diff --git a/advent/common/position.py b/advent/common/position.py new file mode 100644 index 0000000..2395456 --- /dev/null +++ b/advent/common/position.py @@ -0,0 +1,71 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Iterator, Literal + + +@dataclass(slots=True, frozen=True, order=True) +class Position: + x: int + y: int + + def __getitem__(self, index: Literal[0, 1]) -> int: + match index: + case 0: return self.x + case 1: return self.y + case _: raise IndexError() + + def __iter__(self) -> Iterator[int]: + yield self.x + yield self.y + + def set_x(self, x: int) -> Position: + return Position(x, self.y) + + def set_y(self, y: int) -> Position: + return Position(self.x, y) + + def __neg__(self) -> Position: + return Position(-self.x, -self.y) + + def __add__(self, other: Position) -> Position: + return Position(self.x + other.x, self.y + other.y) + + def __sub__(self, other: Position) -> Position: + return Position(self.x - other.x, self.y - other.y) + + def __mul__(self, factor: int) -> Position: + return Position(self.x * factor, self.y * factor) + + def taxicab_distance(self, other: Position | None = None) -> int: + if other is None: + return abs(self.x) + abs(self.y) + else: + return abs(self.x - other.x) + abs(self.y - other.y) + + def right(self) -> Position: + return Position(self.x + 1, self.y) + + def up(self) -> Position: + return Position(self.x, self.y - 1) + + def left(self) -> Position: + return Position(self.x - 1, self.y) + + def down(self) -> Position: + return Position(self.x, self.y + 1) + + def unit_neighbors(self) -> Iterator[Position]: + yield self.right() + yield self.up() + yield self.left() + yield self.down() + + def is_within(self, top_left: Position, bottom_right: Position) -> bool: + return top_left.x <= self.x < bottom_right.x and top_left.y <= self.y < bottom_right.y + + +ORIGIN = Position(0, 0) +UNIT_X = Position(1, 0) +UNIT_Y = Position(0, 1) +UNIT_NEG_X = Position(-1, 0) +UNIT_NEG_Y = Position(0, -1) diff --git a/advent/days/day12/solution.py b/advent/days/day12/solution.py index ebc768b..576afd6 100644 --- a/advent/days/day12/solution.py +++ b/advent/days/day12/solution.py @@ -4,6 +4,8 @@ from queue import Queue from typing import Iterator, Self +from advent.common.position import ORIGIN, Position + day_num = 12 @@ -15,34 +17,17 @@ def part2(lines: Iterator[str]) -> int: return Map.create(lines).find_path('a') -@dataclass(frozen=True, slots=True, order=True, eq=True) -class Position: - x: int - y: int - - def neighbors(self, width: int, height: int) -> Iterator[Position]: - if self.x < width - 1: - yield Position(self.x + 1, self.y) - if self.y > 0: - yield Position(self.x, self.y - 1) - if self.x > 0: - yield Position(self.x - 1, self.y) - if self.y < height - 1: - yield Position(self.x, self.y + 1) - - @dataclass(slots=True, frozen=True) class Map: map: list[str] - width: int - height: int + bottom_right: Position @classmethod def create(cls, input: Iterator[str]) -> Self: map = list(input) width = len(map[0]) height = len(map) - return Map(map, width, height) + return Map(map, Position(width, height)) def can_climb(self, *, from_pos: Position, to_pos: Position) -> bool: """ Checks if one gan walk from the elevation at from_pos to the elevation at to_pos """ @@ -88,6 +73,7 @@ class Map: def next_step(self, current_pos: Position) -> Iterator[Position]: """ yields all neighbors, that could have been the previous step to this one""" - for neighbor in current_pos.neighbors(self.width, self.height): - if self.can_climb(from_pos=neighbor, to_pos=current_pos): + for neighbor in current_pos.unit_neighbors(): + if (neighbor.is_within(ORIGIN, self.bottom_right) + and self.can_climb(from_pos=neighbor, to_pos=current_pos)): yield neighbor diff --git a/advent/days/day15/solution.py b/advent/days/day15/solution.py index 3e2fb8d..06b90cd 100644 --- a/advent/days/day15/solution.py +++ b/advent/days/day15/solution.py @@ -3,6 +3,8 @@ from dataclasses import dataclass from typing import Iterator, Self +from advent.common.position import Position + day_num = 15 @@ -18,7 +20,6 @@ def part2(lines: Iterator[str]) -> int: return sensor_map.get_possible_frequency(int(max_range)) -Position = tuple[int, int] ColRange = tuple[int, int] @@ -30,21 +31,17 @@ class Sensor: @classmethod def parse(cls, line: str) -> tuple[Self, Position]: parts = line.split('=') - sensor = int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip()) - beacon = int(parts[3].split(',')[0].strip()), int(parts[4].strip()) - return cls(sensor, Sensor.manhatten(sensor, beacon)), beacon - - @classmethod - def manhatten(cls, first: Position, other: Position) -> int: - return abs(first[0] - other[0]) + abs(first[1] - other[1]) + sensor = Position(int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip())) + beacon = Position(int(parts[3].split(',')[0].strip()), int(parts[4].strip())) + return cls(sensor, sensor.taxicab_distance(beacon)), beacon def col_range_at_row(self, row: int) -> ColRange | None: - col_distance = self.distance - abs(self.sensor[1] - row) + col_distance = self.distance - abs(self.sensor.y - row) if col_distance < 0: return None - from_x = self.sensor[0] - col_distance - to_x = self.sensor[0] + col_distance + from_x = self.sensor.x - col_distance + to_x = self.sensor.x + col_distance return from_x, to_x @@ -93,7 +90,7 @@ class SensorMap: col_ranges = self.get_impossible(row) seen = sum(rng[1] - rng[0] + 1 for rng in SensorMap.merged_col_ranges(col_ranges)) - beacons = len({beacon[0] for beacon in self.beacons if beacon[1] == row}) + beacons = len({beacon.x for beacon in self.beacons if beacon.y == row}) return seen - beacons @@ -105,7 +102,7 @@ class SensorMap: for one0, one1 in col_ranges: if curr1 < one1: if curr1 < one0: - return curr1 + 1, row + return Position(curr1 + 1, row) if one1 > max_range: break curr1 = one1 diff --git a/advent/days/day15/test_solution.py b/advent/days/day15/test_solution.py index 2d5eb13..449782f 100644 --- a/advent/days/day15/test_solution.py +++ b/advent/days/day15/test_solution.py @@ -1,4 +1,5 @@ from advent.common import input +from advent.common.position import Position from .solution import Sensor, SensorMap, day_num, part1, part2 @@ -19,7 +20,7 @@ def test_part2(): def test_parse(): input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15" - expected = Sensor((2, 18), 7), (-2, 15) + expected = Sensor(Position(2, 18), 7), Position(-2, 15) result = Sensor.parse(input) assert result == expected diff --git a/advent/days/day17/solution.py b/advent/days/day17/solution.py index 017d590..1bd1941 100644 --- a/advent/days/day17/solution.py +++ b/advent/days/day17/solution.py @@ -4,6 +4,8 @@ from itertools import cycle from typing import Iterator, Self +from advent.common.position import Position + day_num = 17 @@ -25,21 +27,6 @@ patterns = [["####"], ] -@dataclass(slots=True, frozen=True) -class Position: - x: int - y: int - - def left(self) -> Position: - return Position(self.x - 1, self.y) - - def right(self) -> Position: - return Position(self.x + 1, self.y) - - def down(self) -> Position: - return Position(self.x, self.y - 1) - - @dataclass(slots=True, frozen=True) class Pattern: lines: list[str] @@ -52,11 +39,11 @@ class Pattern: def width(self) -> int: return len(self.lines[0]) - def stones(self, position: Position) -> Iterator[Position]: + def stones(self, offset: Position) -> Iterator[Position]: for y, line in enumerate(self.lines): for x, block in enumerate(line): if block == "#": - yield Position(position.x + x, position.y + y) + yield Position(offset.x + x, offset.y + y) @dataclass(slots=True) @@ -93,17 +80,21 @@ class Cave: position = Position(2, len(self.cave) + 3) while True: - push = next(self.gas_pushes) - if push == '<': - if position.x > 0 and self.check_free(rock, position.left()): - position = position.left() - else: - if (position.x + rock.width < self.width - and self.check_free(rock, position.right())): - position = position.right() + match next(self.gas_pushes): + case '<': + next_pos = position.left() + if next_pos.x >= 0 and self.check_free(rock, next_pos): + position = next_pos + case '>': + next_pos = position.right() + if (next_pos.x + rock.width <= self.width + and self.check_free(rock, next_pos)): + position = next_pos + case c: raise Exception(f"Illegal char: {c}") - if position.y > 0 and self.check_free(rock, position.down()): - position = position.down() + next_pos = position.up() + if next_pos.y >= 0 and self.check_free(rock, next_pos): + position = next_pos else: self.fix_rock(rock, position) return rock, position diff --git a/advent/days/day18/solution.py b/advent/days/day18/solution.py index 19bc6ce..32896a1 100644 --- a/advent/days/day18/solution.py +++ b/advent/days/day18/solution.py @@ -8,17 +8,17 @@ day_num = 18 def part1(lines: Iterator[str]) -> int: - shower = Shower.create(Position.parse_all(lines)) + shower = Shower.create(Position3D.parse_all(lines)) return shower.faces def part2(lines: Iterator[str]) -> int: - shower = Shower.create(Position.parse_all(lines)) + shower = Shower.create(Position3D.parse_all(lines)) return shower.faces - shower.count_trapped_droplets() @dataclass(slots=True, frozen=True) -class Position: +class Position3D: x: int y: int z: int @@ -26,32 +26,32 @@ class Position: @classmethod def parse(cls, line: str) -> Self: x, y, z = line.split(",") - return Position(int(x), int(y), int(z)) + return Position3D(int(x), int(y), int(z)) @classmethod def parse_all(cls, lines: Iterable[str]) -> Iterator[Self]: return (cls.parse(line) for line in lines) - def neighbors(self) -> Iterator[Position]: - yield Position(self.x + 1, self.y, self.z) - yield Position(self.x - 1, self.y, self.z) - yield Position(self.x, self.y + 1, self.z) - yield Position(self.x, self.y - 1, self.z) - yield Position(self.x, self.y, self.z + 1) - yield Position(self.x, self.y, self.z - 1) + def neighbors(self) -> Iterator[Position3D]: + yield Position3D(self.x + 1, self.y, self.z) + yield Position3D(self.x - 1, self.y, self.z) + yield Position3D(self.x, self.y + 1, self.z) + yield Position3D(self.x, self.y - 1, self.z) + yield Position3D(self.x, self.y, self.z + 1) + yield Position3D(self.x, self.y, self.z - 1) - def min_max(self, mm: tuple[Position, Position] | None) -> tuple[Position, Position]: + def min_max(self, mm: tuple[Position3D, Position3D] | None) -> tuple[Position3D, Position3D]: if mm is None: return self, self - return (Position(min(mm[0].x, self.x), - min(mm[0].y, self.y), - min(mm[0].z, self.z)), - Position(max(mm[1].x, self.x), - max(mm[1].y, self.y), - max(mm[1].z, self.z))) + return (Position3D(min(mm[0].x, self.x), + min(mm[0].y, self.y), + min(mm[0].z, self.z)), + Position3D(max(mm[1].x, self.x), + max(mm[1].y, self.y), + max(mm[1].z, self.z))) - def is_between(self, min: Position, max: Position) -> bool: + def is_between(self, min: Position3D, max: Position3D) -> bool: return (min.x <= self.x <= max.x and min.y <= self.y <= max.y and min.z <= self.z <= max.z) @@ -65,12 +65,12 @@ class DropletType(Enum): @dataclass(slots=True, frozen=True) class Shower: - droplets: dict[Position, tuple[DropletType, int]] + droplets: dict[Position3D, tuple[DropletType, int]] faces: int @classmethod - def create(cls, positions: Iterable[Position]) -> Self: - droplets: dict[Position, tuple[DropletType, int]] = {} + def create(cls, positions: Iterable[Position3D]) -> Self: + droplets: dict[Position3D, tuple[DropletType, int]] = {} faces = 0 for position in positions: candidate = droplets.get(position) @@ -95,14 +95,14 @@ class Shower: def count_trapped_droplets(self) -> int: droplets = self.droplets.copy() - minmax: tuple[Position, Position] | None = None + minmax: tuple[Position3D, Position3D] | None = None for position in droplets.keys(): minmax = position.min_max(minmax) if minmax is None: raise Exception("I got no data to work with") min_values, max_values = minmax - todo: list[Position] = [min_values] + todo: list[Position3D] = [min_values] while todo: current = todo[0] todo = todo[1:] diff --git a/advent/days/day18/test_solution.py b/advent/days/day18/test_solution.py index c4ca091..c31bc0f 100644 --- a/advent/days/day18/test_solution.py +++ b/advent/days/day18/test_solution.py @@ -1,6 +1,6 @@ from advent.common import input -from .solution import Position, Shower, day_num, part1, part2 +from .solution import Position3D, Shower, day_num, part1, part2 def test_part1(): @@ -20,19 +20,19 @@ def test_part2(): def test_simple_count(): lines = ["1,1,1", "1,1,2"] expected = 10 - result = Shower.create(Position.parse_all(lines)) + result = Shower.create(Position3D.parse_all(lines)) assert result.faces == expected def test_example_faces(): lines = input.read_lines(day_num, 'example01.txt') expected = 64 - result = Shower.create(Position.parse_all(lines)) + result = Shower.create(Position3D.parse_all(lines)) assert result.faces == expected def test_example_trapped(): lines = input.read_lines(day_num, 'example01.txt') expected = 6 - result = Shower.create(Position.parse_all(lines)) + result = Shower.create(Position3D.parse_all(lines)) assert result.count_trapped_droplets() == expected diff --git a/advent/days/day22/solution.py b/advent/days/day22/solution.py index 3121e04..c535ccc 100644 --- a/advent/days/day22/solution.py +++ b/advent/days/day22/solution.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass, field +from advent.common.position import UNIT_NEG_X, UNIT_NEG_Y, UNIT_X, UNIT_Y, Position from enum import Enum from typing import Iterator, Self @@ -44,29 +45,17 @@ class Facing(Enum): case Facing.Left: return Facing.Up case Facing.Down: return Facing.Left - def as_position(self) -> Position2D: + def as_position(self) -> Position: match self: - case Facing.Right: return Position2D(1, 0) - case Facing.Up: return Position2D(0, -1) - case Facing.Left: return Position2D(-1, 0) - case Facing.Down: return Position2D(0, 1) - - -@dataclass(slots=True, frozen=True) -class Position2D: - x: int - y: int - - def __add__(self, other: Position2D) -> Position2D: - return Position2D(self.x + other.x, self.y + other.y) - - def __mul__(self, factor: int) -> Position2D: - return Position2D(self.x * factor, self.y * factor) + case Facing.Right: return UNIT_X + case Facing.Up: return UNIT_NEG_Y + case Facing.Left: return UNIT_NEG_X + case Facing.Down: return UNIT_Y @dataclass(slots=True, frozen=True) class Player: - position: Position2D + position: Position facing: Facing @property @@ -113,35 +102,35 @@ class PasswordJungle(ABC): return val, start case _: raise Exception("Illegal char") - def start(self) -> tuple[Position2D, Facing]: + def start(self) -> tuple[Position, Facing]: return self.start_column(0), Facing.Right - def start_column(self, row: int) -> Position2D: + def start_column(self, row: int) -> Position: for col, char in enumerate(self.map[row]): if char != ' ': - return Position2D(col, row) + return Position(col, row) raise Exception("Empty row found") - def end_column(self, row: int) -> Position2D: + def end_column(self, row: int) -> Position: for col in range(len(self.map[row]) - 1, -1, -1): if self.map[row][col] != ' ': - return Position2D(col, row) + return Position(col, row) raise Exception("Empty row found") - def start_row(self, col: int) -> Position2D: + def start_row(self, col: int) -> Position: for row, line in enumerate(self.map): if col < len(line) and line[col] != ' ': - return Position2D(col, row) + return Position(col, row) raise Exception("Empty row found") - def end_row(self, col: int) -> Position2D: + def end_row(self, col: int) -> Position: for row in range(len(self.map) - 1, -1, -1): line = self.map[row] if col < len(line) and line[col] != ' ': - return Position2D(col, row) + return Position(col, row) raise Exception("Empty row found") - def check_tile(self, pos: Position2D) -> str: + def check_tile(self, pos: Position) -> str: if pos.y not in range(0, len(self.map)) or pos.x not in range(0, len(self.map[pos.y])): return ' ' return self.map[pos.y][pos.x] @@ -216,9 +205,9 @@ class CubePosition: @dataclass(slots=True) class PasswordCubeJungle(PasswordJungle): cube_width: int = field(default=50, init=False) - sides: dict[Vector, tuple[Position2D, Vector]] = field(default_factory=dict, init=False) + sides: dict[Vector, tuple[Position, Vector]] = field(default_factory=dict, init=False) - def _find_neighbors(self, map_position: Position2D, facing: Facing, + def _find_neighbors(self, map_position: Position, facing: Facing, cube_position: CubePosition): match facing: case Facing.Right: facing_right = cube_position.facing @@ -300,7 +289,7 @@ class PasswordCubeJungle(PasswordJungle): y = position.y x = position.x + self.cube_width - 1 - delta - result = Player(Position2D(x, y), facing) + result = Player(Position(x, y), facing) return result diff --git a/advent/days/day22/test_solution.py b/advent/days/day22/test_solution.py index 3907ec8..7c61616 100644 --- a/advent/days/day22/test_solution.py +++ b/advent/days/day22/test_solution.py @@ -6,7 +6,7 @@ from .solution import ( PasswordCubeJungle, PasswordSimpleJungle, Player, - Position2D, + Position, Turn, Vector, day_num, @@ -56,39 +56,39 @@ def test_positions(): jungle = PasswordSimpleJungle.create(lines) result = jungle.start_column(0) - assert result == Position2D(8, 0) + assert result == Position(8, 0) result = jungle.start_row(0) - assert result == Position2D(0, 4) + assert result == Position(0, 4) result = jungle.end_column(4) - assert result == Position2D(11, 4) + assert result == Position(11, 4) result = jungle.end_row(4) - assert result == Position2D(4, 7) + assert result == Position(4, 7) result = jungle.start_row(8) - assert result == Position2D(8, 0) + assert result == Position(8, 0) def test_step(): lines = input.read_lines(day_num, 'example01.txt') jungle = PasswordSimpleJungle.create(lines) - person = jungle.step(Player(Position2D(8, 0), Facing.Right), 10) - assert person == Player(Position2D(10, 0), Facing.Right) + person = jungle.step(Player(Position(8, 0), Facing.Right), 10) + assert person == Player(Position(10, 0), Facing.Right) - person = jungle.step(Player(Position2D(10, 0), Facing.Down), 5) - assert person == Player(Position2D(10, 5), Facing.Down) + person = jungle.step(Player(Position(10, 0), Facing.Down), 5) + assert person == Player(Position(10, 5), Facing.Down) - person = jungle.step(Player(Position2D(10, 5), Facing.Right), 5) - assert person == Player(Position2D(3, 5), Facing.Right) + person = jungle.step(Player(Position(10, 5), Facing.Right), 5) + assert person == Player(Position(3, 5), Facing.Right) def test_walk(): lines = input.read_lines(day_num, 'example01.txt') jungle = PasswordSimpleJungle.create(lines) person = jungle.walk() - assert person == Player(Position2D(7, 5), Facing.Right) + assert person == Player(Position(7, 5), Facing.Right) assert person.value == 6032 @@ -102,19 +102,19 @@ def test_cube_info(): lines = input.read_lines(day_num, 'example01.txt') jungle = PasswordCubeJungle.create(lines) - person = Player(Position2D(14, 8), Facing.Up) + person = Player(Position(14, 8), Facing.Up) result = jungle.get_cube_position(person) assert result == (CubePosition(Vector(0, 1, 0), Vector(0, 0, -1)), 2) - person = Player(Position2D(11, 5), Facing.Right) + person = Player(Position(11, 5), Facing.Right) result = jungle.get_cube_position(person) assert result == (CubePosition(Vector(0, 0, -1), Vector(0, 1, 0)), 1) - person = Player(Position2D(1, 7), Facing.Down) + person = Player(Position(1, 7), Facing.Down) result = jungle.get_cube_position(person) assert result == (CubePosition(Vector(0, 0, 1), Vector(-1, 0, 0)), 2) - person = Player(Position2D(10, 11), Facing.Down) + person = Player(Position(10, 11), Facing.Down) result = jungle.get_cube_position(person) assert result == (CubePosition(Vector(-1, 0, 0), Vector(0, 0, 1)), 1) @@ -123,18 +123,18 @@ def test_cube_wrap(): lines = input.read_lines(day_num, 'example01.txt') jungle = PasswordCubeJungle.create(lines) - person = Player(Position2D(14, 8), Facing.Up) + person = Player(Position(14, 8), Facing.Up) result = jungle.wrap(person) - assert result == Player(Position2D(11, 5), Facing.Left) + assert result == Player(Position(11, 5), Facing.Left) - person = Player(Position2D(11, 5), Facing.Right) + person = Player(Position(11, 5), Facing.Right) result = jungle.wrap(person) - assert result == Player(Position2D(14, 8), Facing.Down) + assert result == Player(Position(14, 8), Facing.Down) - person = Player(Position2D(1, 7), Facing.Down) + person = Player(Position(1, 7), Facing.Down) result = jungle.wrap(person) - assert result == Player(Position2D(10, 11), Facing.Up) + assert result == Player(Position(10, 11), Facing.Up) - person = Player(Position2D(10, 11), Facing.Down) + person = Player(Position(10, 11), Facing.Down) result = jungle.wrap(person) - assert result == Player(Position2D(1, 7), Facing.Up) + assert result == Player(Position(1, 7), Facing.Up) diff --git a/advent/days/day23/solution.py b/advent/days/day23/solution.py index 9f25831..fcf96a7 100644 --- a/advent/days/day23/solution.py +++ b/advent/days/day23/solution.py @@ -5,6 +5,8 @@ from itertools import count, cycle from typing import Iterator +from advent.common.position import Position + day_num = 23 @@ -37,59 +39,56 @@ class Direction(Enum): @dataclass(slots=True, frozen=True) -class Position: - x: int - y: int - - def get_adjacent(self, direction: Direction) -> Iterator[Position]: +class ElfPosition(Position): + def get_adjacent(self, direction: Direction) -> Iterator[ElfPosition]: match direction: case Direction.North: - yield Position(self.x - 1, self.y - 1) - yield Position(self.x, self.y - 1) - yield Position(self.x + 1, self.y - 1) + yield ElfPosition(self.x - 1, self.y - 1) + yield ElfPosition(self.x, self.y - 1) + yield ElfPosition(self.x + 1, self.y - 1) case Direction.South: - yield Position(self.x - 1, self.y + 1) - yield Position(self.x, self.y + 1) - yield Position(self.x + 1, self.y + 1) + yield ElfPosition(self.x - 1, self.y + 1) + yield ElfPosition(self.x, self.y + 1) + yield ElfPosition(self.x + 1, self.y + 1) case Direction.West: - yield Position(self.x - 1, self.y - 1) - yield Position(self.x - 1, self.y) - yield Position(self.x - 1, self.y + 1) + yield ElfPosition(self.x - 1, self.y - 1) + yield ElfPosition(self.x - 1, self.y) + yield ElfPosition(self.x - 1, self.y + 1) case Direction.East: - yield Position(self.x + 1, self.y - 1) - yield Position(self.x + 1, self.y) - yield Position(self.x + 1, self.y + 1) + yield ElfPosition(self.x + 1, self.y - 1) + yield ElfPosition(self.x + 1, self.y) + yield ElfPosition(self.x + 1, self.y + 1) - def get_all_adjacent(self) -> Iterator[Position]: + def get_all_adjacent(self) -> Iterator[ElfPosition]: for y in range(-1, 2): for x in range(-1, 2): if x != 0 or y != 0: - yield Position(self.x + x, self.y + y) + yield ElfPosition(self.x + x, self.y + y) - def walk(self, direction: Direction) -> Position: + def walk(self, direction: Direction) -> ElfPosition: match direction: - case Direction.North: return Position(self.x, self.y - 1) - case Direction.South: return Position(self.x, self.y + 1) - case Direction.West: return Position(self.x - 1, self.y) - case Direction.East: return Position(self.x + 1, self.y) + case Direction.North: return ElfPosition(self.x, self.y - 1) + case Direction.South: return ElfPosition(self.x, self.y + 1) + case Direction.West: return ElfPosition(self.x - 1, self.y) + case Direction.East: return ElfPosition(self.x + 1, self.y) - def min(self, other: Position) -> Position: - return Position(min(self.x, other.x), min(self.y, other.y)) + def min(self, other: ElfPosition) -> ElfPosition: + return ElfPosition(min(self.x, other.x), min(self.y, other.y)) - def max(self, other: Position) -> Position: - return Position(max(self.x, other.x), max(self.y, other.y)) + def max(self, other: ElfPosition) -> ElfPosition: + return ElfPosition(max(self.x, other.x), max(self.y, other.y)) @dataclass(slots=True) class Ground: - map: set[Position] + map: set[ElfPosition] def __str__(self) -> str: min_pos, max_pos = self.extent() result = "" for y in range(min_pos.y, max_pos.y + 1): for x in range(min_pos.x, max_pos.x + 1): - if Position(x, y) in self.map: + if ElfPosition(x, y) in self.map: result += '#' else: result += '.' @@ -102,14 +101,14 @@ class Ground: @classmethod def parse(cls, lines: Iterator[str]) -> Ground: - map: set[Position] = set() + map: set[ElfPosition] = set() for y, line in enumerate(lines): for x, tile in enumerate(line): if tile == '#': - map.add(Position(x, y)) + map.add(ElfPosition(x, y)) return Ground(map) - def extent(self) -> tuple[Position, Position]: + def extent(self) -> tuple[ElfPosition, ElfPosition]: min_pos = next(iter(self.map)) max_pos = min_pos for elf in self.map: @@ -126,8 +125,8 @@ class Ground: for n in it: start = next(start_dispenser) - target_map: dict[Position, Position] = {} - target_count: dict[Position, int] = {} + target_map: dict[ElfPosition, ElfPosition] = {} + target_count: dict[ElfPosition, int] = {} for elf in self.map: if all(pos not in self.map for pos in elf.get_all_adjacent()): continue diff --git a/advent/days/day24/solution.py b/advent/days/day24/solution.py index 1fdf946..cf29886 100644 --- a/advent/days/day24/solution.py +++ b/advent/days/day24/solution.py @@ -5,6 +5,8 @@ from queue import PriorityQueue from typing import Iterator +from advent.common.position import UNIT_NEG_X, UNIT_NEG_Y, UNIT_X, UNIT_Y, Position + day_num = 24 @@ -32,7 +34,6 @@ def lcm(num1: int, num2: int) -> int: return num1 * num2 // gcd(num1, num2) -Position = tuple[int, int] BlizList = list[Position] BlizTuple = tuple[BlizList, BlizList, BlizList, BlizList] @@ -54,10 +55,10 @@ class Direction(IntEnum): def position(self) -> Position: match self: - case Direction.East: return 1, 0 - case Direction.North: return 0, -1 - case Direction.West: return -1, 0 - case Direction.South: return 0, 1 + case Direction.East: return UNIT_X + case Direction.North: return UNIT_NEG_Y + case Direction.West: return UNIT_NEG_X + case Direction.South: return UNIT_Y @property def char(self) -> str: @@ -77,10 +78,10 @@ class Weather: def print(self, time: int) -> list[str]: current = self.blizzards[self.normal_time(time)] lines: list[str] = [] - for row in range(self.extent[1]): + for row in range(self.extent.y): line = "" - for col in range(self.extent[0]): - if (char := current.get((col, row))) is not None: + for col in range(self.extent.x): + if (char := current.get(Position(col, row))) is not None: line += char else: line += '.' @@ -95,7 +96,7 @@ class Weather: @classmethod def predict_weather(cls, blizzards: BlizTuple, extent: Position) -> Weather: - repeat = lcm(extent[0], extent[1]) + repeat = lcm(extent.x, extent.y) weather: list[dict[Position, str]] = [Weather.create_dict(blizzards)] for _ in range(repeat - 1): blizzards = Weather.progress_blizzards(blizzards, extent) @@ -108,9 +109,9 @@ class Weather: add = Direction.East.position() result: BlizList = [] for pos in blizzards: - next_pos = pos[0] + add[0], pos[1] + add[1] - if next_pos[0] >= extent[0]: - next_pos = 0, next_pos[1] + next_pos = pos + add + if next_pos.x >= extent.x: + next_pos = next_pos.set_x(0) result.append(next_pos) return result @@ -120,9 +121,9 @@ class Weather: add = Direction.West.position() result: BlizList = [] for pos in blizzards: - next_pos = pos[0] + add[0], pos[1] + add[1] - if next_pos[0] < 0: - next_pos = extent[0] - 1, next_pos[1] + next_pos = pos + add + if next_pos.x < 0: + next_pos = next_pos.set_x(extent.x - 1) result.append(next_pos) return result @@ -132,9 +133,9 @@ class Weather: add = Direction.South.position() result: BlizList = [] for pos in blizzards: - next_pos = pos[0] + add[0], pos[1] + add[1] - if next_pos[1] >= extent[1]: - next_pos = next_pos[0], 0 + next_pos = pos + add + if next_pos.y >= extent.y: + next_pos = next_pos.set_y(0) result.append(next_pos) return result @@ -144,9 +145,9 @@ class Weather: add = Direction.North.position() result: BlizList = [] for pos in blizzards: - next_pos = pos[0] + add[0], pos[1] + add[1] - if next_pos[1] < 0: - next_pos = next_pos[0], extent[1] - 1 + next_pos = pos + add + if next_pos.y < 0: + next_pos = next_pos.set_y(extent.y - 1) result.append(next_pos) return result @@ -206,7 +207,7 @@ class Valley: case '#': return blizzards case '>' | '^' | '<' | 'v': - blizzards = Valley.append(blizzards, Direction.create(char), (col, row)) + blizzards = Valley.append(blizzards, Direction.create(char), Position(col, row)) case '.': pass case _: @@ -223,9 +224,9 @@ class Valley: for row, line in enumerate(lines): if line.startswith("##"): end_col = Valley._get_wallbreak(line) - extent = width, row + extent = Position(width, row) return Valley(Weather.predict_weather(blizzards, extent), extent, - (start_col, -1), (end_col, row)) + Position(start_col, -1), Position(end_col, row)) else: blizzards = Valley.parse_line(blizzards, line, row) assert False, "Unreachable" @@ -234,8 +235,8 @@ class Valley: return '\n'.join(self.print(0)) def print(self, time: int) -> list[str]: - first = '#' + ('#' * self.start[0]) + '.' + ('#' * (self.extent[0] - self.start[0])) - last = '#' + ('#' * self.exit[0]) + '.' + ('#' * (self.extent[0] - self.exit[0])) + first = '#' + ('#' * self.start.x) + '.' + ('#' * (self.extent.x - self.start.x)) + last = '#' + ('#' * self.exit.x) + '.' + ('#' * (self.extent.x - self.exit.x)) lines = self.weather.print(time) lines = [first] + ['#' + line + '#' for line in lines] + [last] return lines @@ -284,9 +285,9 @@ class Step: def print(self) -> list[str]: lines = self.valley.print(self.time) - line = lines[self.position[1] + 1] - lines[self.position[1] + 1] = line[:self.position[0] + 1] + \ - 'E' + line[self.position[0] + 2:] + line = lines[self.position.y + 1] + lines[self.position.y + 1] = line[:self.position.x + 1] + \ + 'E' + line[self.position.x + 2:] return lines @@ -326,11 +327,11 @@ class Step: for dir in Direction: add = dir.position() - next_position = self.position[0] + add[0], self.position[1] + add[1] + next_position = self.position + add if next_position == self.target: yield self.reach_target() - elif (0 <= next_position[0] < self.valley.extent[0] - and 0 <= next_position[1] < self.valley.extent[1]): + elif (0 <= next_position.x < self.valley.extent.x + and 0 <= next_position.y < self.valley.extent.y): if next_position not in impassable: yield self.move(next_position) diff --git a/advent/days/day24/test_solution.py b/advent/days/day24/test_solution.py index e796242..086a7f1 100644 --- a/advent/days/day24/test_solution.py +++ b/advent/days/day24/test_solution.py @@ -1,4 +1,5 @@ from advent.common import input +from advent.common.position import Position from .solution import BlizTuple, Valley, day_num, part1, part2 @@ -20,9 +21,9 @@ def test_part2(): def test_parse_line(): input = "#>>.<^<#" expected: BlizTuple = ( - [(0, 0), (1, 0)], - [(4, 0)], - [(3, 0), (5, 0)], + [Position(0, 0), Position(1, 0)], + [Position(4, 0)], + [Position(3, 0), Position(5, 0)], []) result = Valley.parse_line(([], [], [], []), input, 0) assert result == expected