refactored out Position
This commit is contained in:
parent
923e967056
commit
b83bb6b37a
12 changed files with 252 additions and 216 deletions
71
advent/common/position.py
Normal file
71
advent/common/position.py
Normal 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)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue