def get_neighbors(game_board, row_idx, col_idx, is_mine=None, is_visible=None):
    """
    for a given game board and tile location, get the neighbors without going out of bounds of the board
    :param game_board: the game board
    :param row_idx: row index number of the tile in question
    :param col_idx: column index number of the tile in question
    :param is_mine: mine state of returned neighbors
    :param is_visible: visible state of returned neighbors
    :returns list of lists [(row_idx, col_idx), tile value] for the neighbors
    """

    # bounds for where neighbors could be (min_row_num, max_row_num). You need to protect against this edge case:
    # [1,2,3,4][-1:1] --> []
    # the index method [list of things][start:finish:step] works here, but if fails when you start with a negative
    # and finish with a positive when you don't have a step that's negative.
    # the same issue doesn't exist for the finish > length case
    height = len(game_board) - 1
    width = len(game_board[0]) - 1
    row_min = row_idx - 1 if row_idx > 0 else 0
    row_max = row_idx + 1 if row_idx < height else height
    col_min = col_idx - 1 if col_idx > 0 else 0
    col_max = col_idx + 1 if col_idx < width else width

    neighbors = list()
    # now make the coordinates and get the values
    for row_num in range(row_min, row_max + 1):
        for col_num in range(col_min, col_max + 1):
            # get the tile in question
            tile = game_board[row_num][col_num]

            if not (row_num == row_idx and col_num == col_idx) \
                    and (tile[0] == is_visible or is_visible is None) \
                    and (tile[1] == is_mine or is_mine is None):
                # basically, just check the state of the tile. Don't append if:
                #   If we're on the central tile
                #   If it doesn't match what we wanted from the optionals
                # Otherwise append
                neighbors.append([(row_num, col_num), tile])

    return neighbors