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

View file

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

View file

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

View file

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

View file

@ -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:]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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