# Spatial Logic

Remember the happy puppies problem? A puppy is happy if there is another puppy next to it (up, down, left, right, diagonally):

```
None, 'p', 'p'
None, 'p', None
None, None, 'p'
'p', None, None
```

We wrote code to check all 8 directions and it was messy:

```
def is_happy(grid, row, column):
num_rows = len(grid)
num_columns = len(grid[0])
# up
if row - 1 >= 0 and is_puppy(grid, row - 1, column):
return True
# down
if row + 1 < num_rows and is_puppy(grid, row + 1, column):
return True
# left
if column - 1 >= 0 and is_puppy(grid, row, column - 1):
return True
# right
if column + 1 < num_columns and is_puppy(grid, row, column + 1):
return True
# diagonal up left
if row - 1 >= 0 and column - 1 >= 0 and is_puppy(grid, row - 1, column - 1):
return True
# diagonal down left
if row + 1 < num_rows and column - 1 >= 0 and is_puppy(grid, row + 1, column - 1):
return True
# diagonal up right
if row - 1 >= 0 and column + 1 < num_columns and is_puppy(grid, row - 1, column + 1):
return True
# diagonal down right
if row + 1 < num_rows and column + 1 < num_columns and is_puppy(grid, row + 1, column + 1):
return True
return False
```

## Using ranges

Let’s rethink this problem. Here is the case where we check a puppy in the middle of the grid:

The puppy we are checking is in dark blue, and the puppies we need to check are in all the surrounding squares, in light blue. Given the `(row, column)`

of a puppy, we can check all of the space around him with:

```
def is_happy(grid, row, column):
for r in range(row - 1, row + 2):
for c in range(column - 1, column + 2):
# middle position doesn't count, because that is our puppy
if r == row and c == column:
continue
if is_puppy(grid, r, c):
return True
return False
```

We loop through every possible row from `row - 1`

to `row + 2`

. We need `+ 2`

because range is not inclusive for the upper end. We likewise loop through every possible column from `column - 1`

to `column + 2`

. We have to be sure to skip the middle, because that is our puppy and he can’t be happy with just himself.

If we find a puppy anywhere in this range (except the middle), then we return `True`

.

But what about when the puppy is near the edge?

Here the puppy to check is at the bottom left. We need to be sure not to check the squares to its left and below it, because those are outside the grid.

We can do this by limiting the range:

```
def is_happy(grid, row, column):
# setup number of rows and columns
num_rows = len(grid)
num_columns = len(grid[0])
# find edges of the range for rows
left = max(0, row - 1)
right = min(row + 2, num_rows)
# find edges of the range for columns
up = max(0, column - 1)
down = min(column + 2, num_columns)
# check the area
for r in range(left, right):
for c in range(up, down):
# middle position doesn't count, because that is our puppy
if r == row and c == column:
continue
if is_puppy(grid, r, c):
return True
return False
```

We can combine this with our `is_puppy()`

function:

```
def is_puppy(grid, row, column):
if grid[row][column] == 'p':
return True
return False
```

and our `find_happy()`

function:

```
def find_happy(grid):
happy = []
for row in range(len(grid)):
for column in range(len(grid[row])):
if is_puppy(grid, row, column) and is_happy(grid, row, column):
happy.append((row, column))
return happy
```

We can run this with:

```
grid = [
[None, 'p', 'p'],
[None, 'p', None],
[None, None, 'p'],
['p', None, None],
]
print(find_happy(grid))
```

and we will get:

`[(0, 1), (0, 2), (1, 1), (2, 2)]`

## Using ranges and a guard

There is a different way to solve this problem! Instead of restricting our range, let’s always check all the positions around a puppy:

```
def is_happy(grid, row, column):
for r in range(row - 1, row + 2):
for c in range(column - 1, column + 2):
# middle position doesn't count, because that is our puppy
if r == row and c == column:
continue
if is_puppy(grid, r, c):
return True
return False
```

and then let’s build a *guard* into our `is_puppy()`

function:

```
def is_puppy(grid, row, column):
num_rows = len(grid)
num_columns = len(grid[0])
# check if the row is off the grid
if row < 0 or row >= num_rows:
return False
# check if the column is off the grid
if column < 0 or column >= num_columns:
return False
if grid[row][column] == 'p':
return True
return False
```

We can return False any time we are off the grid — no puppy can possibly be there!

Replace the `is_happy()`

functions and `is_puppy()`

functions as shown above, and the code still works.

## Checking larger ranges

Let’s imagine that a puppy is happy if there is a puppy within 3 cells, in any direction. After all, puppies are pretty good at running and finding each other.

We could “hard code” the number 3 into our `is_happy()`

function. But instead, we can use a *keyword* argument:

```
def is_happy(grid, row, column, distance=1):
for r in range(row - distance, row + distance + 1):
for c in range(column - distance, column + distance + 1):
# middle position doesn't count, because that is our puppy
if r == row and c == column:
continue
if is_puppy(grid, r, c):
return True
return False
```

If we use this new version of `is_happy()`

and re-run our code, we will get the same result, because the default `distance`

is `1`

. But we can change `find_happy()`

as follows:

```
def find_happy(grid):
happy = []
for row in range(len(grid)):
for column in range(len(grid[row])):
if is_puppy(grid, row, column) and is_happy(grid, row, column, distance=3):
happy.append((row, column))
return happy
```

We put a `3`

in for the `distance`

. Now if we re-run the code, we get:

`[(0, 1), (0, 2), (1, 1), (2, 2), (3, 0)]`

All of the puppies are happy!