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 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
return (Position3D(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),
|
||||
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:]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue