Write a function traverse() that takes in a list tb of n strings each containing n lower case characters
(a-z).
tb represents a square table with n rows and n columns. The function returns a string st generated by the procedure below that traverses the grid starting from the top left cell and ending at the bottom right cell.
At every step, the procedure moves either horizontally to the right or vertically down, depending on which of the two cells has a \smaller" letter (i.e., a letter that appears earlier in the alphabetical order).
The letter in the visited cell is then added to st. In case of ties, either direction can be taken.
When the right or bottom edges of the table are reached, there is obviously only a unique next cell to move to. As an example, traverse(["veus", "oxde", "oxlx", "hwuj"]) returns "veudexj"
so the table would look like this:
v o o h
e x x w
u d l u
s e x j
I am new in python and I wrote this code ... but it only prints "veuexj" I would say the problem is in this line if new_list[a - 1][b - 1] == new_list[a - 1][-2]: which force the parameter to skip the 'd' character. #And I don't know how to solve it.
def traverse(tb_list):
new_list = tb_list.copy()
st = new_list[0][0]
parameter = ''
handler = 1
for a in range(1, len(new_list)):
for b in range(handler, len(new_list[a])):
if new_list[a - 1][b - 1] == new_list[a - 1][-2]:
parameter = new_list[a][b]
elif new_list[a - 1][b - 1] > min(new_list[a - 1][b], new_list[a][b - 1]):
parameter = min(new_list[a - 1][b], new_list[a][b - 1])
elif new_list[a - 1][b - 1] < min(new_list[a - 1][b], new_list[a][b - 1]):
parameter = min(new_list[a - 1][b], new_list[a][b - 1])
st += parameter
handler = b
return st
print(traverse(["veus", "oxde", "oxlx", "hwuj"]))
You can try something like this (explanation added as comments):
def traverse(tb_list):
lst = tb_list.copy() #Takes a copy of tb_list
lst = list(zip(*[list(elem) for elem in lst])) #Transposes the list
final = lst[0][0] #Sets final as the first element of the list
index = [0,0] #Sets index to 0,0
while True:
x = index[0] #The x coordinate is the first element of the list
y = index[1] #The y coordinate is the second element of the list
if x == len(lst) - 1: #Checks if the program has reached the right most point of the table
if y == len(list(zip(*lst))) - 1: #Checks if program has reached the bottommost point of the table
return final #If both the conditions are True, it returns final (the final string)
else:
final += lst[x][y+1] #If the program has reached the right most corner, but not the bottommost, then the program moves one step down
index = [x, y+1] #Sets the index to the new coordinates
elif y == len(list(zip(*lst))) - 1: #Does the same thing in the previous if condition button in an opposite way (checks if program has reached bottommost corner first, rightmost corner next)
if x == len(lst) - 1:
return final
else:
final += lst[x + 1][y] #If the program has reached the bottommost corner, but not the rightmost, then the program moves one step right
index = [x + 1, y]
else: #If both conditions are false (rightmost and bottommost)
if lst[x+1][y] < lst[x][y+1]: #Checks if right value is lesser than the bottom value
final += lst[x+1][y] #If True, then it moves one step right and adds the alphabet at that index to final
index = [x+1,y] #Sets the index to the new coords
else: #If the previous if condition is False (bottom val > right val)
final += lst[x][y+1] #Moves one step down and adds the alphabet at that index to final
index = [x,y+1] #Sets the index to the new coords
lst = ["veus", "oxde", "oxlx", "hwuj"]
print(traverse(lst))
Output:
veudexj
I have added the explanation as comments, so take your time to go through it. If you are still not clear with any part of the code, feel free to ask me. Any suggestions to optimize/shorten my code are most welcome.
Related
I'm making a chess program and as such need the program to tell me what piece I have clicked on. So far it tells me if I've clicked on any piece at all, by matching rounded mouse x and y coordinates to a list of current piece coordinates. However, the program doesn't know what exact piece I've clicked, and I'm wondering if using a while loop I can make the loop end when a piece is found, and print/store the section of the list I was on when I found a matching piece to my mouse coords.
Currently I've got this code, which should be a good starting point for my problem
if event.type == pygame.MOUSEBUTTONDOWN:
mousepos = pygame.mouse.get_pos()
#rounddown80 is a function to round down mouse coords to multiple of 80
roundedmouse1 = roundup80(mousepos[0])
roundedmouse2 = roundup80(mousepos[1])
#print(roundedmouse1,roundedmouse2)
mousecoords = [roundedmouse1,roundedmouse2]
#print(mousecoords)
foundpiece = False
while foundpiece == False:
for x in piecespositions:
if x[0] == mousecoords[0] and x[1] == mousecoords[1]:
print("Great job you clicked a piece")
foundpiece = True
And my piecepositions list looks like
piecespositions = [queenblackpos,kingblackpos,bishop1blackpos,bishop2blackpos,knight1blackpos,knight2blackpos,
etc for all pieces, and queenblackpos for example would be [80,160]
You can keep track of what piece you are on inside the while loop, either in for i in range(0,len(piecepositions)) or with a variable that increases.
For example something like this:
while foundpiece == False:
index=0 #Reset index before checking all pieces
for x in piecespositions:
if x[0] == mousecoords[0] and x[1] == mousecoords[1]:
print("Great job you clicked a piece")
foundpiece = True
found_pos=x #store position of the piece that was found
index=index+1 #Keep adding to index to know which piece was found
pygame.display.flip() #Update the window
print (f"Piece found in {index}: {found_pos}") #print the position of the piece as well as what is its index in the list.
This will store the position of the piece that was found in found_pos and keep track of which piece is being checked at the moment using index var. After the while loop ends it prints out the position of the piece and the index of that piece in the list.
Do note though, during the while loop the script will be "stuck" until the while is finished, so it will be unable to update the window during the piece check therefore the game will freeze. So to solve this, you have to add pygame.display.flip() inside the while.
I did something like this. Hope that it will help You:
square_s = 160 # square(tile) size - you can pass your value
letters = ["A", "B", "C", "D", "E", "F", "G", "H"] # letters used in pos symbols
# match each tile on board to the right cords symbol and value(x, y)
def matchSqrCords(sqr_s, letters):
positions = [] # matched positions
for y in range(8): # for each row on the board
for x in range(8): # for each column(tile in a row) on the board
pos = ["", 0, 0] # curr position
pos[0] += letters[y] # add letter prefix to pos symbol
pos[0] += str(x + 1) # add number prefix to pos symbol
pos[1] = sqr_s*(x + 1) # calc max x range of a tile
pos[2] = sqr_s*(y + 1) # calc max y range of a tile
positions.append(pos) # add matched pos to list
return positions # return list with matched positions
def checkPosition(given_cords, cords, sqr_s):
# check if typed values are on the board
if given_cords[0] > sqr_s * 8 or given_cords[1] > sqr_s * 8:
return "Cords not on the board!"
else:
for cord in cords: # check all cords
# check if given cords x value is in range of curr cord
if given_cords[0] <= cord[1] and given_cords[0] > cord[1] - sqr_s:
# check if given cords y value is in range of curr cord
if given_cords[1] <= cord[2] and given_cords[1] > cord[2] - sqr_s:
return cord[0] # return symbol of the found cord
matched_pos = matchSqrCords(square_s, letters=letters)
print(checkPosition([192, 569], matched_pos, square_s))
I'm reading the code below about First Search Program - Artificial Intelligence for Robotics and I stuck a little with the work of these two lines below:
x2 = x+delta[i][0]
y2 = y+delta[i][1]
I know about delta for the movement pattern from a node to the neighbors, but I couldn't understand how these two lines are working here. Could anyone explain them for me please?.
The code below:
#grid format
# 0 = navigable space
# 1 = occupied space
grid=[[0,0,1,0,0,0],
[0,0,1,0,0,0],
[0,0,0,0,1,0],
[0,0,1,1,1,0],
[0,0,0,0,1,0]]
init = [0,0]
goal = [len(grid)-1,len(grid[0])-1]
#Below the four potential actions to the single field
delta=[[-1, 0], #up
[ 0,-1], #left
[ 1, 0], #down
[ 0, 1]] #right
delta_name = ['^','<','V','>']
cost = 1
def search():
#open list elements are of the type [g,x,y]
closed = [[0 for row in range(len(grid[0]))] for col in range(len(grid))]
#We initialize the starting location as checked
closed[init[0]][init[1]] = 1
# we assigned the cordinates and g value
x = init[0]
y = init[1]
g = 0
#our open list will contain our initial value
open = [[g,x,y]]
found = False #flag that is set when search complete
resign= False #Flag set if we can't find expand
#print('initial open list:')
#for i in range(len(open)):
#print(' ', open[i])
#print('----')
while found is False and resign is False:
#Check if we still have elements in the open list
if len(open)==0: #If our open list is empty
resign=True
print('Fail')
print('############# Search terminated without success')
else:
#if there is still elements on our list
#remove node from list
open.sort()
open.reverse() #reverse the list
next = open.pop()
#print('list item')
#print('next')
#Then we assign the three values to x,y and g.
x = next[1]
y = next[2]
g = next[0]
#Check if we are done
if x == goal[0] and y == goal[1]:
found = True
print(next) #The three elements above this if
print('############## Search is success')
else:
#expand winning element and add to new open list
for i in range(len(delta)):
x2 = x+delta[i][0]
y2 = y+delta[i][1]
#if x2 and y2 falls into the grid
if x2 >= 0 and x2 < len(grid) and y2 >=0 and y2 <= len(grid[0])-1:
#if x2 and y2 not checked yet and there is not obstacles
if closed[x2][y2] == 0 and grid[x2][y2] == 0:
g2 = g+cost #we increment the cose
open.append([g2,x2,y2])#we add them to our open list
#print('append list item')
#print([g2,x2,y2])
#Then we check them to never expand again
closed[x2][y2] = 1
search()
x2 and y2 are the position after the i'th potential action in delta is performed.
delta[i][0] means the first element of the i'th action in delta (the change in x caused by the i'th action)
delta[i][1] means the second element of the i'th action in delta (the change in y caused by the i'th action)
So x2=x+delta[i][0] and y2=y+delta[i][1] are applying that i'th potential action and storing the resulting new x and y values in x2 and y2.
In the context of this program, these values are created for each potential action (for i in range(len(delta)) and if the resulting position falls into the grid, hasn't been checked yet, and doesn't have obstacles, then the position is added to the open list.
When I hand this code in on a site (from my university) that corrects it, it is too long for its standards.
Here is the code:
def pangram(String):
import string
alfabet = list(string.ascii_lowercase)
interpunctie = string.punctuation + "’" + "123456789"
String = String.lower()
string_1 = ""
for char in String:
if not char in interpunctie:
string_1 += char
string_1 = string_1.replace(" ", "")
List = list(string_1)
List.sort()
list_2 = []
for index, char in enumerate(List):
if not List[index] == 0:
if not (char == List[index - 1]):
list_2.append(char)
return list_2 == alfabet
def venster(tekst):
pangram_gevonden = False
if pangram(tekst) == False:
return None
for lengte in range(26, len(tekst)):
if pangram_gevonden == True:
break
for n in range(0, len(tekst) - lengte):
if pangram(tekst[n:lengte+n]):
kortste_pangram = tekst[n:lengte+n]
pangram_gevonden = True
break
return kortste_pangram
So the first function (pangram) is fine and it determines whether or not a given string is a pangram: it contains all the letters of the alphabet at least once.
The second function checks whether or not the string(usually a longer tekst) is a pangram or not and if it is, it returns the shortest possible pangram within that tekst (even if that's not correct English). If there are two pangrams with the same length: the most left one is returned.
For this second function I used a double for loop: The first one determines the length of the string that's being checked (26 - len(string)) and the second one uses this length to go through the string at each possible point to check if it is a pangram. Once the shortest (and most left) pangram is found, it breaks out of both of the for loops.
However this (apparantly) still takes too long. So i wonder if anyone knew a faster way of tackling this second function. It doesn't necessarily have to be with a for loop.
Thanks in advance
Lucas
Create a map {letter; int} and activecount counter.
Make two indexes left and right, set them in 0.
Move right index.
If l=s[right] is letter, increment value for map key l.
If value becomes non-zero - increment activecount.
Continue until activecount reaches 26
Now move left index.
If l=s[left] is letter, decrement value for map key l.
If value becomes zero - decrement activecount and stop.
Start moving right index again and so on.
Minimal difference between left and right while
activecount==26 corresponds to the shortest pangram.
Algorithm is linear.
Example code for string containing only lower letters from alphabet 'abcd'. Returns length of the shortest substring that contains all letters from abcd. Does not check for valid chars, is not thoroughly tested.
import string
def findpangram(s):
alfabet = list(string.ascii_lowercase)
map = dict(zip(alfabet, [0]*len(alfabet)))
left = 0
right = 0
ac = 0
minlen = 100000
while left < len(s):
while right < len(s):
l = s[right]
c = map[l]
map[l] = c + 1
right += 1
if c==0:
ac+=1
if ac == 4:
break
if ac < 4:
break
if right - left < minlen:
minlen = right - left
while left < right:
l = s[left]
c = map[l]
map[l] = c - 1
left += 1
if c==1:
ac-=1
break
if right - left + 2 < minlen:
minlen = right - left + 1
return minlen
print(findpangram("acacdbcca"))
I have a problem where I have to find the largest square in an n * n grid.
e.g.
. . . . .
. # . # .
. # . . .
. # . # .
. # . . .
where the biggest square would be 3 by 3 in the bottom corner.
I am supposed to return the most steps someone could take before turning right so that they can repeat this infinitely without hitting a wall "#" or going outside the n * n square which is why the output is one less that the width/length of the square.
My code loops through the grid left to right, top to bottom looking for vertices that face down and to the right. Once it finds one it then looks for the biggest possible vertex facing up and to the right and when it finds that checks all four sides to see whether or not they are made up or .. This code works in under 1 second for me on anything around n = 100, however I need it to run at 1 second for n = 500. Any tips on how I can speed this up?
import sys
input = sys.stdin.readline
n = int(input())
maze = [list(input()) for _ in range(n)]
squares = 0
for r in range(n - 1):
for c in range(n - 1):
if maze[r][c] == '.' and maze[r][c + 1] == '.' and maze[r + 1] [c] == '.':
sides = []
for i in range(min(n - r - 1, n - c - 1), -1, -1):
if maze[r + i][c + i] == '.' and maze[r + i][c + i - 1] == '.' and maze[r + i - 1][c + i] == '.':
sides = i
if maze[r][c : c + sides] == ['.'] * sides and maze[r + sides][c : c + sides] == ['.'] * sides:
a = True
for j in range(sides):
if maze[r + j][c] != '.' or maze[r + j][c + sides] != '.':
a = False
if a and sides > squares:
squares = sides
break
if squares == n - 1:
break
print(squares)
I can think of a O(n^3) algorithm as follows:
Precompute 4 arrays: top[][], bottom[][], left[][], right[][], each stores the maximum length of a direction that you can go from (i,j)
For each (i,j) , use it as a square's bottom left corner, for each its diagonal points (i-1, j+1), (i-2, j+2)...etc., test if those points can be used as the square's top right corner. Store the maximum square side in the process
For step 1, all 4 arrays can be precomputed in O(n^2)
For step 2, as we loop through all (i,j), and for each (i,j) we have to see at most all diagonal points which is at most n of them, total we get O(n^3)
The test in step 2 can be done in O(1) using the 4 precomputed arrays, simply check if the 4 corners of the "possible squares" can be joined by checking the corresponding directions (top, bottom, left, right)
Of course, there are many minor things which can be done to speed up, for example:
In step 2, for each (i,j), only check for diagonal points which is in the range [current_maximum_diagonal_found ... max(right[i][j], top[i][j])]
Update current_maximum_diagonal_found along the whole algorithm, so that we hope for some (i,j), we do not need to check whole n diagonal points.
But strictly speaking, it is still O(n^3), but as far as I know it should be able to run in 1 second for n~500
that's an interesting problem. I tried out some things and ended up with this implementation which is O(n^3). I commented the code so that you can follow the idea hopefully. There's still room for speed improvements, but this version already does the job (e.g. with maze size 500x500):
Finished after 0.708 seconds.
Result: 112581 squares found, maximum square (x=13, y=270, size=18).
This is the source code (Python 3):
import random
import pprint
import time
# small sample maze
maze = ['.....',
'...#.',
'.#...',
'.#.#.',
'.#...']
# convert to boolean maze
maze_bin = [[True if cell == '.' else False for cell in line] for line in maze]
# uncomment to generate a random maze
# maze_size = 500
# threshold = 0.2
# maze_bin = [[1 if random.random() >= threshold else 0 for _ in range(maze_size)] for _ in range(maze_size)]
# take start time
t1 = time.time()
# rotate the maze (first column becomes first row, first row becomes first column)
maze_bin_rot = [[maze_bin[i][j] for i in range(len(maze_bin))] for j in range(len(maze_bin[0]))]
# horizontal_lengths is a two-dimensional list that contains the number of possible steps to the right for every cell.
horizontal_lengths = []
for line in maze_bin:
num = 0
line_lengths = []
for i in reversed(line):
line_lengths.append(i*num)
num = i * (num + i)
horizontal_lengths.append(tuple(reversed(line_lengths)))
# vertical_lengths is a two-dimensional list that contains the number of possible steps to the bottom for every cell.
vertical_lengths_rot = []
for line in maze_bin_rot:
num = 0
line_lengths = []
for i in reversed(line):
line_lengths.append(i*num)
num = i * (num + i)
vertical_lengths_rot.append(tuple(reversed(line_lengths)))
# do the rotation again to be back in normal coordinates
vertical_lengths = [[vertical_lengths_rot[i][j] for i in range(len(vertical_lengths_rot))] for j in range(len(vertical_lengths_rot[0]))]
# calculate the maximum size of a square that has it's upper left corner at (x, y).
# this is the minimum of the possible steps to the right and to the bottom.
max_possible_square = []
for y in range(len(maze_bin)):
line = []
for x in range(len(maze_bin[0])):
line.append(min(horizontal_lengths[y][x], vertical_lengths[y][x]))
max_possible_square.append(line)
# search for squares
results = []
max_size_square = (-1, -1, -1)
for y in range(len(max_possible_square)):
for x in range(len(max_possible_square[0])):
# start with maximum possible size and decrease size until a square is found.
for size in reversed(range(1, max_possible_square[y][x]+1)):
# look at the upper right (x+size,y) and bottom left corner (x,y+size).
# if it's possible to make at least size steps to the right from the bottom left corner
# and at least size steps to the bottom from the upper right corner, this is a valid square.
if horizontal_lengths[y+size][x] >= size and vertical_lengths[y][x+size] >= size:
results.append((x, y, size+1))
if size+1 > max_size_square[2]:
max_size_square = (x, y, size+1)
# break after the the largest square with upper left corner (x,y) has been found.
break
t2 = time.time()
# comment this print section if you use larger grids
print('Maze:')
pprint.pprint(maze_bin)
print('\n')
print('Horizontal possible steps:')
pprint.pprint(horizontal_lengths)
print('\n')
print('Vertical possible steps:')
pprint.pprint(vertical_lengths)
print('\n')
print('Maximum possible size of square:')
pprint.pprint(max_possible_square)
print('\n')
print('Results:')
for square in results:
print('Square: x={}, y={}, size={}'.format(*square))
print('\n')
# final results
print('Finished after {:.3f} seconds.'.format(t2-t1))
print('Result: {} squares found, maximum square (x={}, y={}, size={}).'.format(len(results), *max_size_square))
I hope this is what you were looking for. If you have any questions, just leave a comment below ;)
If we do not want to enumerate all results, one optimization that may be worth considering is the following. It's based on the strategy - "do not proceed further with this cell if it can not lead to the optimal solution"
for y in range(possible_y_value):
for x in range(possible_x_value):
# We are ready to process cell identified by (x,y).
# Check if max_possible_square_length at this cell is greater than size of best_result seen so far. If so, proceed further, otherwise skip this cell
if max_possible_square[y][x]+1 > best_result.size:
# proceed further with the inner most for loop
....
Even from within the inner most for loop, we can break out of the loop at the iteration when it falls below the best_result's size seen so far
I am implementing a Python version of the game Othello / Reversi. However, my algorithm seems to be having trouble when searching in the southwest direction.
Here are some important functions to understand how my current code works:
def _new_game_board(self)->[[str]]:
board = []
for row in range(self.rows):
board.append([])
for col in range(self.columns):
board[-1].append(0)
return board
def _is_valid_position(self, turn:list)->bool:
'''return true if the turn is a valid row and column'''
row = int(turn[0]) - 1
column = int(turn[1]) - 1
if row >= 0:
if row < self.rows:
if column >= 0:
if column < self.columns:
return True
else:
return False
def _is_on_board(self, row:int, col:int)->bool:
'''returns true is coordinate is on the board'''
if row >=0:
if row < self.rows:
if col >=0:
if col < self.columns:
return True
def _searchNorthEast(self)->None:
'''Search the board NorthEast'''
print("NorthEast")
row = self.move_row
column = self.move_column
should_be_flipped = list()
row += 1
column -= 1
if self._is_on_board(row, column):
print("column searching NorthEast on board")
if self.board[row][column] == self._opponent:
should_be_flipped.append([row, column])
while True:
row += 1
column -= 1
if self._is_on_board(row, column):
if self.board[row][column] == self._opponent:
should_be_flipped.append([row, column])
continue
elif self.board[row][column] == self.turn:
self._to_be_flipped.extend(should_be_flipped)
break
else:
break
else:
self._to_be_flipped.extend(should_be_flipped)
else:
pass
def _searchSouthWest(self)->None:
'''Search the board SouthWest'''
print("in SouthWest")
row = self.move_row
column = self.move_column
should_be_flipped = list()
row -= 1
column += 1
if self._is_on_board(row, column):
print("column searching SouthWest on board")
if self.board[row][column] == self._opponent:
should_be_flipped.append([row, column])
while True:
row -= 1
column += 1
if self._is_on_board(row, column):
if self.board[row][column] == self._opponent:
should_be_flipped.append([row, column])
continue
elif self.board[row][column] == self.turn:
self._to_be_flipped.extend(should_be_flipped)
break
else:
break
else:
self._to_be_flipped.extend(should_be_flipped)
else:
pass
def _move_is_valid(self, turn:list)->bool:
'''Verify move is valid'''
self._to_be_flipped = list()
self._opponent = self._get_opposite(self.turn)
if self._is_valid_position(turn):
self.move_row = int(turn[0]) - 1
self.move_column = int(turn[1]) - 1
self._searchRight()
self._searchLeft()
self._searchUp()
self._searchDown()
self._searchNorthWest()
self._searchNorthEast
self._searchSouthEast()
self._searchSouthWest()
if len(self._to_be_flipped) > 0:
return True
else:
return False
Now let's say the current board looks like the following:
. . . .
W W W .
. B B .
. B . .
Turn: B
and the player makes a move to row 1 column 4, it says invalid because it does not detect the white piece in row 2 column 3 to be flipped. All my other functions are written the same way. I can get it to work in every other direction except in this case.
Any ideas why it is not detecting the piece in this diagonal direction?
Don't Repeat Yourself. The _search* methods are extremely redundant which makes it difficult to see that the signs in
row -= 1
column += 1
are correct. Since you've given only two directions (NE, SW) and no documentation of the board orientation, I cannot tell if the signs agree with the board layout or even agree with themselves.
The _search* methods are also too long and should be divided into multiple functions, but that's a secondary concern.
I agree with msw about not repeating stuff. It is tempting to go ahead and do what you can once you see it, but generalizing will save you the headaches of debugging.
Here is some pseudocode that should give the general idea. I might not be able to finagle working with your code, but hopefully this shows how to reduce repetitive code. Note it doesn't matter if -1 is up or down. The board class is simply a 2x2 array of (open square/player 1's piece/player 2's piece,) along with whose turn it is to move.
# x_delta and y_delta are -1/0/1 each based on which of the up to 8 adjacent squares you are checking. Player_num is the player number.
def search_valid(x_candidate, y_candidate, x_delta, y_delta, board, player_num):
y_near = y_candidate + y_delta
x_near = x_candidate + x_delta
if x_near < 0 or x_near >= board_width:
return False
if y_near < 0 or y_near >= board_width:
return False # let's make sure we don't go off the board and throw an exception
if board.pieces[x_candidate+x_delta][y_candidate+y_delta] == 0:
return False #empty square
if board.pieces[x_candidate+x_delta][y_candidate+y_delta] == player_num:
return False #same color piece
return True #if you wanted to detect how many flips, you could use a while loop
Now a succinct function can loop this search_valid to see whether a move is legal or not e.g.
def check_valid_move(x_candidate, y_candidate, board, player_num):
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
if not x and not y:
continue # this is not a move. Maybe we don't strictly need this, since board.pieces[x_candidate+x_delta][y_candidate+y_delta] == player_num anyway, but it seems like good form.
if search_valid(x_candidate, y_candidate, dx, dy, board, player_num):
return True
return False
A similar function could actually flip all the opposing pieces, but that's a bit trickier. You'd need a while function inside the for loops. But you would not have to rewrite code for each direction.