refactored out Position

This commit is contained in:
Ruediger Ludwig 2023-01-08 14:02:48 +01:00
parent 923e967056
commit b83bb6b37a
12 changed files with 252 additions and 216 deletions

71
advent/common/position.py Normal file
View file

@ -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)

View file

@ -4,6 +4,8 @@ from queue import Queue
from typing import Iterator, Self from typing import Iterator, Self
from advent.common.position import ORIGIN, Position
day_num = 12 day_num = 12
@ -15,34 +17,17 @@ def part2(lines: Iterator[str]) -> int:
return Map.create(lines).find_path('a') 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) @dataclass(slots=True, frozen=True)
class Map: class Map:
map: list[str] map: list[str]
width: int bottom_right: Position
height: int
@classmethod @classmethod
def create(cls, input: Iterator[str]) -> Self: def create(cls, input: Iterator[str]) -> Self:
map = list(input) map = list(input)
width = len(map[0]) width = len(map[0])
height = len(map) 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: 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 """ """ 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]: def next_step(self, current_pos: Position) -> Iterator[Position]:
""" yields all neighbors, that could have been the previous step to this one""" """ yields all neighbors, that could have been the previous step to this one"""
for neighbor in current_pos.neighbors(self.width, self.height): for neighbor in current_pos.unit_neighbors():
if self.can_climb(from_pos=neighbor, to_pos=current_pos): if (neighbor.is_within(ORIGIN, self.bottom_right)
and self.can_climb(from_pos=neighbor, to_pos=current_pos)):
yield neighbor yield neighbor

View file

@ -3,6 +3,8 @@ from dataclasses import dataclass
from typing import Iterator, Self from typing import Iterator, Self
from advent.common.position import Position
day_num = 15 day_num = 15
@ -18,7 +20,6 @@ def part2(lines: Iterator[str]) -> int:
return sensor_map.get_possible_frequency(int(max_range)) return sensor_map.get_possible_frequency(int(max_range))
Position = tuple[int, int]
ColRange = tuple[int, int] ColRange = tuple[int, int]
@ -30,21 +31,17 @@ class Sensor:
@classmethod @classmethod
def parse(cls, line: str) -> tuple[Self, Position]: def parse(cls, line: str) -> tuple[Self, Position]:
parts = line.split('=') parts = line.split('=')
sensor = int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip()) sensor = Position(int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip()))
beacon = int(parts[3].split(',')[0].strip()), int(parts[4].strip()) beacon = Position(int(parts[3].split(',')[0].strip()), int(parts[4].strip()))
return cls(sensor, Sensor.manhatten(sensor, beacon)), beacon return cls(sensor, sensor.taxicab_distance(beacon)), beacon
@classmethod
def manhatten(cls, first: Position, other: Position) -> int:
return abs(first[0] - other[0]) + abs(first[1] - other[1])
def col_range_at_row(self, row: int) -> ColRange | None: 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: if col_distance < 0:
return None return None
from_x = self.sensor[0] - col_distance from_x = self.sensor.x - col_distance
to_x = self.sensor[0] + col_distance to_x = self.sensor.x + col_distance
return from_x, to_x return from_x, to_x
@ -93,7 +90,7 @@ class SensorMap:
col_ranges = self.get_impossible(row) col_ranges = self.get_impossible(row)
seen = sum(rng[1] - rng[0] + 1 for rng in SensorMap.merged_col_ranges(col_ranges)) 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 return seen - beacons
@ -105,7 +102,7 @@ class SensorMap:
for one0, one1 in col_ranges: for one0, one1 in col_ranges:
if curr1 < one1: if curr1 < one1:
if curr1 < one0: if curr1 < one0:
return curr1 + 1, row return Position(curr1 + 1, row)
if one1 > max_range: if one1 > max_range:
break break
curr1 = one1 curr1 = one1

View file

@ -1,4 +1,5 @@
from advent.common import input from advent.common import input
from advent.common.position import Position
from .solution import Sensor, SensorMap, day_num, part1, part2 from .solution import Sensor, SensorMap, day_num, part1, part2
@ -19,7 +20,7 @@ def test_part2():
def test_parse(): def test_parse():
input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15" 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) result = Sensor.parse(input)
assert result == expected assert result == expected

View file

@ -4,6 +4,8 @@ from itertools import cycle
from typing import Iterator, Self from typing import Iterator, Self
from advent.common.position import Position
day_num = 17 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) @dataclass(slots=True, frozen=True)
class Pattern: class Pattern:
lines: list[str] lines: list[str]
@ -52,11 +39,11 @@ class Pattern:
def width(self) -> int: def width(self) -> int:
return len(self.lines[0]) 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 y, line in enumerate(self.lines):
for x, block in enumerate(line): for x, block in enumerate(line):
if block == "#": if block == "#":
yield Position(position.x + x, position.y + y) yield Position(offset.x + x, offset.y + y)
@dataclass(slots=True) @dataclass(slots=True)
@ -93,17 +80,21 @@ class Cave:
position = Position(2, len(self.cave) + 3) position = Position(2, len(self.cave) + 3)
while True: while True:
push = next(self.gas_pushes) match next(self.gas_pushes):
if push == '<': case '<':
if position.x > 0 and self.check_free(rock, position.left()): next_pos = position.left()
position = position.left() if next_pos.x >= 0 and self.check_free(rock, next_pos):
else: position = next_pos
if (position.x + rock.width < self.width case '>':
and self.check_free(rock, position.right())): next_pos = position.right()
position = 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()): next_pos = position.up()
position = position.down() if next_pos.y >= 0 and self.check_free(rock, next_pos):
position = next_pos
else: else:
self.fix_rock(rock, position) self.fix_rock(rock, position)
return rock, position return rock, position

View file

@ -8,17 +8,17 @@ day_num = 18
def part1(lines: Iterator[str]) -> int: def part1(lines: Iterator[str]) -> int:
shower = Shower.create(Position.parse_all(lines)) shower = Shower.create(Position3D.parse_all(lines))
return shower.faces return shower.faces
def part2(lines: Iterator[str]) -> int: 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() return shower.faces - shower.count_trapped_droplets()
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Position: class Position3D:
x: int x: int
y: int y: int
z: int z: int
@ -26,32 +26,32 @@ class Position:
@classmethod @classmethod
def parse(cls, line: str) -> Self: def parse(cls, line: str) -> Self:
x, y, z = line.split(",") x, y, z = line.split(",")
return Position(int(x), int(y), int(z)) return Position3D(int(x), int(y), int(z))
@classmethod @classmethod
def parse_all(cls, lines: Iterable[str]) -> Iterator[Self]: def parse_all(cls, lines: Iterable[str]) -> Iterator[Self]:
return (cls.parse(line) for line in lines) return (cls.parse(line) for line in lines)
def neighbors(self) -> Iterator[Position]: def neighbors(self) -> Iterator[Position3D]:
yield Position(self.x + 1, self.y, self.z) yield Position3D(self.x + 1, self.y, self.z)
yield Position(self.x - 1, self.y, self.z) yield Position3D(self.x - 1, self.y, self.z)
yield Position(self.x, self.y + 1, self.z) yield Position3D(self.x, self.y + 1, self.z)
yield Position(self.x, self.y - 1, self.z) yield Position3D(self.x, self.y - 1, self.z)
yield Position(self.x, self.y, self.z + 1) yield Position3D(self.x, self.y, self.z + 1)
yield Position(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: if mm is None:
return self, self return self, self
return (Position(min(mm[0].x, self.x), return (Position3D(min(mm[0].x, self.x),
min(mm[0].y, self.y), min(mm[0].y, self.y),
min(mm[0].z, self.z)), min(mm[0].z, self.z)),
Position(max(mm[1].x, self.x), Position3D(max(mm[1].x, self.x),
max(mm[1].y, self.y), max(mm[1].y, self.y),
max(mm[1].z, self.z))) 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 return (min.x <= self.x <= max.x
and min.y <= self.y <= max.y and min.y <= self.y <= max.y
and min.z <= self.z <= max.z) and min.z <= self.z <= max.z)
@ -65,12 +65,12 @@ class DropletType(Enum):
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Shower: class Shower:
droplets: dict[Position, tuple[DropletType, int]] droplets: dict[Position3D, tuple[DropletType, int]]
faces: int faces: int
@classmethod @classmethod
def create(cls, positions: Iterable[Position]) -> Self: def create(cls, positions: Iterable[Position3D]) -> Self:
droplets: dict[Position, tuple[DropletType, int]] = {} droplets: dict[Position3D, tuple[DropletType, int]] = {}
faces = 0 faces = 0
for position in positions: for position in positions:
candidate = droplets.get(position) candidate = droplets.get(position)
@ -95,14 +95,14 @@ class Shower:
def count_trapped_droplets(self) -> int: def count_trapped_droplets(self) -> int:
droplets = self.droplets.copy() droplets = self.droplets.copy()
minmax: tuple[Position, Position] | None = None minmax: tuple[Position3D, Position3D] | None = None
for position in droplets.keys(): for position in droplets.keys():
minmax = position.min_max(minmax) minmax = position.min_max(minmax)
if minmax is None: if minmax is None:
raise Exception("I got no data to work with") raise Exception("I got no data to work with")
min_values, max_values = minmax min_values, max_values = minmax
todo: list[Position] = [min_values] todo: list[Position3D] = [min_values]
while todo: while todo:
current = todo[0] current = todo[0]
todo = todo[1:] todo = todo[1:]

View file

@ -1,6 +1,6 @@
from advent.common import input 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(): def test_part1():
@ -20,19 +20,19 @@ def test_part2():
def test_simple_count(): def test_simple_count():
lines = ["1,1,1", "1,1,2"] lines = ["1,1,1", "1,1,2"]
expected = 10 expected = 10
result = Shower.create(Position.parse_all(lines)) result = Shower.create(Position3D.parse_all(lines))
assert result.faces == expected assert result.faces == expected
def test_example_faces(): def test_example_faces():
lines = input.read_lines(day_num, 'example01.txt') lines = input.read_lines(day_num, 'example01.txt')
expected = 64 expected = 64
result = Shower.create(Position.parse_all(lines)) result = Shower.create(Position3D.parse_all(lines))
assert result.faces == expected assert result.faces == expected
def test_example_trapped(): def test_example_trapped():
lines = input.read_lines(day_num, 'example01.txt') lines = input.read_lines(day_num, 'example01.txt')
expected = 6 expected = 6
result = Shower.create(Position.parse_all(lines)) result = Shower.create(Position3D.parse_all(lines))
assert result.count_trapped_droplets() == expected assert result.count_trapped_droplets() == expected

View file

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field 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 enum import Enum
from typing import Iterator, Self from typing import Iterator, Self
@ -44,29 +45,17 @@ class Facing(Enum):
case Facing.Left: return Facing.Up case Facing.Left: return Facing.Up
case Facing.Down: return Facing.Left case Facing.Down: return Facing.Left
def as_position(self) -> Position2D: def as_position(self) -> Position:
match self: match self:
case Facing.Right: return Position2D(1, 0) case Facing.Right: return UNIT_X
case Facing.Up: return Position2D(0, -1) case Facing.Up: return UNIT_NEG_Y
case Facing.Left: return Position2D(-1, 0) case Facing.Left: return UNIT_NEG_X
case Facing.Down: return Position2D(0, 1) case Facing.Down: return UNIT_Y
@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)
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Player: class Player:
position: Position2D position: Position
facing: Facing facing: Facing
@property @property
@ -113,35 +102,35 @@ class PasswordJungle(ABC):
return val, start return val, start
case _: raise Exception("Illegal char") case _: raise Exception("Illegal char")
def start(self) -> tuple[Position2D, Facing]: def start(self) -> tuple[Position, Facing]:
return self.start_column(0), Facing.Right 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]): for col, char in enumerate(self.map[row]):
if char != ' ': if char != ' ':
return Position2D(col, row) return Position(col, row)
raise Exception("Empty row found") 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): for col in range(len(self.map[row]) - 1, -1, -1):
if self.map[row][col] != ' ': if self.map[row][col] != ' ':
return Position2D(col, row) return Position(col, row)
raise Exception("Empty row found") 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): for row, line in enumerate(self.map):
if col < len(line) and line[col] != ' ': if col < len(line) and line[col] != ' ':
return Position2D(col, row) return Position(col, row)
raise Exception("Empty row found") 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): for row in range(len(self.map) - 1, -1, -1):
line = self.map[row] line = self.map[row]
if col < len(line) and line[col] != ' ': if col < len(line) and line[col] != ' ':
return Position2D(col, row) return Position(col, row)
raise Exception("Empty row found") 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])): if pos.y not in range(0, len(self.map)) or pos.x not in range(0, len(self.map[pos.y])):
return ' ' return ' '
return self.map[pos.y][pos.x] return self.map[pos.y][pos.x]
@ -216,9 +205,9 @@ class CubePosition:
@dataclass(slots=True) @dataclass(slots=True)
class PasswordCubeJungle(PasswordJungle): class PasswordCubeJungle(PasswordJungle):
cube_width: int = field(default=50, init=False) 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): cube_position: CubePosition):
match facing: match facing:
case Facing.Right: facing_right = cube_position.facing case Facing.Right: facing_right = cube_position.facing
@ -300,7 +289,7 @@ class PasswordCubeJungle(PasswordJungle):
y = position.y y = position.y
x = position.x + self.cube_width - 1 - delta x = position.x + self.cube_width - 1 - delta
result = Player(Position2D(x, y), facing) result = Player(Position(x, y), facing)
return result return result

View file

@ -6,7 +6,7 @@ from .solution import (
PasswordCubeJungle, PasswordCubeJungle,
PasswordSimpleJungle, PasswordSimpleJungle,
Player, Player,
Position2D, Position,
Turn, Turn,
Vector, Vector,
day_num, day_num,
@ -56,39 +56,39 @@ def test_positions():
jungle = PasswordSimpleJungle.create(lines) jungle = PasswordSimpleJungle.create(lines)
result = jungle.start_column(0) result = jungle.start_column(0)
assert result == Position2D(8, 0) assert result == Position(8, 0)
result = jungle.start_row(0) result = jungle.start_row(0)
assert result == Position2D(0, 4) assert result == Position(0, 4)
result = jungle.end_column(4) result = jungle.end_column(4)
assert result == Position2D(11, 4) assert result == Position(11, 4)
result = jungle.end_row(4) result = jungle.end_row(4)
assert result == Position2D(4, 7) assert result == Position(4, 7)
result = jungle.start_row(8) result = jungle.start_row(8)
assert result == Position2D(8, 0) assert result == Position(8, 0)
def test_step(): def test_step():
lines = input.read_lines(day_num, 'example01.txt') lines = input.read_lines(day_num, 'example01.txt')
jungle = PasswordSimpleJungle.create(lines) jungle = PasswordSimpleJungle.create(lines)
person = jungle.step(Player(Position2D(8, 0), Facing.Right), 10) person = jungle.step(Player(Position(8, 0), Facing.Right), 10)
assert person == Player(Position2D(10, 0), Facing.Right) assert person == Player(Position(10, 0), Facing.Right)
person = jungle.step(Player(Position2D(10, 0), Facing.Down), 5) person = jungle.step(Player(Position(10, 0), Facing.Down), 5)
assert person == Player(Position2D(10, 5), Facing.Down) assert person == Player(Position(10, 5), Facing.Down)
person = jungle.step(Player(Position2D(10, 5), Facing.Right), 5) person = jungle.step(Player(Position(10, 5), Facing.Right), 5)
assert person == Player(Position2D(3, 5), Facing.Right) assert person == Player(Position(3, 5), Facing.Right)
def test_walk(): def test_walk():
lines = input.read_lines(day_num, 'example01.txt') lines = input.read_lines(day_num, 'example01.txt')
jungle = PasswordSimpleJungle.create(lines) jungle = PasswordSimpleJungle.create(lines)
person = jungle.walk() person = jungle.walk()
assert person == Player(Position2D(7, 5), Facing.Right) assert person == Player(Position(7, 5), Facing.Right)
assert person.value == 6032 assert person.value == 6032
@ -102,19 +102,19 @@ def test_cube_info():
lines = input.read_lines(day_num, 'example01.txt') lines = input.read_lines(day_num, 'example01.txt')
jungle = PasswordCubeJungle.create(lines) jungle = PasswordCubeJungle.create(lines)
person = Player(Position2D(14, 8), Facing.Up) person = Player(Position(14, 8), Facing.Up)
result = jungle.get_cube_position(person) result = jungle.get_cube_position(person)
assert result == (CubePosition(Vector(0, 1, 0), Vector(0, 0, -1)), 2) 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) result = jungle.get_cube_position(person)
assert result == (CubePosition(Vector(0, 0, -1), Vector(0, 1, 0)), 1) 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) result = jungle.get_cube_position(person)
assert result == (CubePosition(Vector(0, 0, 1), Vector(-1, 0, 0)), 2) 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) result = jungle.get_cube_position(person)
assert result == (CubePosition(Vector(-1, 0, 0), Vector(0, 0, 1)), 1) 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') lines = input.read_lines(day_num, 'example01.txt')
jungle = PasswordCubeJungle.create(lines) jungle = PasswordCubeJungle.create(lines)
person = Player(Position2D(14, 8), Facing.Up) person = Player(Position(14, 8), Facing.Up)
result = jungle.wrap(person) 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) 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) 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) result = jungle.wrap(person)
assert result == Player(Position2D(1, 7), Facing.Up) assert result == Player(Position(1, 7), Facing.Up)

View file

@ -5,6 +5,8 @@ from itertools import count, cycle
from typing import Iterator from typing import Iterator
from advent.common.position import Position
day_num = 23 day_num = 23
@ -37,59 +39,56 @@ class Direction(Enum):
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Position: class ElfPosition(Position):
x: int def get_adjacent(self, direction: Direction) -> Iterator[ElfPosition]:
y: int
def get_adjacent(self, direction: Direction) -> Iterator[Position]:
match direction: match direction:
case Direction.North: case Direction.North:
yield Position(self.x - 1, self.y - 1) yield ElfPosition(self.x - 1, self.y - 1)
yield Position(self.x, self.y - 1) yield ElfPosition(self.x, self.y - 1)
yield Position(self.x + 1, self.y - 1) yield ElfPosition(self.x + 1, self.y - 1)
case Direction.South: case Direction.South:
yield Position(self.x - 1, self.y + 1) yield ElfPosition(self.x - 1, self.y + 1)
yield Position(self.x, self.y + 1) yield ElfPosition(self.x, self.y + 1)
yield Position(self.x + 1, self.y + 1) yield ElfPosition(self.x + 1, self.y + 1)
case Direction.West: case Direction.West:
yield Position(self.x - 1, self.y - 1) yield ElfPosition(self.x - 1, self.y - 1)
yield Position(self.x - 1, self.y) yield ElfPosition(self.x - 1, self.y)
yield Position(self.x - 1, self.y + 1) yield ElfPosition(self.x - 1, self.y + 1)
case Direction.East: case Direction.East:
yield Position(self.x + 1, self.y - 1) yield ElfPosition(self.x + 1, self.y - 1)
yield Position(self.x + 1, self.y) yield ElfPosition(self.x + 1, self.y)
yield Position(self.x + 1, self.y + 1) 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 y in range(-1, 2):
for x in range(-1, 2): for x in range(-1, 2):
if x != 0 or y != 0: 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: match direction:
case Direction.North: return Position(self.x, self.y - 1) case Direction.North: return ElfPosition(self.x, self.y - 1)
case Direction.South: return Position(self.x, self.y + 1) case Direction.South: return ElfPosition(self.x, self.y + 1)
case Direction.West: return Position(self.x - 1, self.y) case Direction.West: return ElfPosition(self.x - 1, self.y)
case Direction.East: return Position(self.x + 1, self.y) case Direction.East: return ElfPosition(self.x + 1, self.y)
def min(self, other: Position) -> Position: def min(self, other: ElfPosition) -> ElfPosition:
return Position(min(self.x, other.x), min(self.y, other.y)) return ElfPosition(min(self.x, other.x), min(self.y, other.y))
def max(self, other: Position) -> Position: def max(self, other: ElfPosition) -> ElfPosition:
return Position(max(self.x, other.x), max(self.y, other.y)) return ElfPosition(max(self.x, other.x), max(self.y, other.y))
@dataclass(slots=True) @dataclass(slots=True)
class Ground: class Ground:
map: set[Position] map: set[ElfPosition]
def __str__(self) -> str: def __str__(self) -> str:
min_pos, max_pos = self.extent() min_pos, max_pos = self.extent()
result = "" result = ""
for y in range(min_pos.y, max_pos.y + 1): for y in range(min_pos.y, max_pos.y + 1):
for x in range(min_pos.x, max_pos.x + 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 += '#' result += '#'
else: else:
result += '.' result += '.'
@ -102,14 +101,14 @@ class Ground:
@classmethod @classmethod
def parse(cls, lines: Iterator[str]) -> Ground: def parse(cls, lines: Iterator[str]) -> Ground:
map: set[Position] = set() map: set[ElfPosition] = set()
for y, line in enumerate(lines): for y, line in enumerate(lines):
for x, tile in enumerate(line): for x, tile in enumerate(line):
if tile == '#': if tile == '#':
map.add(Position(x, y)) map.add(ElfPosition(x, y))
return Ground(map) return Ground(map)
def extent(self) -> tuple[Position, Position]: def extent(self) -> tuple[ElfPosition, ElfPosition]:
min_pos = next(iter(self.map)) min_pos = next(iter(self.map))
max_pos = min_pos max_pos = min_pos
for elf in self.map: for elf in self.map:
@ -126,8 +125,8 @@ class Ground:
for n in it: for n in it:
start = next(start_dispenser) start = next(start_dispenser)
target_map: dict[Position, Position] = {} target_map: dict[ElfPosition, ElfPosition] = {}
target_count: dict[Position, int] = {} target_count: dict[ElfPosition, int] = {}
for elf in self.map: for elf in self.map:
if all(pos not in self.map for pos in elf.get_all_adjacent()): if all(pos not in self.map for pos in elf.get_all_adjacent()):
continue continue

View file

@ -5,6 +5,8 @@ from queue import PriorityQueue
from typing import Iterator from typing import Iterator
from advent.common.position import UNIT_NEG_X, UNIT_NEG_Y, UNIT_X, UNIT_Y, Position
day_num = 24 day_num = 24
@ -32,7 +34,6 @@ def lcm(num1: int, num2: int) -> int:
return num1 * num2 // gcd(num1, num2) return num1 * num2 // gcd(num1, num2)
Position = tuple[int, int]
BlizList = list[Position] BlizList = list[Position]
BlizTuple = tuple[BlizList, BlizList, BlizList, BlizList] BlizTuple = tuple[BlizList, BlizList, BlizList, BlizList]
@ -54,10 +55,10 @@ class Direction(IntEnum):
def position(self) -> Position: def position(self) -> Position:
match self: match self:
case Direction.East: return 1, 0 case Direction.East: return UNIT_X
case Direction.North: return 0, -1 case Direction.North: return UNIT_NEG_Y
case Direction.West: return -1, 0 case Direction.West: return UNIT_NEG_X
case Direction.South: return 0, 1 case Direction.South: return UNIT_Y
@property @property
def char(self) -> str: def char(self) -> str:
@ -77,10 +78,10 @@ class Weather:
def print(self, time: int) -> list[str]: def print(self, time: int) -> list[str]:
current = self.blizzards[self.normal_time(time)] current = self.blizzards[self.normal_time(time)]
lines: list[str] = [] lines: list[str] = []
for row in range(self.extent[1]): for row in range(self.extent.y):
line = "" line = ""
for col in range(self.extent[0]): for col in range(self.extent.x):
if (char := current.get((col, row))) is not None: if (char := current.get(Position(col, row))) is not None:
line += char line += char
else: else:
line += '.' line += '.'
@ -95,7 +96,7 @@ class Weather:
@classmethod @classmethod
def predict_weather(cls, blizzards: BlizTuple, extent: Position) -> Weather: 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)] weather: list[dict[Position, str]] = [Weather.create_dict(blizzards)]
for _ in range(repeat - 1): for _ in range(repeat - 1):
blizzards = Weather.progress_blizzards(blizzards, extent) blizzards = Weather.progress_blizzards(blizzards, extent)
@ -108,9 +109,9 @@ class Weather:
add = Direction.East.position() add = Direction.East.position()
result: BlizList = [] result: BlizList = []
for pos in blizzards: for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1] next_pos = pos + add
if next_pos[0] >= extent[0]: if next_pos.x >= extent.x:
next_pos = 0, next_pos[1] next_pos = next_pos.set_x(0)
result.append(next_pos) result.append(next_pos)
return result return result
@ -120,9 +121,9 @@ class Weather:
add = Direction.West.position() add = Direction.West.position()
result: BlizList = [] result: BlizList = []
for pos in blizzards: for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1] next_pos = pos + add
if next_pos[0] < 0: if next_pos.x < 0:
next_pos = extent[0] - 1, next_pos[1] next_pos = next_pos.set_x(extent.x - 1)
result.append(next_pos) result.append(next_pos)
return result return result
@ -132,9 +133,9 @@ class Weather:
add = Direction.South.position() add = Direction.South.position()
result: BlizList = [] result: BlizList = []
for pos in blizzards: for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1] next_pos = pos + add
if next_pos[1] >= extent[1]: if next_pos.y >= extent.y:
next_pos = next_pos[0], 0 next_pos = next_pos.set_y(0)
result.append(next_pos) result.append(next_pos)
return result return result
@ -144,9 +145,9 @@ class Weather:
add = Direction.North.position() add = Direction.North.position()
result: BlizList = [] result: BlizList = []
for pos in blizzards: for pos in blizzards:
next_pos = pos[0] + add[0], pos[1] + add[1] next_pos = pos + add
if next_pos[1] < 0: if next_pos.y < 0:
next_pos = next_pos[0], extent[1] - 1 next_pos = next_pos.set_y(extent.y - 1)
result.append(next_pos) result.append(next_pos)
return result return result
@ -206,7 +207,7 @@ class Valley:
case '#': case '#':
return blizzards return blizzards
case '>' | '^' | '<' | 'v': case '>' | '^' | '<' | 'v':
blizzards = Valley.append(blizzards, Direction.create(char), (col, row)) blizzards = Valley.append(blizzards, Direction.create(char), Position(col, row))
case '.': case '.':
pass pass
case _: case _:
@ -223,9 +224,9 @@ class Valley:
for row, line in enumerate(lines): for row, line in enumerate(lines):
if line.startswith("##"): if line.startswith("##"):
end_col = Valley._get_wallbreak(line) end_col = Valley._get_wallbreak(line)
extent = width, row extent = Position(width, row)
return Valley(Weather.predict_weather(blizzards, extent), extent, return Valley(Weather.predict_weather(blizzards, extent), extent,
(start_col, -1), (end_col, row)) Position(start_col, -1), Position(end_col, row))
else: else:
blizzards = Valley.parse_line(blizzards, line, row) blizzards = Valley.parse_line(blizzards, line, row)
assert False, "Unreachable" assert False, "Unreachable"
@ -234,8 +235,8 @@ class Valley:
return '\n'.join(self.print(0)) return '\n'.join(self.print(0))
def print(self, time: int) -> list[str]: def print(self, time: int) -> list[str]:
first = '#' + ('#' * self.start[0]) + '.' + ('#' * (self.extent[0] - self.start[0])) first = '#' + ('#' * self.start.x) + '.' + ('#' * (self.extent.x - self.start.x))
last = '#' + ('#' * self.exit[0]) + '.' + ('#' * (self.extent[0] - self.exit[0])) last = '#' + ('#' * self.exit.x) + '.' + ('#' * (self.extent.x - self.exit.x))
lines = self.weather.print(time) lines = self.weather.print(time)
lines = [first] + ['#' + line + '#' for line in lines] + [last] lines = [first] + ['#' + line + '#' for line in lines] + [last]
return lines return lines
@ -284,9 +285,9 @@ class Step:
def print(self) -> list[str]: def print(self) -> list[str]:
lines = self.valley.print(self.time) lines = self.valley.print(self.time)
line = lines[self.position[1] + 1] line = lines[self.position.y + 1]
lines[self.position[1] + 1] = line[:self.position[0] + 1] + \ lines[self.position.y + 1] = line[:self.position.x + 1] + \
'E' + line[self.position[0] + 2:] 'E' + line[self.position.x + 2:]
return lines return lines
@ -326,11 +327,11 @@ class Step:
for dir in Direction: for dir in Direction:
add = dir.position() 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: if next_position == self.target:
yield self.reach_target() yield self.reach_target()
elif (0 <= next_position[0] < self.valley.extent[0] elif (0 <= next_position.x < self.valley.extent.x
and 0 <= next_position[1] < self.valley.extent[1]): and 0 <= next_position.y < self.valley.extent.y):
if next_position not in impassable: if next_position not in impassable:
yield self.move(next_position) yield self.move(next_position)

View file

@ -1,4 +1,5 @@
from advent.common import input from advent.common import input
from advent.common.position import Position
from .solution import BlizTuple, Valley, day_num, part1, part2 from .solution import BlizTuple, Valley, day_num, part1, part2
@ -20,9 +21,9 @@ def test_part2():
def test_parse_line(): def test_parse_line():
input = "#>>.<^<#" input = "#>>.<^<#"
expected: BlizTuple = ( expected: BlizTuple = (
[(0, 0), (1, 0)], [Position(0, 0), Position(1, 0)],
[(4, 0)], [Position(4, 0)],
[(3, 0), (5, 0)], [Position(3, 0), Position(5, 0)],
[]) [])
result = Valley.parse_line(([], [], [], []), input, 0) result = Valley.parse_line(([], [], [], []), input, 0)
assert result == expected assert result == expected