What is the best way of checking if a part intrudes on another part's personal space (but not necessarily intersects it)?

I’ve been trying to make a randomly generated board game, but I’m stuck on some logic needed to determine whether or not a space is too close to another space on the board so it can properly create a zig-zag pattern like so:

Before/After photos of what I'm trying to achieve


The way I initially implemented this was way too slow. For a bit of background context, all of the spaces on the board are stored in a grid generated by the program. What I used to do was find the position of the space I wanted to check and then see if there were any spaces in the grid spaces surrounding the space in question. Thanks to how crappily I implemented the grid, I can’t check if the grid space around the space in question is occupied. Instead, I have to check the location of all the spaces on the grid to see if they are in the adjacent coordinates. If you’re still confused, here’s a visual example of how this used to be implemented:

Implementation Infographic

While this works, it’s very slow and makes board generation take exponentially longer to generate as the total amount of generated board spaces increases. This isn’t good, as I want to be able to generate 900 spaces across three different boards so I can have some form of “map selection” when the players first spawn in. Any suggestions for making this checking algorithm faster? Obviously I’d prefer easier fixes as opposed to harder ones, but I’m still willing to reimplement anything I need to get this working

I assume that the ignored cell in your image is the cell placed in the previous iteration of the placement loop and that the next potential placement cell is next to the previous placement cell.

If implementing the grid such that you can directly check whether a cell is occupied is not an option, then I can’t think of a way to reduce the time complexity in terms of number of parts but I do have a suggestion for reducing the time complexity in terms of required distance between parts.

If I understood correctly, then in your current approach, checking whether a part can be placed requires going through the list of already placed parts’ grid locations multiple times. The number of times you go through the list depends on the number of cells within the check distance and this number of cells depends on the distance.

Couldn’t you instead just go through that list once per potential placement cell and for each occupied cell that isn’t a cell to ignore, calculate the distance from it to the potential placement cell. If the distance is too small, then the part can’t be placed.

Also, in the image, you mention that the distance is 1 for example purposes so I assume it can be greater than 1 in practice. If the required distance is more than 1, then the strict distance check needs to ignore more than one previous cell. This is because if the check distance was 2, for example, then the first placement cell would prevent the third placement unless it’s ignored. Having to skip the strict distance check for multiple cells may cause problems so in the code below, I added a different kind of check for these cells. For each of these (excluding the latest placement cell), I check whether the distance from the potential placement cell to it is smaller than the distance from the latest placement cell to it. If that’s the case then this potential placement cell is considered invalid for placement.

I wrote this code with the assumption that you are using Manhattan distance for defining which cells to check. You can modify the distance function to use some other kind of distance such as euclidean distance, but that may also require increasing the number of cells that are ignored by the strict distance check.

local function getDistance(cell0: Vector2, cell1: Vector2): number
	local offset: Vector2 = cell1 - cell0
	local manhattanDistance: number = math.abs(offset.X) + math.abs(offset.Y)
	return manhattanDistance
end

local function canPartBePlaced(cell: Vector2, occupiedCells: {Vector2}, checkDist: number): boolean
	if cell == occupiedCells[#occupiedCells] then
		return false
	end
	
	-- last index that is checked is #occupiedCells - checkDist so that bigger checkDist causes more previous
	-- cells to be ignored by this loop.
	for iOccupiedCell: number = 1, #occupiedCells - checkDist do
		local occupiedCell: Vector2 = occupiedCells[iOccupiedCell]
		local distance: number = getDistance(cell, occupiedCell)
		if distance > checkDist then
			continue
		end

		return false
	end
	
	-- This loop checks whether the distance to at least one of the cells ignored in the earlier loop
	-- would reduce. If that's the case, then this isn't accepted as a placement position.
	for iOccupiedCell: number = math.max(#occupiedCells - checkDist + 1, 1), #occupiedCells - 1 do
		local occupiedCell: Vector2 = occupiedCells[iOccupiedCell]
		local distance: number = getDistance(cell, occupiedCell)
		if distance < getDistance(occupiedCells[#occupiedCells], occupiedCell) then
			return false
		end
	end
	
	return true
end