681 lines
23 KiB
Python
681 lines
23 KiB
Python
from __future__ import annotations
|
|
import re
|
|
from .aoc_ocr import convert_array_6
|
|
from .coordinate import Coordinate, DistanceAlgorithm, Shape
|
|
from collections import deque
|
|
from collections.abc import Callable
|
|
from enum import Enum
|
|
from heapq import heappop, heappush
|
|
from math import inf
|
|
from typing import Any, Dict, List, Iterable, Mapping
|
|
|
|
OFF = False
|
|
ON = True
|
|
|
|
|
|
class GridTransformation(Enum):
|
|
# Rotations always take the axis to rotate around as if it were the z-axis and then rotate clockwise
|
|
# Counter-Rotations likewise, just anti-clockwise
|
|
# 3D-only OPs have a number > 10
|
|
ROTATE_Z = 3
|
|
ROTATE_X = 11
|
|
ROTATE_Y = 12
|
|
COUNTER_ROTATE_X = 14
|
|
COUNTER_ROTATE_Y = 15
|
|
COUNTER_ROTATE_Z = 7
|
|
FLIP_X = 4
|
|
FLIP_Y = 5
|
|
FLIP_Z = 13
|
|
|
|
# Handy aliases
|
|
FLIP_HORIZONTALLY = 5
|
|
FLIP_VERTICALLY = 4
|
|
ROTATE_RIGHT = 3
|
|
ROTATE_LEFT = 7
|
|
|
|
|
|
class Grid:
|
|
def __init__(self, default=False):
|
|
self.__default = default
|
|
self.__grid = {}
|
|
self.minX = None
|
|
self.minY = None
|
|
self.maxX = None
|
|
self.maxY = None
|
|
self.minZ = None
|
|
self.maxZ = None
|
|
self.mode3D = False
|
|
|
|
def __trackBoundaries(self, pos: Coordinate):
|
|
if self.minX is None:
|
|
self.minX, self.maxX, self.minY, self.maxY = pos[0], pos[0], pos[1], pos[1]
|
|
else:
|
|
self.minX = pos[0] if pos[0] < self.minX else self.minX
|
|
self.minY = pos[1] if pos[1] < self.minY else self.minY
|
|
self.maxX = pos[0] if pos[0] > self.maxX else self.maxX
|
|
self.maxY = pos[1] if pos[1] > self.maxY else self.maxY
|
|
|
|
if self.mode3D:
|
|
if self.minZ is None:
|
|
self.minZ = self.maxZ = pos[2]
|
|
else:
|
|
self.minZ = pos[2] if pos[2] < self.minZ else self.minZ
|
|
self.maxZ = pos[2] if pos[2] > self.maxZ else self.maxZ
|
|
|
|
def recalcBoundaries(self) -> None:
|
|
self.minX, self.maxX, self.minY, self.maxY, self.minZ, self.maxZ = (
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)
|
|
for c in self.__grid:
|
|
self.__trackBoundaries(c)
|
|
|
|
def getBoundaries(self) -> (int, int, int, int, int, int):
|
|
if self.mode3D:
|
|
return self.minX, self.minY, self.maxX, self.maxY, self.minZ, self.maxZ
|
|
else:
|
|
return self.minX, self.minY, self.maxX, self.maxY, -inf, inf
|
|
|
|
def rangeX(self, pad: int = 0, reverse=False):
|
|
if reverse:
|
|
return range(self.maxX + pad, self.minX - pad - 1, -1)
|
|
else:
|
|
return range(self.minX - pad, self.maxX + pad + 1)
|
|
|
|
def rangeY(self, pad: int = 0, reverse=False):
|
|
if reverse:
|
|
return range(self.maxY + pad, self.minY - pad - 1, -1)
|
|
else:
|
|
return range(self.minY - pad, self.maxY + pad + 1)
|
|
|
|
def rangeZ(self, pad: int = 0, reverse=False):
|
|
if not self.mode3D:
|
|
raise ValueError("rangeZ not available in 2D space")
|
|
if reverse:
|
|
return range(self.maxZ + pad, self.minZ - pad - 1, -1)
|
|
else:
|
|
return range(self.minZ - pad, self.maxZ + pad + 1)
|
|
|
|
def get_column(self, column: int) -> list[Any]:
|
|
return [self.get(Coordinate(column, y)) for y in self.rangeY()]
|
|
|
|
def get_row(self, row: int) -> list[Any]:
|
|
return [self.get(Coordinate(x, row)) for x in self.rangeX()]
|
|
|
|
def toggle(self, pos: Coordinate):
|
|
if pos in self.__grid:
|
|
del self.__grid[pos]
|
|
else:
|
|
self.__trackBoundaries(pos)
|
|
self.__grid[pos] = not self.__default
|
|
|
|
def toggleGrid(self):
|
|
for x in self.rangeX():
|
|
for y in self.rangeY():
|
|
if not self.mode3D:
|
|
self.toggle(Coordinate(x, y))
|
|
else:
|
|
for z in self.rangeZ():
|
|
self.toggle(Coordinate(x, y, z))
|
|
|
|
def set(self, pos: Coordinate, value: Any = True) -> Any:
|
|
if pos[2] is not None:
|
|
self.mode3D = True
|
|
|
|
if (value == self.__default) and pos in self.__grid:
|
|
del self.__grid[pos]
|
|
elif value != self.__default:
|
|
self.__trackBoundaries(pos)
|
|
self.__grid[pos] = value
|
|
|
|
return value
|
|
|
|
def move(
|
|
self,
|
|
pos: Coordinate,
|
|
vec: Coordinate,
|
|
):
|
|
target = pos + vec
|
|
self.set(target, self.get(pos))
|
|
if pos in self.__grid:
|
|
del self.__grid[pos]
|
|
|
|
def add(self, pos: Coordinate, value: int | float = 1) -> int | float:
|
|
return self.set(pos, self.get(pos) + value)
|
|
|
|
def sub(self, pos: Coordinate, value: int | float = 1) -> int | float:
|
|
return self.set(pos, self.get(pos) - value)
|
|
|
|
def mul(self, pos: Coordinate, value: int | float = 1) -> int | float:
|
|
return self.set(pos, self.get(pos) * value)
|
|
|
|
def div(self, pos: Coordinate, value: int | float = 1) -> int | float:
|
|
return self.set(pos, self.get(pos) / value)
|
|
|
|
def add_shape(self, shape: Shape, value: int | float = 1) -> None:
|
|
for x in range(shape.top_left[0], shape.bottom_right[0] + 1):
|
|
for y in range(shape.top_left[1], shape.bottom_right[1] + 1):
|
|
if not shape.mode_3d:
|
|
pos = Coordinate(x, y)
|
|
self.set(pos, self.get(pos) + value)
|
|
else:
|
|
for z in range(shape.top_left[2], shape.bottom_right[2] + 1):
|
|
pos = Coordinate(x, y, z)
|
|
self.set(pos, self.get(pos) + value)
|
|
|
|
def get(self, pos: Coordinate) -> Any:
|
|
return self.__grid.get(pos, self.__default)
|
|
|
|
def getOnCount(self) -> int:
|
|
return len(self.__grid)
|
|
|
|
def find(self, value: Any) -> Iterable[Coordinate]:
|
|
for k, v in self.__grid.items():
|
|
if v == value:
|
|
yield k
|
|
|
|
def count(self, value: Any) -> int:
|
|
return list(self.__grid.values()).count(value)
|
|
|
|
def isSet(self, pos: Coordinate) -> bool:
|
|
return pos in self.__grid
|
|
|
|
def getCorners(self) -> List[Coordinate]:
|
|
if not self.mode3D:
|
|
return [
|
|
Coordinate(self.minX, self.minY),
|
|
Coordinate(self.minX, self.maxY),
|
|
Coordinate(self.maxX, self.minY),
|
|
Coordinate(self.maxX, self.maxY),
|
|
]
|
|
else:
|
|
return [
|
|
Coordinate(self.minX, self.minY, self.minZ),
|
|
Coordinate(self.minX, self.minY, self.maxZ),
|
|
Coordinate(self.minX, self.maxY, self.minZ),
|
|
Coordinate(self.minX, self.maxY, self.maxZ),
|
|
Coordinate(self.maxX, self.minY, self.minZ),
|
|
Coordinate(self.maxX, self.minY, self.maxZ),
|
|
Coordinate(self.maxX, self.maxY, self.minZ),
|
|
Coordinate(self.maxX, self.maxY, self.maxZ),
|
|
]
|
|
|
|
def isCorner(self, pos: Coordinate) -> bool:
|
|
return pos in self.getCorners()
|
|
|
|
def isWithinBoundaries(self, pos: Coordinate, pad: int = 0) -> bool:
|
|
if self.mode3D:
|
|
return (
|
|
self.minX + pad <= pos[0] <= self.maxX - pad
|
|
and self.minY + pad <= pos[1] <= self.maxY - pad
|
|
and self.minZ + pad <= pos[2] <= self.maxZ - pad
|
|
)
|
|
else:
|
|
return self.minX + pad <= pos[0] <= self.maxX - pad and self.minY + pad <= pos[1] <= self.maxY - pad
|
|
|
|
def getActiveCells(self, x: int = None, y: int = None, z: int = None) -> Iterable[Coordinate]:
|
|
if x is not None or y is not None or z is not None:
|
|
return (
|
|
c
|
|
for c in self.__grid.keys()
|
|
if (c[0] == x if x is not None else True)
|
|
and (c[1] == y if y is not None else True)
|
|
and (c[2] == z if z is not None else True)
|
|
)
|
|
else:
|
|
return self.__grid.keys()
|
|
|
|
def getRegion(self, start: Coordinate, includeDiagonal: bool = False) -> Iterable[Coordinate]:
|
|
start_value = self.get(start)
|
|
queue = deque()
|
|
queue.append(start)
|
|
visited = set()
|
|
while queue:
|
|
next_coord = queue.popleft()
|
|
if next_coord in visited or not self.isWithinBoundaries(next_coord) or self.get(next_coord) != start_value:
|
|
continue
|
|
visited.add(next_coord)
|
|
yield next_coord
|
|
for n in self.getNeighboursOf(next_coord, includeDefault=True, includeDiagonal=includeDiagonal):
|
|
queue.append(n)
|
|
|
|
def getActiveRegion(
|
|
self,
|
|
start: Coordinate,
|
|
includeDiagonal: bool = False,
|
|
ignore: List[Coordinate] = None,
|
|
) -> List[Coordinate]:
|
|
if not self.get(start):
|
|
return []
|
|
if ignore is None:
|
|
ignore = []
|
|
ignore.append(start)
|
|
for c in self.getNeighboursOf(start, includeDiagonal=includeDiagonal):
|
|
if c not in ignore:
|
|
ignore = self.getActiveRegion(c, includeDiagonal, ignore)
|
|
|
|
return ignore
|
|
|
|
def values(self):
|
|
return self.__grid.values()
|
|
|
|
def getSum(self, includeNegative: bool = True) -> int | float:
|
|
if not self.mode3D:
|
|
return sum(
|
|
self.get(Coordinate(x, y))
|
|
for x in self.rangeX()
|
|
for y in self.rangeY()
|
|
if includeNegative or self.get(Coordinate(x, y)) >= 0
|
|
)
|
|
else:
|
|
return sum(
|
|
self.get(Coordinate(x, y, z))
|
|
for x in self.rangeX()
|
|
for y in self.rangeY()
|
|
for z in self.rangeZ()
|
|
if includeNegative or self.get(Coordinate(x, y)) >= 0
|
|
)
|
|
|
|
def getNeighboursOf(
|
|
self,
|
|
pos: Coordinate,
|
|
includeDefault: bool = False,
|
|
includeDiagonal: bool = True,
|
|
) -> Iterable[Coordinate]:
|
|
neighbours = pos.getNeighbours(
|
|
includeDiagonal=includeDiagonal,
|
|
minX=self.minX,
|
|
minY=self.minY,
|
|
minZ=self.minZ,
|
|
maxX=self.maxX,
|
|
maxY=self.maxY,
|
|
maxZ=self.maxZ,
|
|
)
|
|
for x in neighbours:
|
|
if includeDefault or x in self.__grid:
|
|
yield x
|
|
|
|
def getNeighbourSum(
|
|
self,
|
|
pos: Coordinate,
|
|
includeNegative: bool = True,
|
|
includeDiagonal: bool = True,
|
|
) -> int | float:
|
|
neighbour_sum = 0
|
|
for neighbour in self.getNeighboursOf(pos, includeDefault=includeDiagonal):
|
|
if includeNegative or self.get(neighbour) > 0:
|
|
neighbour_sum += self.get(neighbour)
|
|
|
|
return neighbour_sum
|
|
|
|
def flip(self, c1: Coordinate, c2: Coordinate):
|
|
buf = self.get(c1)
|
|
self.set(c1, self.get(c2))
|
|
self.set(c2, buf)
|
|
|
|
def transform(self, mode: GridTransformation):
|
|
if mode.value > 10 and not self.mode3D:
|
|
raise ValueError("Operation not possible in 2D space", mode)
|
|
|
|
coords = self.__grid
|
|
self.__grid = {}
|
|
if mode == GridTransformation.ROTATE_X:
|
|
shift_z = self.maxY
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(c[0], c[2], -c[1]), v)
|
|
self.shift(shift_z=shift_z)
|
|
elif mode == GridTransformation.ROTATE_Y:
|
|
shift_x = self.maxX
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(-c[2], c[1], c[0]), v)
|
|
self.shift(shift_x=shift_x)
|
|
elif mode == GridTransformation.ROTATE_Z:
|
|
shift_x = self.maxX
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(-c[1], c[0], c[2]), v)
|
|
self.shift(shift_x=shift_x)
|
|
elif mode == GridTransformation.COUNTER_ROTATE_X:
|
|
shift_y = self.maxY
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(c[0], -c[2], c[1]), v)
|
|
self.shift(shift_y=shift_y)
|
|
elif mode == GridTransformation.COUNTER_ROTATE_Y:
|
|
shift_z = self.maxZ
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(c[2], c[1], -c[0]), v)
|
|
self.shift(shift_z=shift_z)
|
|
elif mode == GridTransformation.COUNTER_ROTATE_Z:
|
|
shift_y = self.maxY
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(c[1], -c[0], c[2]), v)
|
|
self.shift(shift_y=shift_y)
|
|
elif mode == GridTransformation.FLIP_X:
|
|
shift_x = self.maxX
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(-c[0], c[1], c[2]), v)
|
|
self.shift(shift_x=shift_x)
|
|
elif mode == GridTransformation.FLIP_Y:
|
|
shift_y = self.maxY
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(c[0], -c[1], c[2]), v)
|
|
self.shift(shift_y=shift_y)
|
|
elif mode == GridTransformation.FLIP_Z:
|
|
shift_z = self.maxZ
|
|
for c, v in coords.items():
|
|
self.set(Coordinate(c[0], c[1], -c[2]), v)
|
|
self.shift(shift_z=shift_z)
|
|
else:
|
|
raise NotImplementedError(mode)
|
|
|
|
self.recalcBoundaries()
|
|
|
|
def shift(self, shift_x: int = 0, shift_y: int = 0, shift_z: int = 0):
|
|
self.minX, self.minY = self.minX + shift_x, self.minY + shift_y
|
|
self.maxX, self.maxY = self.maxX + shift_x, self.maxY + shift_y
|
|
if self.mode3D:
|
|
self.minZ, self.maxZ = self.minZ + shift_z, self.maxZ + shift_z
|
|
coords = self.__grid
|
|
self.__grid = {}
|
|
for c, v in coords.items():
|
|
if self.mode3D:
|
|
nc = Coordinate(c[0] + shift_x, c[1] + shift_y, c[2] + shift_z)
|
|
else:
|
|
nc = Coordinate(c[0] + shift_x, c[1] + shift_y)
|
|
self.set(nc, v)
|
|
|
|
def shift_zero(self, recalc: bool = True):
|
|
# self.shift() to (0, 0, 0) being top, left, front
|
|
if recalc:
|
|
self.recalcBoundaries()
|
|
if self.mode3D:
|
|
self.shift(0 - self.minX, 0 - self.minY, 0 - self.minZ)
|
|
else:
|
|
self.shift(0 - self.minX, 0 - self.minY)
|
|
|
|
def getPath_BFS(
|
|
self,
|
|
pos_from: Coordinate,
|
|
pos_to: Coordinate,
|
|
includeDiagonal: bool,
|
|
walls: List[Any] = None,
|
|
stop_at_first: Any = None,
|
|
) -> List[Coordinate] | None:
|
|
queue = deque()
|
|
came_from = {pos_from: None}
|
|
queue.append(pos_from)
|
|
if walls is None:
|
|
walls = [self.__default]
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
found_end = False
|
|
for c in self.getNeighboursOf(
|
|
current,
|
|
includeDiagonal=includeDiagonal,
|
|
includeDefault=self.__default not in walls,
|
|
):
|
|
if c in came_from and self.get(c) in walls:
|
|
continue
|
|
came_from[c] = current
|
|
if c == pos_to or (stop_at_first is not None and self.get(c) == stop_at_first):
|
|
pos_to = c
|
|
found_end = True
|
|
break
|
|
queue.append(c)
|
|
if found_end:
|
|
break
|
|
|
|
if pos_to not in came_from:
|
|
return None
|
|
|
|
ret = []
|
|
while pos_to in came_from:
|
|
ret.insert(0, pos_to)
|
|
pos_to = came_from[pos_to]
|
|
|
|
return ret
|
|
|
|
def getPath(
|
|
self,
|
|
pos_from: Coordinate,
|
|
pos_to: Coordinate,
|
|
includeDiagonal: bool,
|
|
walls: List[Any] = None,
|
|
weighted: bool = False,
|
|
) -> List[Coordinate] | None:
|
|
f_costs = []
|
|
if walls is None:
|
|
walls = [self.__default]
|
|
|
|
openNodes: Dict[Coordinate, tuple] = {}
|
|
closedNodes: Dict[Coordinate, tuple] = {}
|
|
|
|
openNodes[pos_from] = (0, pos_from.getDistanceTo(pos_to), None)
|
|
heappush(f_costs, (0, pos_from))
|
|
|
|
while f_costs:
|
|
_, currentCoord = heappop(f_costs)
|
|
if currentCoord not in openNodes:
|
|
continue
|
|
currentNode = openNodes[currentCoord]
|
|
|
|
closedNodes[currentCoord] = currentNode
|
|
del openNodes[currentCoord]
|
|
if currentCoord == pos_to:
|
|
break
|
|
|
|
for neighbour in self.getNeighboursOf(currentCoord, includeDefault=True, includeDiagonal=includeDiagonal):
|
|
if self.get(neighbour) in walls or neighbour in closedNodes:
|
|
continue
|
|
|
|
if weighted:
|
|
neighbourDist = self.get(neighbour)
|
|
elif not includeDiagonal:
|
|
neighbourDist = 1
|
|
else:
|
|
neighbourDist = currentCoord.getDistanceTo(neighbour, DistanceAlgorithm.MANHATTAN, includeDiagonal)
|
|
|
|
targetDist = neighbour.getDistanceTo(pos_to)
|
|
f_cost = targetDist + neighbourDist + currentNode[1]
|
|
|
|
if neighbour not in openNodes or f_cost < openNodes[neighbour][0]:
|
|
openNodes[neighbour] = (
|
|
f_cost,
|
|
currentNode[1] + neighbourDist,
|
|
currentCoord,
|
|
)
|
|
heappush(f_costs, (f_cost, neighbour))
|
|
|
|
if pos_to not in closedNodes:
|
|
return None
|
|
else:
|
|
currentNode = closedNodes[pos_to]
|
|
pathCoords = [pos_to]
|
|
while currentNode[2]:
|
|
pathCoords.append(currentNode[2])
|
|
currentNode = closedNodes[currentNode[2]]
|
|
|
|
return pathCoords
|
|
|
|
def sub_grid(
|
|
self,
|
|
from_x: int,
|
|
from_y: int,
|
|
to_x: int,
|
|
to_y: int,
|
|
from_z: int = None,
|
|
to_z: int = None,
|
|
) -> "Grid":
|
|
if self.mode3D and (from_z is None or to_z is None):
|
|
raise ValueError("sub_grid() on mode3d Grids requires from_z and to_z to be set")
|
|
count_x, count_y, count_z = 0, 0, 0
|
|
new_grid = Grid(self.__default)
|
|
for x in range(from_x, to_x + 1):
|
|
for y in range(from_y, to_y + 1):
|
|
if not self.mode3D:
|
|
new_grid.set(Coordinate(count_x, count_y), self.get(Coordinate(x, y)))
|
|
else:
|
|
for z in range(from_z, to_z + 1):
|
|
new_grid.set(
|
|
Coordinate(count_x, count_y, count_z),
|
|
self.get(Coordinate(x, y, z)),
|
|
)
|
|
count_z += 1
|
|
|
|
count_z = 0
|
|
count_y += 1
|
|
count_y = 0
|
|
count_x += 1
|
|
|
|
return new_grid
|
|
|
|
def update(self, x: int, y: int, grid: Grid) -> None:
|
|
put_x, put_y = x, y
|
|
for get_x in grid.rangeX():
|
|
for get_y in grid.rangeY():
|
|
self.set(Coordinate(put_x, put_y), grid.get(Coordinate(get_x, get_y)))
|
|
put_y += 1
|
|
put_y = y
|
|
put_x += 1
|
|
|
|
def print(
|
|
self,
|
|
spacer: str = "",
|
|
true_char: str = "#",
|
|
false_char: str = " ",
|
|
translate: dict = None,
|
|
mark: list = None,
|
|
z_level: int = None,
|
|
bool_mode: bool = False,
|
|
):
|
|
if translate is None:
|
|
translate = {}
|
|
|
|
if true_char is not None and True not in translate:
|
|
translate[True] = true_char
|
|
if false_char is not None and False not in translate:
|
|
translate[False] = false_char
|
|
|
|
for y in range(self.minY, self.maxY + 1):
|
|
for x in range(self.minX, self.maxX + 1):
|
|
pos = Coordinate(x, y, z_level)
|
|
|
|
if mark and pos in mark:
|
|
print("X", end="")
|
|
elif bool_mode:
|
|
print(true_char if self.get(pos) else false_char, end="")
|
|
else:
|
|
value = self.get(pos)
|
|
if isinstance(value, list):
|
|
value = len(value)
|
|
|
|
if isinstance(value, Enum):
|
|
value = value.value
|
|
|
|
print(value if value not in translate else translate[value], end="")
|
|
print(spacer, end="")
|
|
|
|
print()
|
|
|
|
def get_aoc_ocr_string(self, x_shift: int = 0, y_shift: int = 0):
|
|
return convert_array_6(
|
|
[
|
|
["#" if self.get(Coordinate(x + x_shift, y + y_shift)) else "." for x in self.rangeX()]
|
|
for y in self.rangeY()
|
|
]
|
|
)
|
|
|
|
def __str__(self, true_char: str = "#", false_char: str = "."):
|
|
return "/".join(
|
|
"".join(true_char if self.get(Coordinate(x, y)) else false_char for x in range(self.minX, self.maxX + 1))
|
|
for y in range(self.minY, self.maxY + 1)
|
|
)
|
|
|
|
@classmethod
|
|
def from_str(
|
|
cls,
|
|
grid_string: str,
|
|
default: Any = False,
|
|
true_char: str = "#",
|
|
true_value: Any = True,
|
|
translate: dict = None,
|
|
mode3d: bool = False,
|
|
) -> "Grid":
|
|
if translate is None:
|
|
translate = {}
|
|
if true_char is not None and True not in translate.values() and true_char not in translate:
|
|
translate[true_char] = true_value if true_value is not None else True
|
|
|
|
ret = cls(default=default)
|
|
for y, line in enumerate(grid_string.split("/")):
|
|
for x, c in enumerate(line):
|
|
if mode3d:
|
|
coord = Coordinate(x, y, 0)
|
|
else:
|
|
coord = Coordinate(x, y)
|
|
|
|
if c in translate:
|
|
ret.set(coord, translate[c])
|
|
else:
|
|
ret.set(coord, c)
|
|
|
|
return ret
|
|
|
|
@classmethod
|
|
def from_data(
|
|
cls,
|
|
data: Iterable[Iterable],
|
|
default: Any = False,
|
|
translate: Mapping[str, Any] = None,
|
|
gen_3d: bool = False,
|
|
) -> Grid:
|
|
"""
|
|
Every entry in data will be treated as row, every entry in data[entry] will be a separate column.
|
|
gen_3d = True will just add z=0 to every Coordinate
|
|
translate is used on every data[entry] and if present as key, its value will be used instead
|
|
a value in translate can be a function with the following signature: def translate(value: Any) -> Any
|
|
a key in translate is either a string of len 1 or it will be treated as regexp
|
|
if multiple regexp match, the first encountered wins
|
|
if there is a key that matches the entry it wins over any mathing regexp
|
|
"""
|
|
grid = cls(default=default)
|
|
|
|
regex_in_translate = False
|
|
if translate is not None:
|
|
for k in translate:
|
|
if len(k) > 1:
|
|
regex_in_translate = True
|
|
|
|
for y, row in enumerate(data):
|
|
for x, col in enumerate(row):
|
|
if translate is not None and col in translate:
|
|
if isinstance(translate[col], Callable):
|
|
col = translate[col](col)
|
|
else:
|
|
col = translate[col]
|
|
elif regex_in_translate:
|
|
for k, v in translate.items():
|
|
if len(k) == 1:
|
|
continue
|
|
|
|
if re.search(k, col):
|
|
if isinstance(v, Callable):
|
|
col = translate[k](col)
|
|
else:
|
|
col = v
|
|
break
|
|
|
|
if gen_3d:
|
|
grid.set(Coordinate(x, y, 0), col)
|
|
else:
|
|
grid.set(Coordinate(x, y), col)
|
|
|
|
return grid
|
|
|
|
def __hash__(self):
|
|
return hash(frozenset(self.__grid.items()))
|