# types from typing import Any, Callable, Container, FrozenSet, Union Boolean = bool Integer = int Integertuple = tuple[Integer, Integer] Numerical = Union[Integer, Integertuple] IntegerSet = FrozenSet[Integer] Grid = tuple[tuple[Integer]] Cell = tuple[Integer, Integertuple] Object = FrozenSet[Cell] Objects = FrozenSet[Object] Indices = FrozenSet[Integertuple] IndicesSet = FrozenSet[Indices] Patch = Union[Object, Indices] Element = Union[Object, Grid] Piece = Union[Grid, Patch] tupletuple = tuple[tuple] ContainerContainer = Container[Container] # constants ZERO = 0 ONE = 1 TWO = 2 THREE = 3 FOUR = 4 FIVE = 5 SIX = 6 SEVEN = 7 EIGHT = 8 NINE = 9 TEN = 10 F = False T = True NEG_ONE = -1 ORIGIN = (0, 0) UNITY = (1, 1) DOWN = (1, 0) RIGHT = (0, 1) UP = (-1, 0) LEFT = (0, -1) NEG_TWO = -2 NEG_UNITY = (-1, -1) UP_RIGHT = (-1, 1) DOWN_LEFT = (1, -1) ZERO_BY_TWO = (0, 2) TWO_BY_ZERO = (2, 0) TWO_BY_TWO = (2, 2) THREE_BY_THREE = (3, 3) # primitives def identity(x: Any) -> Any: """identity function""" return x def add(a: Numerical, b: Numerical) -> Numerical: """addition""" if isinstance(a, int) and isinstance(b, int): return a + b elif isinstance(a, tuple) and isinstance(b, tuple): return (a[0] + b[0], a[1] + b[1]) elif isinstance(a, int) and isinstance(b, tuple): return (a + b[0], a + b[1]) return (a[0] + b, a[1] + b) def subtract(a: Numerical, b: Numerical) -> Numerical: """subtraction""" if isinstance(a, int) and isinstance(b, int): return a - b elif isinstance(a, tuple) and isinstance(b, tuple): return (a[0] - b[0], a[1] - b[1]) elif isinstance(a, int) and isinstance(b, tuple): return (a - b[0], a - b[1]) return (a[0] - b, a[1] - b) def multiply(a: Numerical, b: Numerical) -> Numerical: """multiplication""" if isinstance(a, int) and isinstance(b, int): return a * b elif isinstance(a, tuple) and isinstance(b, tuple): return (a[0] * b[0], a[1] * b[1]) elif isinstance(a, int) and isinstance(b, tuple): return (a * b[0], a * b[1]) return (a[0] * b, a[1] * b) def divide(a: Numerical, b: Numerical) -> Numerical: """floor division""" if isinstance(a, int) and isinstance(b, int): return a // b elif isinstance(a, tuple) and isinstance(b, tuple): return (a[0] // b[0], a[1] // b[1]) elif isinstance(a, int) and isinstance(b, tuple): return (a // b[0], a // b[1]) return (a[0] // b, a[1] // b) def invert(n: Numerical) -> Numerical: """inversion with respect to addition""" return -n if isinstance(n, int) else (-n[0], -n[1]) def even(n: Integer) -> Boolean: """evenness""" return n % 2 == 0 def double(n: Numerical) -> Numerical: """scaling by two""" return n * 2 if isinstance(n, int) else (n[0] * 2, n[1] * 2) def halve(n: Numerical) -> Numerical: """scaling by one half""" return n // 2 if isinstance(n, int) else (n[0] // 2, n[1] // 2) def flip(b: Boolean) -> Boolean: """logical not""" return not b def equality(a: Any, b: Any) -> Boolean: """equality""" return a == b def contained(value: Any, container: Container) -> Boolean: """element of""" return value in container def combine(a: Container, b: Container) -> Container: """union""" return type(a)((*a, *b)) def intersection(a: FrozenSet, b: FrozenSet) -> FrozenSet: """returns the intersection of two containers""" return a & b def difference(a: Container, b: Container) -> Container: """difference""" return type(a)(e for e in a if e not in b) def dedupe(iterable: tuple) -> tuple: """remove duplicates""" return tuple(e for i, e in enumerate(iterable) if iterable.index(e) == i) def order(container: Container, compfunc: Callable) -> tuple: """order container by custom key""" return tuple(sorted(container, key=compfunc)) def repeat(item: Any, num: Integer) -> tuple: """repetition of item within vector""" return tuple(item for i in range(num)) def greater(a: Integer, b: Integer) -> Boolean: """greater""" return a > b def size(container: Container) -> Integer: """cardinality""" return len(container) def merge(containers: ContainerContainer) -> Container: """merging""" return type(containers)(e for c in containers for e in c) def maximum(container: IntegerSet) -> Integer: """maximum""" return max(container, default=0) def minimum(container: IntegerSet) -> Integer: """minimum""" return min(container, default=0) def valmax(container: Container, compfunc: Callable) -> Integer: """maximum by custom function""" return compfunc(max(container, key=compfunc, default=0)) def valmin(container: Container, compfunc: Callable) -> Integer: """minimum by custom function""" return compfunc(min(container, key=compfunc, default=0)) def argmax(container: Container, compfunc: Callable) -> Any: """largest item by custom order""" return max(container, key=compfunc, default=None) def argmin(container: Container, compfunc: Callable) -> Any: """smallest item by custom order""" return min(container, key=compfunc, default=None) def mostcommon(container: Container) -> Any: """most common item""" return max(set(container), key=container.count) def leastcommon(container: Container) -> Any: """least common item""" return min(set(container), key=container.count) def initset(value: Any) -> FrozenSet: """initialize container""" return frozenset({value}) def both(a: Boolean, b: Boolean) -> Boolean: """logical and""" return a and b def either(a: Boolean, b: Boolean) -> Boolean: """logical or""" return a or b def increment(x: Numerical) -> Numerical: """incrementing""" return x + 1 if isinstance(x, int) else (x[0] + 1, x[1] + 1) def decrement(x: Numerical) -> Numerical: """decrementing""" return x - 1 if isinstance(x, int) else (x[0] - 1, x[1] - 1) def crement(x: Numerical) -> Numerical: """incrementing positive and decrementing negative""" if isinstance(x, int): return 0 if x == 0 else (x + 1 if x > 0 else x - 1) return ( 0 if x[0] == 0 else (x[0] + 1 if x[0] > 0 else x[0] - 1), 0 if x[1] == 0 else (x[1] + 1 if x[1] > 0 else x[1] - 1), ) def sign(x: Numerical) -> Numerical: """sign""" if isinstance(x, int): return 0 if x == 0 else (1 if x > 0 else -1) return (0 if x[0] == 0 else (1 if x[0] > 0 else -1), 0 if x[1] == 0 else (1 if x[1] > 0 else -1)) def positive(x: Integer) -> Boolean: """positive""" return x > 0 def toivec(i: Integer) -> Integertuple: """vector pointing vertically""" return (i, 0) def tojvec(j: Integer) -> Integertuple: """vector pointing horizontally""" return (0, j) def sfilter(container: Container, condition: Callable) -> Container: """keep elements in container that satisfy condition""" return type(container)(e for e in container if condition(e)) def mfilter(container: Container, function: Callable) -> FrozenSet: """filter and merge""" return merge(sfilter(container, function)) def extract(container: Container, condition: Callable) -> Any: """first element of container that satisfies condition""" return next(e for e in container if condition(e)) def totuple(container: FrozenSet) -> tuple: """conversion to tuple""" return tuple(container) def first(container: Container) -> Any: """first item of container""" return next(iter(container)) def last(container: Container) -> Any: """last item of container""" return max(enumerate(container))[1] def insert(value: Any, container: FrozenSet) -> FrozenSet: """insert item into container""" return container.union(frozenset({value})) def remove(value: Any, container: Container) -> Container: """remove item from container""" return type(container)(e for e in container if e != value) def other(container: Container, value: Any) -> Any: """other value in the container""" return first(remove(value, container)) def interval(start: Integer, stop: Integer, step: Integer) -> tuple: """range""" return tuple(range(start, stop, step)) def astuple(a: Integer, b: Integer) -> Integertuple: """constructs a tuple""" return (a, b) def product(a: Container, b: Container) -> FrozenSet: """cartesian product""" return frozenset((i, j) for j in b for i in a) def pair(a: tuple, b: tuple) -> tupletuple: """zipping of two tuples""" return tuple(zip(a, b)) def branch(condition: Boolean, if_value: Any, else_value: Any) -> Any: """if else branching""" return if_value if condition else else_value def compose(outer: Callable, inner: Callable) -> Callable: """function composition""" return lambda x: outer(inner(x)) def chain(h: Callable, g: Callable, f: Callable) -> Callable: """function composition with three functions""" return lambda x: h(g(f(x))) def matcher(function: Callable, target: Any) -> Callable: """construction of equality function""" return lambda x: function(x) == target def rbind(function: Callable, fixed: Any) -> Callable: """fix the rightmost argument""" n = function.__code__.co_argcount if n == 2: return lambda x: function(x, fixed) elif n == 3: return lambda x, y: function(x, y, fixed) else: return lambda x, y, z: function(x, y, z, fixed) def lbind(function: Callable, fixed: Any) -> Callable: """fix the leftmost argument""" n = function.__code__.co_argcount if n == 2: return lambda y: function(fixed, y) elif n == 3: return lambda y, z: function(fixed, y, z) else: return lambda y, z, a: function(fixed, y, z, a) def power(function: Callable, n: Integer) -> Callable: """power of function""" if n == 1: return function return compose(function, power(function, n - 1)) def fork(outer: Callable, a: Callable, b: Callable) -> Callable: """creates a wrapper function""" return lambda x: outer(a(x), b(x)) def apply(function: Callable, container: Container) -> Container: """apply function to each item in container""" return type(container)(function(e) for e in container) def rapply(functions: Container, value: Any) -> Container: """apply each function in container to value""" return type(functions)(function(value) for function in functions) def mapply(function: Callable, container: ContainerContainer) -> FrozenSet: """apply and merge""" return merge(apply(function, container)) def papply(function: Callable, a: tuple, b: tuple) -> tuple: """apply function on two vectors""" return tuple(function(i, j) for i, j in zip(a, b)) def mpapply(function: Callable, a: tuple, b: tuple) -> tuple: """apply function on two vectors and merge""" return merge(papply(function, a, b)) def prapply(function: Callable, a: Container, b: Container) -> FrozenSet: """apply function on cartesian product""" return frozenset(function(i, j) for j in b for i in a) def mostcolor(element: Element) -> Integer: """most common color""" values = [v for r in element for v in r] if isinstance(element, tuple) else [v for v, _ in element] return max(set(values), key=values.count) def leastcolor(element: Element) -> Integer: """least common color""" values = [v for r in element for v in r] if isinstance(element, tuple) else [v for v, _ in element] return min(set(values), key=values.count) def height(piece: Piece) -> Integer: """height of grid or patch""" if len(piece) == 0: return 0 if isinstance(piece, tuple): return len(piece) return lowermost(piece) - uppermost(piece) + 1 def width(piece: Piece) -> Integer: """width of grid or patch""" if len(piece) == 0: return 0 if isinstance(piece, tuple): return len(piece[0]) return rightmost(piece) - leftmost(piece) + 1 def shape(piece: Piece) -> Integertuple: """height and width of grid or patch""" return (height(piece), width(piece)) def portrait(piece: Piece) -> Boolean: """whether height is greater than width""" return height(piece) > width(piece) def colorcount(element: Element, value: Integer) -> Integer: """number of cells with color""" if isinstance(element, tuple): return sum(row.count(value) for row in element) return sum(v == value for v, _ in element) def colorfilter(objs: Objects, value: Integer) -> Objects: """filter objects by color""" return frozenset(obj for obj in objs if next(iter(obj))[0] == value) def sizefilter(container: Container, n: Integer) -> FrozenSet: """filter items by size""" return frozenset(item for item in container if len(item) == n) def asindices(grid: Grid) -> Indices: """indices of all grid cells""" return frozenset((i, j) for i in range(len(grid)) for j in range(len(grid[0]))) def ofcolor(grid: Grid, value: Integer) -> Indices: """indices of all grid cells with value""" return frozenset((i, j) for i, r in enumerate(grid) for j, v in enumerate(r) if v == value) def ulcorner(patch: Patch) -> Integertuple: """index of upper left corner""" return tuple(map(min, zip(*toindices(patch)))) def urcorner(patch: Patch) -> Integertuple: """index of upper right corner""" return tuple(map(lambda ix: {0: min, 1: max}[ix[0]](ix[1]), enumerate(zip(*toindices(patch))))) def llcorner(patch: Patch) -> Integertuple: """index of lower left corner""" return tuple(map(lambda ix: {0: max, 1: min}[ix[0]](ix[1]), enumerate(zip(*toindices(patch))))) def lrcorner(patch: Patch) -> Integertuple: """index of lower right corner""" return tuple(map(max, zip(*toindices(patch)))) def crop(grid: Grid, start: Integertuple, dims: Integertuple) -> Grid: """subgrid specified by start and dimension""" return tuple(r[start[1] : start[1] + dims[1]] for r in grid[start[0] : start[0] + dims[0]]) def toindices(patch: Patch) -> Indices: """indices of object cells""" if len(patch) == 0: return frozenset() if isinstance(next(iter(patch))[1], tuple): return frozenset(index for value, index in patch) return patch def recolor(value: Integer, patch: Patch) -> Object: """recolor patch""" return frozenset((value, index) for index in toindices(patch)) def shift(patch: Patch, directions: Integertuple) -> Patch: """shift patch""" if len(patch) == 0: return patch di, dj = directions if isinstance(next(iter(patch))[1], tuple): return frozenset((value, (i + di, j + dj)) for value, (i, j) in patch) return frozenset((i + di, j + dj) for i, j in patch) def normalize(patch: Patch) -> Patch: """moves upper left corner to origin""" if len(patch) == 0: return patch return shift(patch, (-uppermost(patch), -leftmost(patch))) def dneighbors(loc: Integertuple) -> Indices: """directly adjacent indices""" return frozenset({(loc[0] - 1, loc[1]), (loc[0] + 1, loc[1]), (loc[0], loc[1] - 1), (loc[0], loc[1] + 1)}) def ineighbors(loc: Integertuple) -> Indices: """diagonally adjacent indices""" return frozenset( {(loc[0] - 1, loc[1] - 1), (loc[0] - 1, loc[1] + 1), (loc[0] + 1, loc[1] - 1), (loc[0] + 1, loc[1] + 1)} ) def neighbors(loc: Integertuple) -> Indices: """adjacent indices""" return dneighbors(loc) | ineighbors(loc) def objects(grid: Grid, univalued: Boolean, diagonal: Boolean, without_bg: Boolean) -> Objects: """objects occurring on the grid""" bg = mostcolor(grid) if without_bg else None objs = set() occupied = set() h, w = len(grid), len(grid[0]) unvisited = asindices(grid) diagfun = neighbors if diagonal else dneighbors for loc in unvisited: if loc in occupied: continue val = grid[loc[0]][loc[1]] if val == bg: continue obj = {(val, loc)} cands = {loc} while len(cands) > 0: neighborhood = set() for cand in cands: v = grid[cand[0]][cand[1]] if (val == v) if univalued else (v != bg): obj.add((v, cand)) occupied.add(cand) neighborhood |= {(i, j) for i, j in diagfun(cand) if 0 <= i < h and 0 <= j < w} cands = neighborhood - occupied objs.add(frozenset(obj)) return frozenset(objs) def partition(grid: Grid) -> Objects: """each cell with the same value part of the same object""" return frozenset( frozenset((v, (i, j)) for i, r in enumerate(grid) for j, v in enumerate(r) if v == value) for value in palette(grid) ) def fgpartition(grid: Grid) -> Objects: """each cell with the same value part of the same object without background""" return frozenset( frozenset((v, (i, j)) for i, r in enumerate(grid) for j, v in enumerate(r) if v == value) for value in palette(grid) - {mostcolor(grid)} ) def uppermost(patch: Patch) -> Integer: """row index of uppermost occupied cell""" return min(i for i, j in toindices(patch)) def lowermost(patch: Patch) -> Integer: """row index of lowermost occupied cell""" return max(i for i, j in toindices(patch)) def leftmost(patch: Patch) -> Integer: """column index of leftmost occupied cell""" return min(j for i, j in toindices(patch)) def rightmost(patch: Patch) -> Integer: """column index of rightmost occupied cell""" return max(j for i, j in toindices(patch)) def square(piece: Piece) -> Boolean: """whether the piece forms a square""" return ( len(piece) == len(piece[0]) if isinstance(piece, tuple) else height(piece) * width(piece) == len(piece) and height(piece) == width(piece) ) def vline(patch: Patch) -> Boolean: """whether the piece forms a vertical line""" return height(patch) == len(patch) and width(patch) == 1 def hline(patch: Patch) -> Boolean: """whether the piece forms a horizontal line""" return width(patch) == len(patch) and height(patch) == 1 def hmatching(a: Patch, b: Patch) -> Boolean: """whether there exists a row for which both patches have cells""" return len(set(i for i, j in toindices(a)) & set(i for i, j in toindices(b))) > 0 def vmatching(a: Patch, b: Patch) -> Boolean: """whether there exists a column for which both patches have cells""" return len(set(j for i, j in toindices(a)) & set(j for i, j in toindices(b))) > 0 def manhattan(a: Patch, b: Patch) -> Integer: """closest manhattan distance between two patches""" return min(abs(ai - bi) + abs(aj - bj) for ai, aj in toindices(a) for bi, bj in toindices(b)) def adjacent(a: Patch, b: Patch) -> Boolean: """whether two patches are adjacent""" return manhattan(a, b) == 1 def bordering(patch: Patch, grid: Grid) -> Boolean: """whether a patch is adjacent to a grid border""" return ( uppermost(patch) == 0 or leftmost(patch) == 0 or lowermost(patch) == len(grid) - 1 or rightmost(patch) == len(grid[0]) - 1 ) def centerofmass(patch: Patch) -> Integertuple: """center of mass""" return tuple(map(lambda x: sum(x) // len(patch), zip(*toindices(patch)))) def palette(element: Element) -> IntegerSet: """colors occurring in object or grid""" if isinstance(element, tuple): return frozenset({v for r in element for v in r}) return frozenset({v for v, _ in element}) def numcolors(element: Element) -> IntegerSet: """number of colors occurring in object or grid""" return len(palette(element)) def color(obj: Object) -> Integer: """color of object""" return next(iter(obj))[0] def toobject(patch: Patch, grid: Grid) -> Object: """object from patch and grid""" h, w = len(grid), len(grid[0]) return frozenset((grid[i][j], (i, j)) for i, j in toindices(patch) if 0 <= i < h and 0 <= j < w) def asobject(grid: Grid) -> Object: """conversion of grid to object""" return frozenset((v, (i, j)) for i, r in enumerate(grid) for j, v in enumerate(r)) def rot90(grid: Grid) -> Grid: """quarter clockwise rotation""" return tuple(row for row in zip(*grid[::-1])) def rot180(grid: Grid) -> Grid: """half rotation""" return tuple(tuple(row[::-1]) for row in grid[::-1]) def rot270(grid: Grid) -> Grid: """quarter anticlockwise rotation""" return tuple(tuple(row[::-1]) for row in zip(*grid[::-1]))[::-1] def hmirror(piece: Piece) -> Piece: """mirroring along horizontal""" if isinstance(piece, tuple): return piece[::-1] d = ulcorner(piece)[0] + lrcorner(piece)[0] if isinstance(next(iter(piece))[1], tuple): return frozenset((v, (d - i, j)) for v, (i, j) in piece) return frozenset((d - i, j) for i, j in piece) def vmirror(piece: Piece) -> Piece: """mirroring along vertical""" if isinstance(piece, tuple): return tuple(row[::-1] for row in piece) d = ulcorner(piece)[1] + lrcorner(piece)[1] if isinstance(next(iter(piece))[1], tuple): return frozenset((v, (i, d - j)) for v, (i, j) in piece) return frozenset((i, d - j) for i, j in piece) def dmirror(piece: Piece) -> Piece: """mirroring along diagonal""" if isinstance(piece, tuple): return tuple(zip(*piece)) a, b = ulcorner(piece) if isinstance(next(iter(piece))[1], tuple): return frozenset((v, (j - b + a, i - a + b)) for v, (i, j) in piece) return frozenset((j - b + a, i - a + b) for i, j in piece) def cmirror(piece: Piece) -> Piece: """mirroring along counterdiagonal""" if isinstance(piece, tuple): return tuple(zip(*(r[::-1] for r in piece[::-1]))) return vmirror(dmirror(vmirror(piece))) def fill(grid: Grid, value: Integer, patch: Patch) -> Grid: """fill value at indices""" h, w = len(grid), len(grid[0]) grid_filled = list(list(row) for row in grid) for i, j in toindices(patch): if 0 <= i < h and 0 <= j < w: grid_filled[i][j] = value return tuple(tuple(row) for row in grid_filled) def paint(grid: Grid, obj: Object) -> Grid: """paint object to grid""" h, w = len(grid), len(grid[0]) grid_painted = list(list(row) for row in grid) for value, (i, j) in obj: if 0 <= i < h and 0 <= j < w: grid_painted[i][j] = value return tuple(tuple(row) for row in grid_painted) def underfill(grid: Grid, value: Integer, patch: Patch) -> Grid: """fill value at indices that are background""" h, w = len(grid), len(grid[0]) bg = mostcolor(grid) grid_filled = list(list(row) for row in grid) for i, j in toindices(patch): if 0 <= i < h and 0 <= j < w: if grid_filled[i][j] == bg: grid_filled[i][j] = value return tuple(tuple(row) for row in grid_filled) def underpaint(grid: Grid, obj: Object) -> Grid: """paint object to grid where there is background""" h, w = len(grid), len(grid[0]) bg = mostcolor(grid) grid_painted = list(list(row) for row in grid) for value, (i, j) in obj: if 0 <= i < h and 0 <= j < w: if grid_painted[i][j] == bg: grid_painted[i][j] = value return tuple(tuple(row) for row in grid_painted) def hupscale(grid: Grid, factor: Integer) -> Grid: """upscale grid horizontally""" upscaled_grid = tuple() for row in grid: upscaled_row = tuple() for value in row: upscaled_row = upscaled_row + tuple(value for num in range(factor)) upscaled_grid = upscaled_grid + (upscaled_row,) return upscaled_grid def vupscale(grid: Grid, factor: Integer) -> Grid: """upscale grid vertically""" upscaled_grid = tuple() for row in grid: upscaled_grid = upscaled_grid + tuple(row for num in range(factor)) return upscaled_grid def upscale(element: Element, factor: Integer) -> Element: """upscale object or grid""" if isinstance(element, tuple): upscaled_grid = tuple() for row in element: upscaled_row = tuple() for value in row: upscaled_row = upscaled_row + tuple(value for num in range(factor)) upscaled_grid = upscaled_grid + tuple(upscaled_row for num in range(factor)) return upscaled_grid else: if len(element) == 0: return frozenset() di_inv, dj_inv = ulcorner(element) di, dj = (-di_inv, -dj_inv) normed_obj = shift(element, (di, dj)) upscaled_obj = set() for value, (i, j) in normed_obj: for io in range(factor): for jo in range(factor): upscaled_obj.add((value, (i * factor + io, j * factor + jo))) return shift(frozenset(upscaled_obj), (di_inv, dj_inv)) def downscale(grid: Grid, factor: Integer) -> Grid: """downscale grid""" h, w = len(grid), len(grid[0]) downscaled_grid = tuple() for i in range(h): downscaled_row = tuple() for j in range(w): if j % factor == 0: downscaled_row = downscaled_row + (grid[i][j],) downscaled_grid = downscaled_grid + (downscaled_row,) h = len(downscaled_grid) downscaled_grid2 = tuple() for i in range(h): if i % factor == 0: downscaled_grid2 = downscaled_grid2 + (downscaled_grid[i],) return downscaled_grid2 def hconcat(a: Grid, b: Grid) -> Grid: """concatenate two grids horizontally""" return tuple(i + j for i, j in zip(a, b)) def vconcat(a: Grid, b: Grid) -> Grid: """concatenate two grids vertically""" return a + b def subgrid(patch: Patch, grid: Grid) -> Grid: """smallest subgrid containing object""" return crop(grid, ulcorner(patch), shape(patch)) def hsplit(grid: Grid, n: Integer) -> tuple: """split grid horizontally""" h, w = len(grid), len(grid[0]) // n offset = len(grid[0]) % n != 0 return tuple(crop(grid, (0, w * i + i * offset), (h, w)) for i in range(n)) def vsplit(grid: Grid, n: Integer) -> tuple: """split grid vertically""" h, w = len(grid) // n, len(grid[0]) offset = len(grid) % n != 0 return tuple(crop(grid, (h * i + i * offset, 0), (h, w)) for i in range(n)) def cellwise(a: Grid, b: Grid, fallback: Integer) -> Grid: """cellwise match of two grids""" h, w = len(a), len(a[0]) resulting_grid = tuple() for i in range(h): row = tuple() for j in range(w): a_value = a[i][j] value = a_value if a_value == b[i][j] else fallback row = row + (value,) resulting_grid = resulting_grid + (row,) return resulting_grid def replace(grid: Grid, replacee: Integer, replacer: Integer) -> Grid: """color substitution""" return tuple(tuple(replacer if v == replacee else v for v in r) for r in grid) def switch(grid: Grid, a: Integer, b: Integer) -> Grid: """color switching""" return tuple(tuple(v if (v != a and v != b) else {a: b, b: a}[v] for v in r) for r in grid) def center(patch: Patch) -> Integertuple: """center of the patch""" return (uppermost(patch) + height(patch) // 2, leftmost(patch) + width(patch) // 2) def position(a: Patch, b: Patch) -> Integertuple: """relative position between two patches""" ia, ja = center(toindices(a)) ib, jb = center(toindices(b)) if ia == ib: return (0, 1 if ja < jb else -1) elif ja == jb: return (1 if ia < ib else -1, 0) elif ia < ib: return (1, 1 if ja < jb else -1) elif ia > ib: return (-1, 1 if ja < jb else -1) def index(grid: Grid, loc: Integertuple) -> Integer: """color at location""" i, j = loc h, w = len(grid), len(grid[0]) if not (0 <= i < h and 0 <= j < w): return None return grid[loc[0]][loc[1]] def canvas(value: Integer, dimensions: Integertuple) -> Grid: """grid construction""" return tuple(tuple(value for j in range(dimensions[1])) for i in range(dimensions[0])) def corners(patch: Patch) -> Indices: """indices of corners""" return frozenset({ulcorner(patch), urcorner(patch), llcorner(patch), lrcorner(patch)}) def connect(a: Integertuple, b: Integertuple) -> Indices: """line between two points""" ai, aj = a bi, bj = b si = min(ai, bi) ei = max(ai, bi) + 1 sj = min(aj, bj) ej = max(aj, bj) + 1 if ai == bi: return frozenset((ai, j) for j in range(sj, ej)) elif aj == bj: return frozenset((i, aj) for i in range(si, ei)) elif bi - ai == bj - aj: return frozenset((i, j) for i, j in zip(range(si, ei), range(sj, ej))) elif bi - ai == aj - bj: return frozenset((i, j) for i, j in zip(range(si, ei), range(ej - 1, sj - 1, -1))) return frozenset() def cover(grid: Grid, patch: Patch) -> Grid: """remove object from grid""" return fill(grid, mostcolor(grid), toindices(patch)) def trim(grid: Grid) -> Grid: """trim border of grid""" return tuple(r[1:-1] for r in grid[1:-1]) def move(grid: Grid, obj: Object, offset: Integertuple) -> Grid: """move object on grid""" return paint(cover(grid, obj), shift(obj, offset)) def tophalf(grid: Grid) -> Grid: """upper half of grid""" return grid[: len(grid) // 2] def bottomhalf(grid: Grid) -> Grid: """lower half of grid""" return grid[len(grid) // 2 + len(grid) % 2 :] def lefthalf(grid: Grid) -> Grid: """left half of grid""" return rot270(tophalf(rot90(grid))) def righthalf(grid: Grid) -> Grid: """right half of grid""" return rot270(bottomhalf(rot90(grid))) def vfrontier(location: Integertuple) -> Indices: """vertical frontier""" return frozenset((i, location[1]) for i in range(30)) def hfrontier(location: Integertuple) -> Indices: """horizontal frontier""" return frozenset((location[0], j) for j in range(30)) def backdrop(patch: Patch) -> Indices: """indices in bounding box of patch""" if len(patch) == 0: return frozenset({}) indices = toindices(patch) si, sj = ulcorner(indices) ei, ej = lrcorner(patch) return frozenset((i, j) for i in range(si, ei + 1) for j in range(sj, ej + 1)) def delta(patch: Patch) -> Indices: """indices in bounding box but not part of patch""" if len(patch) == 0: return frozenset({}) return backdrop(patch) - toindices(patch) def gravitate(source: Patch, destination: Patch) -> Integertuple: """direction to move source until adjacent to destination""" source_i, source_j = center(source) destination_i, destination_j = center(destination) i, j = 0, 0 if vmatching(source, destination): i = 1 if source_i < destination_i else -1 else: j = 1 if source_j < destination_j else -1 direction = (i, j) gravitation_i, gravitation_j = i, j maxcount = 42 c = 0 while not adjacent(source, destination) and c < maxcount: c += 1 gravitation_i += i gravitation_j += j source = shift(source, direction) return (gravitation_i - i, gravitation_j - j) def inbox(patch: Patch) -> Indices: """inbox for patch""" ai, aj = uppermost(patch) + 1, leftmost(patch) + 1 bi, bj = lowermost(patch) - 1, rightmost(patch) - 1 si, sj = min(ai, bi), min(aj, bj) ei, ej = max(ai, bi), max(aj, bj) vlines = {(i, sj) for i in range(si, ei + 1)} | {(i, ej) for i in range(si, ei + 1)} hlines = {(si, j) for j in range(sj, ej + 1)} | {(ei, j) for j in range(sj, ej + 1)} return frozenset(vlines | hlines) def outbox(patch: Patch) -> Indices: """outbox for patch""" ai, aj = uppermost(patch) - 1, leftmost(patch) - 1 bi, bj = lowermost(patch) + 1, rightmost(patch) + 1 si, sj = min(ai, bi), min(aj, bj) ei, ej = max(ai, bi), max(aj, bj) vlines = {(i, sj) for i in range(si, ei + 1)} | {(i, ej) for i in range(si, ei + 1)} hlines = {(si, j) for j in range(sj, ej + 1)} | {(ei, j) for j in range(sj, ej + 1)} return frozenset(vlines | hlines) def box(patch: Patch) -> Indices: """outline of patch""" if len(patch) == 0: return patch ai, aj = ulcorner(patch) bi, bj = lrcorner(patch) si, sj = min(ai, bi), min(aj, bj) ei, ej = max(ai, bi), max(aj, bj) vlines = {(i, sj) for i in range(si, ei + 1)} | {(i, ej) for i in range(si, ei + 1)} hlines = {(si, j) for j in range(sj, ej + 1)} | {(ei, j) for j in range(sj, ej + 1)} return frozenset(vlines | hlines) def shoot(start: Integertuple, direction: Integertuple) -> Indices: """line from starting point and direction""" return connect(start, (start[0] + 42 * direction[0], start[1] + 42 * direction[1])) def occurrences(grid: Grid, obj: Object) -> Indices: """locations of occurrences of object in grid""" occurrences = set() normed = normalize(obj) h, w = len(grid), len(grid[0]) for i in range(h): for j in range(w): occurs = True for v, (a, b) in shift(normed, (i, j)): if 0 <= a < h and 0 <= b < w: if grid[a][b] != v: occurs = False break else: occurs = False break if occurs: occurrences.add((i, j)) return frozenset(occurrences) def frontiers(grid: Grid) -> Objects: """set of frontiers""" h, w = len(grid), len(grid[0]) row_indices = tuple(i for i, r in enumerate(grid) if len(set(r)) == 1) column_indices = tuple(j for j, c in enumerate(dmirror(grid)) if len(set(c)) == 1) hfrontiers = frozenset({frozenset({(grid[i][j], (i, j)) for j in range(w)}) for i in row_indices}) vfrontiers = frozenset({frozenset({(grid[i][j], (i, j)) for i in range(h)}) for j in column_indices}) return hfrontiers | vfrontiers def compress(grid: Grid) -> Grid: """removes frontiers from grid""" ri = tuple(i for i, r in enumerate(grid) if len(set(r)) == 1) ci = tuple(j for j, c in enumerate(dmirror(grid)) if len(set(c)) == 1) return tuple(tuple(v for j, v in enumerate(r) if j not in ci) for i, r in enumerate(grid) if i not in ri) def hperiod(obj: Object) -> Integer: """horizontal periodicity""" normalized = normalize(obj) w = width(normalized) for p in range(1, w): offsetted = shift(normalized, (0, -p)) pruned = frozenset({(c, (i, j)) for c, (i, j) in offsetted if j >= 0}) if pruned.issubset(normalized): return p return w def vperiod(obj: Object) -> Integer: """vertical periodicity""" normalized = normalize(obj) h = height(normalized) for p in range(1, h): offsetted = shift(normalized, (-p, 0)) pruned = frozenset({(c, (i, j)) for c, (i, j) in offsetted if i >= 0}) if pruned.issubset(normalized): return p return h