BYU logo Computer Science

More spatial Logic

We want to write a function that returns true if there are three consecutive puppies any direction (left, right, up, down, diagonal).

a grid showing an example of three consecutive puppies diagonally

Let’s imagine we are checking a grid, moving from left to right:

a grid showing three consecutive puppies in a single row

We do not need to also check from right to left! Any group of consecutive puppies we find going right would also be found going left — but we don’t need to check both. The same applies to lookup up/down and diagonally.

This limits our searching a bit. We only need to check four directions:

  • going right
  • going down
  • going down and to the left
  • going down and to the right

Using guards

Let’s re-use the is_puppy() function with guards. We wrote this previously.

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

This will ensure we never check out of bounds on the grid.

Checking for consecutive puppies

Now we can write functions checking for consecutive puppies without worrying about the edges of the grid. First, a function to look to the right:

def has_three_right(grid, row, column):
    for c in range(column, column + 3):
        if not is_puppy(grid, row, c):
            return False
    
    return True

We look at three columns in a row, starting with our current column. And if there is not a puppy in any of these cells, we can immediately return False. If we get through all three columns and find a puppy everywhere, we return True.

Similarly, we can write a function to check down:

def has_three_down(grid, row, column):
    for r in range(row, row + 3):
        if not is_puppy(grid, r, column):
            return False
    
    return True

a function to check down and to the left:

def has_three_down_left(grid, row, column):
    for r, c in zip(range(row, row + 3), range(column, column - 3, -1)):
        if not is_puppy(grid, r, c):
            return False

    return True

and a function to check down and to the right:

def has_three_down_right(grid, row, column):
    for r, c in zip(range(row, row + 3), range(column, column + 3)):
        if not is_puppy(grid, r, c):
            return False

    return True

Notice that for these last two we use zip! This lets us look diagonally. For example, to go up and to the right, we are zipping the lists [row, row + 1, row +2] and [column, column + 1, column + 2] to get a list of tuples: [(row, column), (row + 1, column + 1), (row + 2, column + 2)].

In addition, in all of these functions we use r for ranges counting rows and c for ranges counting columns, so we have to be sure to use r and/or c when checking for puppies.

Checking for three in a row

Now we can write the primary function:

def has_three_in_a_row(grid, value='X'):
    num_rows = len(grid)
    num_columns = len(grid[0])
    for row in range(num_rows):
        for column in range(num_columns):
            if has_three_right(grid, row, column)
                    or has_three_down(grid, row, column) \
                    or has_three_down_left(grid, row, column) \
                    or has_three_down_right(grid, row, column):
                return True
    return False

We are using \ to continue the if statement on multiple lines since it is really long

Running the code

We can run this code with:

park1 = [
    [None, None, 'p', 'p'],
    [None, 'p', 'p', 'p'],
    [None, None, None, None],
    [None, 'p', None, None]
]

park2 = [
    [None, None, 'p', 'p'],
    [None, None, 'p', None],
    [None, 'p', None, 'p'],
    [None, 'p', None, None]
]

park3 = [
    [None, None, None],
    [None, None, None],
    [None, None, None]
]

print(has_three_in_a_row(park1))
print(has_three_in_a_row(park2))
print(has_three_in_a_row(park3))

We get:

True
True
False