Find the closest enemy in a 2D grid with wrapping allowed
Here

enemy_locations = np.where(grid == 2)
enemy_locations = ((x, y) for y, x in zip(
    enemy_locations[0], enemy_locations[1]))


the x/y-coordinates are swapped, and later the x-coordinate of each enemy is subtracted from the y-coordinate of the friend, and vice versa:

moves = [sum((abs(friendly_coords[1] - enemy[0]),
                abs(friendly_coords[0] - enemy[1])))
            for enemy in enemy_locations]


I find that confusing. If you don't swap the coordinates then the computation of the locations becomes simpler

enemy_locations = zip(*np.where(grid == 2))


and the distance computation becomes more logical:

moves = [sum((abs(friendly_coords[0] - enemy[0]),
                abs(friendly_coords[1] - enemy[1])))
            for enemy in enemy_locations]


Making moves a generator instead of a list would be sufficient.



Here

friendly_coords = tuple(int(i) for i in np.where(grid == 1))
assert len(friendly_coords) == 2, "Only one friendly is allowed"


you check if exactly one friend is given. But that check does not work: If there is zero or more than two 1 elements in the array then the int(i) conversion already fails with a

TypeError: only size-1 arrays can be converted to Python scalars


The error message is not appropriate if there is no friend.

Also assertions are for finding programming errors. A grid with zero or more than 2 friends is a wrong parameter and a reason to raise a ValueError:


  Raised when an operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.


My suggestion for that part would be

friendly_locations = list(zip(*np.where(grid == 1)))
if len(friendly_locations) != 1:
    raise ValueError("There must be exactly one friend")
friendly_coord = friendly_locations[0]




In order to compute the “wrapped distances” you augment the array by additional rows and columns. The dimensions of the array increase from \$ (m, n) \$ to at most \$ (2m-1, 2n-1) \$, i.e. the number of elements is almost quadrupled if the friend is located at one of the corners.

This can be completely avoided if the computation of the distance takes the wrapping into account:

def wrapped_distance(p1: Tuple[int, int], p2: Tuple[int, int], shape: Tuple[int, int]):
    """Return the wrapped distance between two points.

    The Manhattan distance from p1 to p2 in a grid of the given shape
    is computed, taking wrapping around the edges of the grid into account.
    """
    deltax = abs(p1[0] - p2[0])
    deltay = abs(p1[1] - p2[1])
    return min(deltax, shape[0] - deltax) + min(deltay, shape[1] - deltay)


And if the array is not modified then there is no need anymore to re-compute the friends's location. Also the pre-scan for the existence of enemies

if 2 not in grid:
    return 0


can now be replaced by calling min() with a default argument.

The main function then becomes:

def calc_closest_enemy(matrix: List[str]) -&gt; int:
    """Calculate the minimum number of moves to reach an enemy.

    (... rest of your docstring omitted ...)
    """
    grid = np.array([list(s) for s in matrix], dtype=np.int32)

    friendly_locations = list(zip(*np.where(grid == 1)))
    if len(friendly_locations) != 1:
        raise ValueError("There must be exactly one friend")
    friendly_coord = friendly_locations[0]

    enemy_locations = zip(*np.where(grid == 2))
    distances = (wrapped_distance(friendly_coord, enemy, grid.shape)
                for enemy in enemy_locations)
    return min(distances, default = 0)