0

I'm using a pandas dataframe to store a dynamic 2D game map for a rougelike style game map editor. The player can draw and erase rooms. I need to draw walls around these changing rooms.

I have this:

0  1  2  3  4  5  6
0  .  .  .  x  .  .
1  .  .  x  x  .  .
2  .  x  x  x  .  .
3  .  .  .  .  .  .
4  .  .  .  .  .  . 

And need this:

0  1  2  3  4  5  6
0  .  #  #  x  #  .  
1  #  #  x  x  #  . 
2  #  x  x  x  #  . 
3  #  #  #  #  #  . 
4  .  .  .  .  .  . 

What is the most efficient way to do this?

So far I followed the approach outlined here, but this leaves me with some nested if and for before and after the lambda. As I have to check first if a cell is currently dug out. Then check all eight neighbors if they are dug out or not before changing the matching cells. This really takes a tool on the frame rate. I can't be the first to struggle with something like this, but got stuck at finding a solution.

I was hoping to find a way by applying mask or a similar binary comparison. Still, I have no idea how to efficiently do the neighbor checks without falling back into nested loops.

mozway
  • 194,879
  • 13
  • 39
  • 75
JackBox
  • 69
  • 5

1 Answers1

1

What you want to do is called a binary dilation. You can do this on the underlying numpy array with scipy.ndimage.morphology.binary_dilation:

from scipy.ndimage.morphology import binary_dilation
import numpy as np

a = df.eq('x').to_numpy()
# [[False False  True  True  True False]
#  [False  True  True  True  True False]
#  [ True  True  True  True  True False]
#  [False  True  True  True False False]
#  [False False False False False False]]

df = pd.DataFrame(np.where(binary_dilation(a), 'x', df))

output:

   0  1  2  3  4  5
0  .  .  x  x  x  .
1  .  x  x  x  x  .
2  x  x  x  x  x  .
3  .  x  x  x  .  .
4  .  .  .  .  .  .

Now to get a different symbol, you can use a more complex mask (binary_dilation(a)^a) with a XOR operation (^):

a = df.eq('x').to_numpy()
df = pd.DataFrame(np.where(binary_dilation(a)^a, '#', df))

output:

   0  1  2  3  4  5
0  .  .  #  x  #  .
1  .  #  x  x  #  .
2  #  x  x  x  #  .
3  .  #  #  #  .  .
4  .  .  .  .  .  .
all neighbors

Use a different structuring element (here a 3x3 matrix of 1s):

from scipy.ndimage.morphology import binary_dilation

a = df.eq('x').to_numpy()
kernel = np.ones((3,3))

df = pd.DataFrame(np.where(binary_dilation(a, kernel)^a, '#', df))

output:

   0  1  2  3  4  5
0  .  #  #  x  #  .
1  #  #  x  x  #  .
2  #  x  x  x  #  .
3  #  #  #  #  #  .
4  .  .  .  .  .  .
other kernels

You can easily adapt the code to have any combination of neighbors

example: top left

kernel = np.array([[1, 1, 0],
                   [1, 1, 0],
                   [0, 0, 0]])

   0  1  2  3  4  5
0  .  #  #  x  .  .
1  #  #  x  x  .  .
2  #  x  x  x  .  .
3  .  .  .  .  .  .
4  .  .  .  .  .  .
mozway
  • 194,879
  • 13
  • 39
  • 75
  • Thank you! This is a step forward. But I need all eight neighbors. This solution only gives me six. Any way to also catch the edges like 0,1 or 3,0 etc.? – JackBox Feb 12 '22 at 10:54
  • 1
    @JackBox I hadn't seen. Yes of course, just use a different kind of structuring element ;) I'll update the answer – mozway Feb 12 '22 at 11:00
  • 1
    @JackBox note that you could do the same with pandas only using many `shift` operations, but this would be much less efficient/flexible ;) – mozway Feb 12 '22 at 11:12