Compare commits

...

3 Commits

Author SHA1 Message Date
2de9725adb Grid(): make use of the fact that Coordinate is a tupple for some speedups
All checks were successful
Publish to PyPI / Publish to PyPI (push) Successful in 1m18s
2023-12-14 16:18:25 +01:00
32c07d2913 Coordinate() now behaves more tupley-like (methods not accept tuples as parameters, including __add__ and __sub__) 2023-12-14 15:08:49 +01:00
0408432e3d Grid() is now hashable 2023-12-14 11:24:37 +01:00
2 changed files with 120 additions and 134 deletions

View File

@ -30,11 +30,11 @@ class Coordinate(tuple):
return self[2] return self[2]
def is3D(self) -> bool: def is3D(self) -> bool:
return self.z is not None return self[2] is not None
def getDistanceTo( def getDistanceTo(
self, self,
target: Coordinate, target: Coordinate | tuple,
algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN, algorithm: DistanceAlgorithm = DistanceAlgorithm.EUCLIDEAN,
includeDiagonals: bool = False, includeDiagonals: bool = False,
) -> Union[int, float]: ) -> Union[int, float]:
@ -48,40 +48,40 @@ class Coordinate(tuple):
:return: Distance to Target :return: Distance to Target
""" """
if algorithm == DistanceAlgorithm.EUCLIDEAN: if algorithm == DistanceAlgorithm.EUCLIDEAN:
if self.z is None: if self[2] is None:
return sqrt(abs(self.x - target.x) ** 2 + abs(self.y - target.y) ** 2) return sqrt(abs(self[0] - target[0]) ** 2 + abs(self[1] - target[1]) ** 2)
else: else:
return sqrt( return sqrt(
abs(self.x - target.x) ** 2 abs(self[0] - target[0]) ** 2
+ abs(self.y - target.y) ** 2 + abs(self[1] - target[1]) ** 2
+ abs(self.z - target.z) ** 2 + abs(self[2] - target[2]) ** 2
) )
elif algorithm == DistanceAlgorithm.CHEBYSHEV: elif algorithm == DistanceAlgorithm.CHEBYSHEV:
if self.z is None: if self[2] is None:
return max(abs(target.x - self.x), abs(target.y - self.y)) return max(abs(target[0] - self[0]), abs(target[1] - self[1]))
else: else:
return max( return max(
abs(target.x - self.x), abs(target[0] - self[0]),
abs(target.y - self.y), abs(target[1] - self[1]),
abs(target.z - self.z), abs(target[2] - self[2]),
) )
elif algorithm == DistanceAlgorithm.MANHATTAN: elif algorithm == DistanceAlgorithm.MANHATTAN:
if not includeDiagonals: if not includeDiagonals:
if self.z is None: if self[2] is None:
return abs(self.x - target.x) + abs(self.y - target.y) return abs(self[0] - target[0]) + abs(self[1] - target[1])
else: else:
return ( return (
abs(self.x - target.x) abs(self[0] - target[0])
+ abs(self.y - target.y) + abs(self[1] - target[1])
+ abs(self.z - target.z) + abs(self[2] - target[2])
) )
else: else:
dist = [abs(self.x - target.x), abs(self.y - target.y)] dist = [abs(self[0] - target[0]), abs(self[1] - target[1])]
if self.z is None: if self[2] is None:
o_dist = max(dist) - min(dist) o_dist = max(dist) - min(dist)
return o_dist + 1.4 * min(dist) return o_dist + 1.4 * min(dist)
else: else:
dist.append(abs(self.z - target.z)) dist.append(abs(self[2] - target[2]))
d_steps = min(dist) d_steps = min(dist)
dist.remove(min(dist)) dist.remove(min(dist))
dist = [x - d_steps for x in dist] dist = [x - d_steps for x in dist]
@ -97,13 +97,13 @@ class Coordinate(tuple):
minZ: int = -inf, minZ: int = -inf,
maxZ: int = inf, maxZ: int = inf,
) -> bool: ) -> bool:
if self.z is None: if self[2] is None:
return minX <= self.x <= maxX and minY <= self.y <= maxY return minX <= self[0] <= maxX and minY <= self[1] <= maxY
else: else:
return ( return (
minX <= self.x <= maxX minX <= self[0] <= maxX
and minY <= self.y <= maxY and minY <= self[1] <= maxY
and minZ <= self.z <= maxZ and minZ <= self[2] <= maxZ
) )
def getCircle( def getCircle(
@ -118,9 +118,9 @@ class Coordinate(tuple):
maxZ: int = inf, maxZ: int = inf,
) -> list[Coordinate]: ) -> list[Coordinate]:
ret = [] ret = []
if self.z is None: # mode 2D if self[2] is None: # mode 2D
for x in range(self.x - radius * 2, self.x + radius * 2 + 1): for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
for y in range(self.y - radius * 2, self.y + radius * 2 + 1): for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
target = Coordinate(x, y) target = Coordinate(x, y)
if not target.inBoundaries(minX, minY, maxX, maxY): if not target.inBoundaries(minX, minY, maxX, maxY):
continue continue
@ -133,9 +133,9 @@ class Coordinate(tuple):
ret.append(target) ret.append(target)
else: else:
for x in range(self.x - radius * 2, self.x + radius * 2 + 1): for x in range(self[0] - radius * 2, self[0] + radius * 2 + 1):
for y in range(self.y - radius * 2, self.y + radius * 2 + 1): for y in range(self[1] - radius * 2, self[1] + radius * 2 + 1):
for z in range(self.z - radius * 2, self.z + radius * 2 + 1): for z in range(self[2] - radius * 2, self[2] + radius * 2 + 1):
target = Coordinate(x, y) target = Coordinate(x, y)
if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ): if not target.inBoundaries(minX, minY, maxX, maxY, minZ, maxZ):
continue continue
@ -171,7 +171,7 @@ class Coordinate(tuple):
:param maxZ: ignore all neighbours that would have an Z value above this :param maxZ: ignore all neighbours that would have an Z value above this
:return: list of Coordinate :return: list of Coordinate
""" """
if self.z is None: if self[2] is None:
if includeDiagonal: if includeDiagonal:
nb_list = [ nb_list = [
(-1, -1), (-1, -1),
@ -187,8 +187,8 @@ class Coordinate(tuple):
nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)] nb_list = [(-1, 0), (1, 0), (0, -1), (0, 1)]
for dx, dy in nb_list: for dx, dy in nb_list:
if minX <= self.x + dx <= maxX and minY <= self.y + dy <= maxY: if minX <= self[0] + dx <= maxX and minY <= self[0] + dy <= maxY:
yield self.__class__(self.x + dx, self.y + dy) yield self.__class__(self[0] + dx, self[1] + dy)
else: else:
if includeDiagonal: if includeDiagonal:
nb_list = [ nb_list = [
@ -210,19 +210,19 @@ class Coordinate(tuple):
for dx, dy, dz in nb_list: for dx, dy, dz in nb_list:
if ( if (
minX <= self.x + dx <= maxX minX <= self[0] + dx <= maxX
and minY <= self.y + dy <= maxY and minY <= self[1] + dy <= maxY
and minZ <= self.z + dz <= maxZ and minZ <= self[2] + dz <= maxZ
): ):
yield self.__class__(self.x + dx, self.y + dy, self.z + dz) yield self.__class__(self[0] + dx, self[1] + dy, self[2] + dz)
def getAngleTo(self, target: Coordinate, normalized: bool = False) -> float: def getAngleTo(self, target: Coordinate | tuple, normalized: bool = False) -> float:
"""normalized returns an angle going clockwise with 0 starting in the 'north'""" """normalized returns an angle going clockwise with 0 starting in the 'north'"""
if self.z is not None: if self[2] is not None:
raise NotImplementedError() # which angle?!?! raise NotImplementedError() # which angle?!?!
dx = target.x - self.x dx = target[0] - self[0]
dy = target.y - self.y dy = target[1] - self[1]
if not normalized: if not normalized:
return degrees(atan2(dy, dx)) return degrees(atan2(dy, dx))
else: else:
@ -232,77 +232,77 @@ class Coordinate(tuple):
else: else:
return 180.0 + abs(angle) return 180.0 + abs(angle)
def getLineTo(self, target: Coordinate) -> List[Coordinate]: def getLineTo(self, target: Coordinate | tuple) -> List[Coordinate]:
diff = target - self diff = target - self
if self.z is None: if self[2] is None:
steps = gcd(diff.x, diff.y) steps = gcd(diff[0], diff[0])
step_x = diff.x // steps step_x = diff[0] // steps
step_y = diff.y // steps step_y = diff[1] // steps
return [ return [
self.__class__(self.x + step_x * i, self.y + step_y * i) self.__class__(self[0] + step_x * i, self[1] + step_y * i)
for i in range(steps + 1) for i in range(steps + 1)
] ]
else: else:
steps = gcd(diff.x, diff.y, diff.z) steps = gcd(diff[0], diff[1], diff[2])
step_x = diff.x // steps step_x = diff[0] // steps
step_y = diff.y // steps step_y = diff[1] // steps
step_z = diff.z // steps step_z = diff[2] // steps
return [ return [
self.__class__( self.__class__(
self.x + step_x * i, self.y + step_y * i, self.z + step_z * i self[0] + step_x * i, self[1] + step_y * i, self[2] + step_z * i
) )
for i in range(steps + 1) for i in range(steps + 1)
] ]
def reverse(self) -> Coordinate: def reverse(self) -> Coordinate:
if self.z is None: if self[2] is None:
return self.__class__(-self.x, -self.y) return self.__class__(-self[0], -self[1])
else: else:
return self.__class__(-self.x, -self.y, -self.z) return self.__class__(-self[0], -self[1], -self[2])
def __add__(self, other: Coordinate) -> Coordinate: def __add__(self, other: Coordinate | tuple) -> Coordinate:
if self.z is None: if self[2] is None:
return self.__class__(self.x + other.x, self.y + other.y) return self.__class__(self[0] + other[0], self[1] + other[1])
else: else:
return self.__class__(self.x + other.x, self.y + other.y, self.z + other.z) return self.__class__(self[0] + other[0], self[1] + other[1], self[2] + other[2])
def __sub__(self, other: Coordinate) -> Coordinate: def __sub__(self, other: Coordinate | tuple) -> Coordinate:
if self.z is None: if self[2] is None:
return self.__class__(self.x - other.x, self.y - other.y) return self.__class__(self[0] - other[0], self[1] - other[1])
else: else:
return self.__class__(self.x - other.x, self.y - other.y, self.z - other.z) return self.__class__(self[0] - other[0], self[1] - other[1], self[2] - other[2])
def __mul__(self, other: int) -> Coordinate: def __mul__(self, other: int) -> Coordinate:
if self.z is None: if self[2] is None:
return self.__class__(self.x * other, self.y * other) return self.__class__(self[0] * other, self[1] * other)
else: else:
return self.__class__(self.x * other, self.y * other, self.z * other) return self.__class__(self[0] * other, self[1] * other, self[2] * other)
def __floordiv__(self, other) -> Coordinate: def __floordiv__(self, other: int | float) -> Coordinate:
if self.z is None: if self[2] is None:
return self.__class__(self.x // other, self.y // other) return self.__class__(self[0] // other, self[1] // other)
else: else:
return self.__class__(self.x // other, self.y // other, self.z // other) return self.__class__(self[0] // other, self[1] // other, self[2] // other)
def __truediv__(self, other): def __truediv__(self, other: int | float) -> Coordinate:
return self // other return self // other
def __str__(self): def __str__(self):
if self.z is None: if self[2] is None:
return "(%d,%d)" % (self.x, self.y) return "(%d,%d)" % (self[0], self[1])
else: else:
return "(%d,%d,%d)" % (self.x, self.y, self.z) return "(%d,%d,%d)" % (self[0], self[1], self[2])
def __repr__(self): def __repr__(self):
if self.z is None: if self[2] is None:
return "%s(x=%d, y=%d)" % (self.__class__.__name__, self.x, self.y) return "%s(x=%d, y=%d)" % (self.__class__.__name__, self[0], self[1])
else: else:
return "%s(x=%d, y=%d, z=%d)" % ( return "%s(x=%d, y=%d, z=%d)" % (
self.__class__.__name__, self.__class__.__name__,
self.x, self[0],
self.y, self[1],
self.z, self[2],
) )
@classmethod @classmethod

View File

@ -1,11 +1,9 @@
from __future__ import annotations from __future__ import annotations
import re import re
from collections import deque
from collections.abc import Callable
from .aoc_ocr import convert_array_6 from .aoc_ocr import convert_array_6
from .coordinate import Coordinate, DistanceAlgorithm, Shape from .coordinate import Coordinate, DistanceAlgorithm, Shape
from collections import deque
from collections.abc import Callable
from enum import Enum from enum import Enum
from heapq import heappop, heappush from heapq import heappop, heappush
from math import inf from math import inf
@ -50,19 +48,19 @@ class Grid:
def __trackBoundaries(self, pos: Coordinate): def __trackBoundaries(self, pos: Coordinate):
if self.minX is None: if self.minX is None:
self.minX, self.maxX, self.minY, self.maxY = pos.x, pos.x, pos.y, pos.y self.minX, self.maxX, self.minY, self.maxY = pos[0], pos[0], pos[1], pos[1]
else: else:
self.minX = pos.x if pos.x < self.minX else self.minX self.minX = pos[0] if pos[0] < self.minX else self.minX
self.minY = pos.y if pos.y < self.minY else self.minY self.minY = pos[1] if pos[1] < self.minY else self.minY
self.maxX = pos.x if pos.x > self.maxX else self.maxX self.maxX = pos[0] if pos[0] > self.maxX else self.maxX
self.maxY = pos.y if pos.y > self.maxY else self.maxY self.maxY = pos[1] if pos[1] > self.maxY else self.maxY
if self.mode3D: if self.mode3D:
if self.minZ is None: if self.minZ is None:
self.minZ = self.maxZ = pos.z self.minZ = self.maxZ = pos[2]
else: else:
self.minZ = pos.z if pos.z < self.minZ else self.minZ self.minZ = pos[2] if pos[2] < self.minZ else self.minZ
self.maxZ = pos.z if pos.z > self.maxZ else self.maxZ self.maxZ = pos[2] if pos[2] > self.maxZ else self.maxZ
def recalcBoundaries(self) -> None: def recalcBoundaries(self) -> None:
self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = ( self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = (
@ -125,7 +123,7 @@ class Grid:
self.toggle(Coordinate(x, y, z)) self.toggle(Coordinate(x, y, z))
def set(self, pos: Coordinate, value: Any = True) -> Any: def set(self, pos: Coordinate, value: Any = True) -> Any:
if pos.z is not None: if pos[2] is not None:
self.mode3D = True self.mode3D = True
if (value == self.__default) and pos in self.__grid: if (value == self.__default) and pos in self.__grid:
@ -159,13 +157,13 @@ class Grid:
return self.set(pos, self.get(pos) / value) return self.set(pos, self.get(pos) / value)
def add_shape(self, shape: Shape, value: int | float = 1) -> None: def add_shape(self, shape: Shape, value: int | float = 1) -> None:
for x in range(shape.top_left.x, shape.bottom_right.x + 1): for x in range(shape.top_left[0], shape.bottom_right[0] + 1):
for y in range(shape.top_left.y, shape.bottom_right.y + 1): for y in range(shape.top_left[1], shape.bottom_right[1] + 1):
if not shape.mode_3d: if not shape.mode_3d:
pos = Coordinate(x, y) pos = Coordinate(x, y)
self.set(pos, self.get(pos) + value) self.set(pos, self.get(pos) + value)
else: else:
for z in range(shape.top_left.z, shape.bottom_right.z + 1): for z in range(shape.top_left[2], shape.bottom_right[2] + 1):
pos = Coordinate(x, y, z) pos = Coordinate(x, y, z)
self.set(pos, self.get(pos) + value) self.set(pos, self.get(pos) + value)
@ -207,29 +205,29 @@ class Grid:
def isWithinBoundaries(self, pos: Coordinate, pad: int = 0) -> bool: def isWithinBoundaries(self, pos: Coordinate, pad: int = 0) -> bool:
if self.mode3D: if self.mode3D:
return ( return (
self.minX + pad <= pos.x <= self.maxX - pad self.minX + pad <= pos[0] <= self.maxX - pad
and self.minY + pad <= pos.y <= self.maxY - pad and self.minY + pad <= pos[1] <= self.maxY - pad
and self.minZ + pad <= pos.z <= self.maxZ - pad and self.minZ + pad <= pos[2] <= self.maxZ - pad
) )
else: else:
return ( return (
self.minX + pad <= pos.x <= self.maxX - pad self.minX + pad <= pos[0] <= self.maxX - pad
and self.minY + pad <= pos.y <= self.maxY - pad and self.minY + pad <= pos[1] <= self.maxY - pad
) )
def getActiveCells( def getActiveCells(
self, x: int = None, y: int = None, z: int = None self, x: int = None, y: int = None, z: int = None
) -> List[Coordinate]: ) -> Iterable[Coordinate]:
if x is not None or y is not None or z is not None: if x is not None or y is not None or z is not None:
return [ return (
c c
for c in self.__grid.keys() for c in self.__grid.keys()
if (c.x == x if x is not None else True) if (c[0] == x if x is not None else True)
and (c.y == y if y is not None else True) and (c[1] == y if y is not None else True)
and (c.z == z if z is not None else True) and (c[2] == z if z is not None else True)
] )
else: else:
return list(self.__grid.keys()) return self.__grid.keys()
def getActiveRegion( def getActiveRegion(
self, self,
@ -273,7 +271,7 @@ class Grid:
pos: Coordinate, pos: Coordinate,
includeDefault: bool = False, includeDefault: bool = False,
includeDiagonal: bool = True, includeDiagonal: bool = True,
) -> List[Coordinate]: ) -> Iterable[Coordinate]:
neighbours = pos.getNeighbours( neighbours = pos.getNeighbours(
includeDiagonal=includeDiagonal, includeDiagonal=includeDiagonal,
minX=self.minX, minX=self.minX,
@ -314,47 +312,47 @@ class Grid:
if mode == GridTransformation.ROTATE_X: if mode == GridTransformation.ROTATE_X:
shift_z = self.maxY shift_z = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c.x, c.z, -c.y), v) self.set(Coordinate(c[0], c[2], -c[1]), v)
self.shift(shift_z=shift_z) self.shift(shift_z=shift_z)
elif mode == GridTransformation.ROTATE_Y: elif mode == GridTransformation.ROTATE_Y:
shift_x = self.maxX shift_x = self.maxX
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(-c.z, c.y, c.x), v) self.set(Coordinate(-c[2], c[1], c[0]), v)
self.shift(shift_x=shift_x) self.shift(shift_x=shift_x)
elif mode == GridTransformation.ROTATE_Z: elif mode == GridTransformation.ROTATE_Z:
shift_x = self.maxX shift_x = self.maxX
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(-c.y, c.x, c.z), v) self.set(Coordinate(-c[1], c[0], c[2]), v)
self.shift(shift_x=shift_x) self.shift(shift_x=shift_x)
elif mode == GridTransformation.COUNTER_ROTATE_X: elif mode == GridTransformation.COUNTER_ROTATE_X:
shift_y = self.maxY shift_y = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c.x, -c.z, c.y), v) self.set(Coordinate(c[0], -c[2], c[1]), v)
self.shift(shift_y=shift_y) self.shift(shift_y=shift_y)
elif mode == GridTransformation.COUNTER_ROTATE_Y: elif mode == GridTransformation.COUNTER_ROTATE_Y:
shift_z = self.maxZ shift_z = self.maxZ
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c.z, c.y, -c.x), v) self.set(Coordinate(c[2], c[1], -c[0]), v)
self.shift(shift_z=shift_z) self.shift(shift_z=shift_z)
elif mode == GridTransformation.COUNTER_ROTATE_Z: elif mode == GridTransformation.COUNTER_ROTATE_Z:
shift_y = self.maxY shift_y = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c.y, -c.x, c.z), v) self.set(Coordinate(c[1], -c[0], c[2]), v)
self.shift(shift_y=shift_y) self.shift(shift_y=shift_y)
elif mode == GridTransformation.FLIP_X: elif mode == GridTransformation.FLIP_X:
shift_x = self.maxX shift_x = self.maxX
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(-c.x, c.y, c.z), v) self.set(Coordinate(-c[0], c[1], c[2]), v)
self.shift(shift_x=shift_x) self.shift(shift_x=shift_x)
elif mode == GridTransformation.FLIP_Y: elif mode == GridTransformation.FLIP_Y:
shift_y = self.maxY shift_y = self.maxY
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c.x, -c.y, c.z), v) self.set(Coordinate(c[0], -c[1], c[2]), v)
self.shift(shift_y=shift_y) self.shift(shift_y=shift_y)
elif mode == GridTransformation.FLIP_Z: elif mode == GridTransformation.FLIP_Z:
shift_z = self.maxZ shift_z = self.maxZ
for c, v in coords.items(): for c, v in coords.items():
self.set(Coordinate(c.x, c.y, -c.z), v) self.set(Coordinate(c[0], c[1], -c[2]), v)
self.shift(shift_z=shift_z) self.shift(shift_z=shift_z)
else: else:
raise NotImplementedError(mode) raise NotImplementedError(mode)
@ -370,9 +368,9 @@ class Grid:
self.__grid = {} self.__grid = {}
for c, v in coords.items(): for c, v in coords.items():
if self.mode3D: if self.mode3D:
nc = Coordinate(c.x + shift_x, c.y + shift_y, c.z + shift_z) nc = Coordinate(c[0] + shift_x, c[1] + shift_y, c[2] + shift_z)
else: else:
nc = Coordinate(c.x + shift_x, c.y + shift_y) nc = Coordinate(c[0] + shift_x, c[1] + shift_y)
self.set(nc, v) self.set(nc, v)
def shift_zero(self, recalc: bool = True): def shift_zero(self, recalc: bool = True):
@ -684,17 +682,5 @@ class Grid:
return grid return grid
def __eq__(self, other: Grid) -> bool: def __hash__(self):
if not isinstance(other, Grid): return hash(frozenset(self.__grid.items()))
return False
other_active = set(other.getActiveCells())
for c, v in self.__grid.items():
if other.get(c) != v:
return False
other_active.remove(c)
if other_active:
return False
return True