EDIT 30/03/2021: Question was really poorly-worded, reformulating it
I implemented an Alpha-Beta Prunning algorithm in Python and I was wondering if it is normal for it not to go for the fastest victory route (sometimes it will go for a victory in 2 moves while it could have won in 1).
import math
from collections import Counter
from copy import copy, deepcopy
""" Board Class Definition """
class Board:
""" constructor """
def __init__(self):
# init data
self.data = [ "." for i in range(9) ]
""" copy constructor equivalent """
#staticmethod
def copy(board):
return deepcopy(board)
""" play at given coordinates """
def play_at(self, position, color):
# check if you can play
if self.data[position] == ".":
# make the move
self.data[position] = color
return True
# did not play
return False
""" get coordinates of empty pieces on the board """
def get_playable_coord(self):
# define coordinates of empty tiles
return [ i for i in range(9) if self.data[i] == "." ]
""" board is full """
def is_full(self):
# define tile counter
c = Counter( [ self.data[i] for i in range(9) ] )
return ( c["x"] + c["o"] == 9 )
""" get winner of the board """
def get_winner(self):
# straight lines to check
straightLines = [ (0, 1, 2) , (3, 4, 5) , (6, 7, 8) , (0, 3, 6) , (1, 4, 7) , (2, 5, 8) , (0, 4, 8) , (2, 4, 6) ]
# check straight lines - 8 in total
for i in range(8):
# get counter of line of tiles
c = Counter( [ self.data[j] for j in straightLines[i] ] )
# different scenarii
if c["x"] == 3:
return "x"
elif c["o"] == 3:
return "o"
# if board is full, game is a draw
if self.is_full():
return "draw"
# return None by default
return None
""" get heuristic value of board - for "x" if 'reverse' == False """
def get_heuristic_value(self, reverse):
# init variable
value = 0
# straight lines to check
straightLines = [ (0, 1, 2) , (3, 4, 5) , (6, 7, 8) , (0, 3, 6) , (1, 4, 7) , (2, 5, 8) , (0, 4, 8) , (2, 4, 6) ]
# check straight lines - 8 in total
for i in range(8):
# get counter of line of tiles
c = Counter( [ self.data[j] for j in straightLines[i] ] )
# different scenarii
if c["x"] == 3:
value += 100
elif c["x"] == 2 and c["."] == 1:
value += 10
elif c["x"] == 1 and c["."] == 2:
value += 1
elif c["o"] == 3:
value -= 100
elif c["o"] == 2 and c["."] == 1:
value -= 10
elif c["o"] == 1 and c["."] == 2:
value -= 1
# return heuristic value
if reverse:
return -value
else:
return value
""" Model Class Definition """
class Model:
""" constructor """
def __init__(self, color):
# define parameters
self.color = color
self.other = self.get_opponent(color)
# define board
self.board = Board()
# define winner
self.winner = None
# 'x' plays first
if self.other == "x":
self.make_ai_move()
""" get opponent """
def get_opponent(self, player):
if player == "x":
return "o"
return "x"
""" player makes a move in given position """
def make_player_move(self, pos):
if self.winner is None:
# get result of board method
res = self.board.play_at(pos, self.color)
# check end of game <?>
self.winner = self.board.get_winner()
if res and self.winner is None:
# make AI move
self.make_ai_move()
""" AI makes a move by using alphabeta pruning on all child nodes """
def make_ai_move(self):
# init variables
best, bestValue = None, - math.inf
for i in self.board.get_playable_coord():
# copy board as child
copie = Board.copy(self.board)
copie.play_at(i, self.other)
# use alpha beta && (potentially) register play
value = self.alphabeta(copie, 10, - math.inf, math.inf, False)
if value > bestValue:
best, bestValue = i, value
# play at best coordinates
self.board.play_at(best, self.other)
# check end of game <?>
self.winner = self.board.get_winner()
""" alpha beta function (minimax optimization) """
def alphabeta(self, node, depth, alpha, beta, maximizingPlayer):
# ending condition
if depth == 0 or node.get_winner() is not None:
return node.get_heuristic_value(self.other == "o")
# recursive part initialization
if maximizingPlayer:
value = - math.inf
for pos in node.get_playable_coord():
# copy board as child
child = Board.copy(node)
child.play_at(pos, self.other)
value = max(value, self.alphabeta(child, depth-1, alpha, beta, False))
# update alpha
alpha = max(alpha, value)
if alpha >= beta:
break
return value
else:
value = math.inf
for pos in node.get_playable_coord():
# copy board as child
child = Board.copy(node)
child.play_at(pos, self.color)
value = min(value, self.alphabeta(child, depth-1, alpha, beta, True))
# update beta
beta = min(beta, value)
if beta <= alpha:
break
return value
My conclusion on the question:
Alpha-Beta Pruning is a depth-first search algorithm, not a breadth-first search algorithm, so I think it is natural for it to pick the first route it finds no matter its depth, and not search for the quickest one...
I know it's not the answer to the question, but I would like to suggest perhaps simplier approach for AI tac-tac-toe player, which involves calculating whether the position is winning or losing. This will require considering all valid positions that may happen at any time in the game, but since the field is 3x3, number of valid positions is less than 3^9 = 19683 (every position is either 'x', 'o' or ' '). This is not a hard bound, since lots of positions are invalid from the game rules perspective. I suggest you start from here, because the algorithm you are talking about are mainly used in harder games where full search is infeasible.
Hence, all you need to do is to calculate winning/losing metric for every position once after you start the program and then make a decision in O(1). This is acceptable for 3x3 field, but perhaps not much more.
The general approach is described here: https://cp-algorithms.com/game_theory/games_on_graphs.html. In a nutshell you build a tree of possible moves, mark the leaves as winning or losing and work your way up by considering all children transitions (for example, if every transition lead to a winning position for the opposite player, the position in losing).
In case you understand russian, here is a link to the original page: http://e-maxx.ru/algo/games_on_graphs
P.S. I was also playing with this game at some point in the past and implementing this approach. Here is my repo in case you want to investigate: https://github.com/yuuurchyk/cpp_tic_tac_toe. Fair warning: it's written in C++ and the code is a bit ugly
Related
I'm making a very simple Python chess engine using the standard Python chess library with a very simple evaluation function; the sum of the total black piece weights (positive) plus the sum of the total white piece weights (negative). The engine always plays as black.
I used the Negamax Wikipedia page for guidance and the depth is to the fourth ply. I don't expect grandmaster performance, but the engine makes very questionable moves, for example: e2e4 and f1c4 for white causes the engine to freely give up it's pawn via b7b5.
Can anyone help me out? I'm completely lost as to what I did wrong. The negamax (called search) and the evaluation function is shown below:
import chess
import time
import math
from time import sleep
from chessboard import display
scoreMovePair = {}
def colorMap(color):
if color == True:
return -1
return 1
def pieceMap(pieceNum):
if pieceNum == 1:
return 1
elif pieceNum == 2:
return 3
elif pieceNum == 3:
return 3
elif pieceNum == 4:
return 5
elif pieceNum == 5:
return 9
return pieceNum
def posEval(board):
score = 0
for i in range(0, 64):
piece = board.piece_at(i)
if piece != None:
score = score + pieceMap(piece.piece_type)*colorMap(piece.color)
return score
def search(board, level, a, b, moveSet, color):
if level == 4:
score = posEval(board)
scoreMovePair[score] = moveSet[0]
return score*color
if board.is_checkmate():
return 1000*colorMap(board.turn)
value = -10000
for move in board.legal_moves:
board.push(move)
moveSet.append(move)
value = max(value, -search(board, level + 1, -b, -a, moveSet, -color))
a = max(a, value)
moveSet.pop()
board.pop()
if (a >= b):
break
return value
def main():
global scoreMovepair
board = chess.Board()
display.start(board.fen())
while not display.checkForQuit():
validMoves = list(board.legal_moves)
if len(validMoves) == 0:
break
else:
move = input("Enter move: ")
t0 = time.time()
move = str(move)
myMove = chess.Move.from_uci(move)
if myMove in validMoves:
board.push_san(move)
value = search(board, 0, -10000, 10000, [], 1)
move = scoreMovePair[value]
print(scoreMovePair)
print("FINAL -> "+str(value))
board.push(move)
print(board.fen())
display.update(board.fen())
sleep(1)
t1 = time.time()
print(t1-t0)
else:
continue
display.terminate()
if __name__ == "__main__":
main()
Just based on a first glance, I would say you may be missing a "quiescence search" (meaning a search for quietness). Also called "captures only search".
https://www.chessprogramming.org/Quiescence_Search
This is a search that is called instead of an evaluation function on your leaf nodes (nodes where max depth is reached). The search makes only capture moves until there are no more captures (with unlimited depth).
In short, without this search, whoever gets the last move in the search (determined by depth) will be able to do anything without consequences. This can lead to some weird results.
I have written a selection of functions to try and solve a N-puzzle / 8-puzzle.
I am quite content with my ability to manipulate the puzzle but am struggling with how to iterate and find the best path. My skills are not in OOP either and so the functions are simple.
The idea is obviously to reduce the heruistic distance and place all pieces in their desired locations.
I have read up a lot of other questions regarding this topic but they're often more advanced and OOP focused.
When I try and iterate through there are no good moves. I'm not sure how to perform the A* algorithm.
from math import sqrt, fabs
import copy as cp
# Trial puzzle
puzzle1 = [
[3,5,4],
[2,1,0],
[6,7,8]]
# This function is used minimise typing later
def starpiece(piece):
'''Checks the input of a *arg and returns either tuple'''
if piece == ():
return 0
elif isinstance(piece[0], (str, int)) == True:
return piece[0]
elif isinstance(piece[0], (tuple, list)) and len(piece[0]) == 2:
return piece[0]
# This function creates the goal puzzle layout
def goal(puzzle):
'''Input a nested list and output an goal list'''
n = len(puzzle) * len(puzzle)
goal = [x for x in range(1,n)]
goal.append(0)
nested_goal = [goal[i:i+len(puzzle)] for i in range(0, len(goal), len(puzzle))]
return nested_goal
# This fuction gives either the coordinates (as a tuple) of a piece in the puzzle
# or the piece in the puzzle at give coordinates
def search(puzzle, *piece):
'''Input a puzzle and piece value and output a tuple of coordinates.
If no piece is selected 0 is chosen by default. If coordinates are
entered the piece value at those coordinates are outputed'''
piece = starpiece(piece)
if isinstance(piece, (tuple, list)) == True:
return puzzle[piece[0]][piece[1]]
for slice1, sublist in enumerate(puzzle):
for slice2, item in enumerate(sublist):
if puzzle[slice1][slice2] == piece:
x, y = slice1, slice2
return (x, y)
# This function gives the neighbours of a piece at a given position as a list of coordinates
def neighbours(puzzle, *piece):
'''Input a position (as a tuple) or piece and output a list
of adjacent neighbours. Default are the neighbours to 0'''
length = len(puzzle) - 1
return_list = []
piece = starpiece(piece)
if isinstance(piece, tuple) != True:
piece = search(puzzle, piece)
if (piece[0] - 1) >= 0:
x_minus = (piece[0] - 1)
return_list.append((x_minus, piece[1]))
if (piece[0] + 1) <= length:
x_plus = (piece[0] + 1)
return_list.append((x_plus, piece[1]))
if (piece[1] - 1) >= 0:
y_minus = (piece[1] - 1)
return_list.append((piece[0], y_minus))
if (piece[1] + 1) <= length:
y_plus = (piece[1] + 1)
return_list.append((piece[0], y_plus))
return return_list
# This function swaps piece values of adjacent cells
def swap(puzzle, cell1, *cell2):
'''Moves two cells, if adjacent a swap occurs. Default value for cell2 is 0.
Input either a cell value or cell cooridinates'''
cell2 = starpiece(cell2)
if isinstance(cell1, (str, int)) == True:
cell1 = search(puzzle, cell1)
if isinstance(cell2, (str, int)) == True:
cell2 = search(puzzle, cell2)
puzzleSwap = cp.deepcopy(puzzle)
if cell1 == cell2:
print('Warning: no swap occured as both cell values were {}'.format(search(puzzle,cell1)))
return puzzleSwap
elif cell1 in neighbours(puzzleSwap, cell2):
puzzleSwap[cell1[0]][cell1[1]], puzzleSwap[cell2[0]][cell2[1]] = puzzleSwap[cell2[0]][cell2[1]], puzzleSwap[cell1[0]][cell1[1]]
return puzzleSwap
else:
print('''Warning: no swap occured as cells aren't adjacent''')
return puzzleSwap
# This function gives true if a piece is in it's correct position
def inplace(puzzle, p):
'''Ouputs bool on whether a piece is in it's correct position'''
if search(puzzle, p) == search(goal(puzzle), p):
return True
else:
return False
# These functions give heruistic measurements
def heruistic(puzzle):
'''All returns heruistic (misplaced, total distance) as a tuple. Other
choices are: heruistic misplaced, heruistic distance or heruistic list'''
heruistic_misplaced = 0
heruistic_distance = 0
heruistic_distance_total = 0
heruistic_list = []
for sublist in puzzle:
for item in sublist:
if inplace(puzzle, item) == False:
heruistic_misplaced += 1
for sublist in puzzle:
for item in sublist:
a = search(puzzle, item)
b = search(goal(puzzle), item)
heruistic_distance = int(fabs(a[0] - b[0]) + fabs(a[1] - b[1]))
heruistic_distance_total += heruistic_distance
heruistic_list.append(heruistic_distance)
return (heruistic_misplaced, heruistic_distance_total, heruistic_list)
def hm(puzzle):
'''Outputs heruistic misplaced'''
return heruistic(puzzle)[0]
def hd(puzzle):
'''Outputs total heruistic distance'''
return heruistic(puzzle)[1]
def hl(puzzle):
'''Outputs heruistic list'''
return heruistic(puzzle)[2]
def hp(puzzle, p):
'''Outputs heruistic distance at a given location'''
x, y = search(puzzle, p)[0], search(puzzle, p)[1]
return heruistic(puzzle)[2][(x * len(puzzle)) + y]
# This is supposted to iterate along a route according to heruistics but doesn't work
def iterMove(puzzle):
state = cp.deepcopy(puzzle)
while state != goal(puzzle):
state_hd = hd(state)
state_hm = hm(state)
moves = neighbours(state)
ok_moves = []
good_moves = []
for move in moves:
maybe_state = swap(state, move)
if hd(maybe_state) < state_hd and hm(maybe_state) < state_hm:
good_moves.append(move)
elif hd(maybe_state) < state_hd:
ok_moves.append(move)
elif hm(maybe_state) < state_hm:
ok_moves.append(move)
if good_moves != []:
print(state)
state = swap(state, good_moves[0])
elif ok_moves != []:
print(state)
state = swap(state, ok_moves[0])
>> iterMove(puzzle1)
'no good moves'
To implement A* in Python you can use https://docs.python.org/3/library/heapq.html for a priority queue. You put possible positions into the queue with a priority of "cost so far + heuristic for remaining cost". When you take them out of the queue you check a set of already seen positions. Skip this one if you've seen the position, else add it to the set and then process.
An untested version of the critical piece of code:
queue = [(heuristic(starting_position), 0, starting_position, None)]
while 0 < len(queue):
(est_moves, cur_moves, position, history) = heapq.heappop(queue)
if position in seen:
continue
elif position = solved:
return history
else:
seen.add(position)
for move in possible_moves(position):
next_position = position_after_move(position, move)
est_moves = cur_moves + 1 + heuristic(next_position)
heapq.heappush(queue,
(est_moves, cur_moves+1,
next_position, (move, history)))
return None
I'm trying to implement the minimax algorithm in my tic tac toe game. I watched several videos, analysed multiple programs with minimax algorithm and I think I do know how it works now. My program is working but it seems like the algorithm has no clue what he is doing. It outputs pads on the board but it doesn't block me or tries to win. Like it's random. It would be nice if someone could have a look at my minimax algorithm and tell what's wrong! It would also be nice to tell me whats wrong with my explanation and don't just downvote.
from copy import deepcopy
class Board:
def __init__(self, board=None):
self.winning_combos = (
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6])
if board is not None:
self.board = board
else:
self.board = [None for i in range(9)]
def check_combos(self):
""" checks every combo if its used """
for symbol in ['X', 'O']:
for win_comb in self.winning_combos:
sum = 0
for field in win_comb:
if self.board[field] == symbol:
sum += 1
if sum == 3:
return symbol
return None
def complete(self):
""" check if the game is complete, caused by win or draw """
cc = self.check_combos()
if cc is not None:
return cc
if len(self.empty_pads()) <= 0:
return "DRAW"
return False
def show(self):
""" print board """
print(str(self.board[0:3]) + "\n" +
str(self.board[3:6]) + "\n" +
str(self.board[6:9]))
def empty_pads(self):
""" returns list with indexes of every unused/empty field/pad """
list = []
for pad in range(len(self.board)):
if self.board[pad] is None:
list.append(pad)
return list
def set(self, position, player):
""" sets the players symbol on the given position """
self.board[position] = player
def copy(self):
return deepcopy(self)
def get_enemy_player(player):
if player == 'X':
return 'O'
return 'X'
def get_player_value(player):
""" X = max, O = min """
if player == 'X':
return 1
else:
return -1
def get_player_by_value(value):
if value == -1:
return "O"
elif value == 1:
return "X"
else:
return "NONE"
def max_v(node):
if node.depth == 0 or node.board.complete():
return get_player_value(node.board.complete())
bestVal = -100
for child in node.children:
v = minimax(child)
if v >= bestVal:
bestVal = v
node.bestmove = child.move
return bestVal
def min_v(node):
if node.depth == 0 or node.board.complete():
return get_player_value(node.board.complete())
bestVal = 100
for child in node.children:
v = minimax(child)
if v <= bestVal:
bestVal = v
node.bestmove = child.move
return bestVal
def minimax(node):
if node.depth == 0 or node.board.complete():
return get_player_value(node.board.complete())
if get_player_value(node.player) == 1:
return max_v(node)
elif get_player_value(node.player) == -1:
return min_v(node)
class Node:
def __init__(self, depth, player, board, pad):
self.depth = depth
self.player = player
self.board = board
self.move = pad
self.board.set(pad, self.player)
self.bestmove = int
self.children = []
self.CreateChildren()
def CreateChildren(self):
if self.depth > 0 and not self.board.complete():
for index in self.board.empty_pads():
board = self.board.copy()
self.children.append(Node(self.depth - 1, get_enemy_player(self.player), board, index))
if __name__ == "__main__":
board = Board()
board.show()
while not board.complete():
player = 'X'
player_move = int(input('Move: ')) - 1
if player_move not in board.empty_pads():
continue
board.set(player_move, player)
board.show()
if board.complete():
break
player = get_enemy_player(player)
node = Node(9, player, board.copy(), player_move)
minmax = minimax(node)
print(node.bestmove+1)
for child in node.children:
print("move: " + str(child.move + 1) + " --> " + get_player_by_value(minmax) + " win")
board.set(node.bestmove, player)
board.show()
print(board.complete())
PS: I do know why the "moves: " ouput is always the same, but that's not the point.
I see multiple issues in your program.
As for your actual question: Your program acts as if the computer does not distinguish between a loss for it and a draw. Nowhere in your code can I find you assigning a value of 0 for a draw, while it appears you assign 1 for a win and -1 for a loss. Your code should prefer a draw to a loss but it sees no difference. That is why it looks "Like it's random". My analysis here may be off, but the following issues explain why it is difficult for me to tell.
Your style should be improved, to improve readability and ease of maintenance and to avoid bugs. Your code is much too difficult for me to understand, partly because...
You have far too few comments for anyone other than you to understand what the code is trying to do. In a few months you will not be able to remember, so write it down in the code.
You have too many blank lines, violating PEP8 and making harder to see much code on the screen.
You do not take your output seriously enough, as shown when you say "ou[t]put is always the same, but that's not the point." It is hard for anyone, including you, to tell what is happening in your code without good output. Work on that, and add some temporary print or logging statements that tell you more about what is happening inside.
Some of your routines return values of varying types. The complete() function sometimes returns the string "DRAW", sometimes the Boolean False, and sometimes a value from self.check_combos(), whatever type that is. Your routines max_v() and min_v() sometimes return a string value from get_player_value() and sometimes an integer from variable bestVal.
I'm trying to do a snake game, where 2 snakes compete between each other. One snake simply follows the food, and avoids obstacles, the other, is the one, for which i'm writing the code, and is supposed to find the best way to get to the food. The food position, every bit of the map and the position of the other snake is known, and the position of the food changes, with every movement of the snakes.
If the map allows it, if there is no obstacle, the snake can traverse through the walls, to go to the other side of the map, like the map is a donut. The snake doesn't move diagonally, only vertically and horizontally, and it can't move backwards.
I'm using jump point search to find a way to the food, and it's working fine, although at 50fps some times the game slows down a bit.
The major problem i'm having, is finding a way to avoid dead ends. If the food gets in a dead end, i want to wait that it leaves the dead end, but what happens is that my snake, goes there, and then dies. Because i'm not avoiding dead ends, when my snake get's big enough, sometimes it crashes in its own body.
This is the code of the agent of my snake.
class AgentStudent(Snake, SearchDomain):
def __init__(self, body=[(0, 0)], direction=(1, 0), name="punkJD"):
super().__init__(body, direction, name=name)
self.count = 0;
#given the current state, and the next state, it returns a direction ( (1,0), (-1,0), (0,1), (0,-1) )
def dir(self, state, n_state):
if state[0] == 0 and n_state[0] == (self.mapsize[0] - 1):
return left
elif state[0] == (self.mapsize[0] - 1) and n_state[0] == 0:
return right
elif state[1] == 0 and n_state[1] == (self.mapsize[1] - 1):
return up
elif state[1] == (self.mapsize[1] - 1) and n_state == 0:
return down
return n_state[0] - state[0], n_state[1] - state[1]
#doesn't matter for the question
def update(self, points=None, mapsize=None, count=None, agent_time=None):
self.mapsize = mapsize
return None
#given current position and food position, it will create a class that will do the search. Seach code bellow
def search_food(self, pos, foodpos):
prob = SearchProblem(self, pos, foodpos, self.olddir)
my_tree = SearchTree(prob, self.mapsize, self.maze)
#doesn't matter, before i was using A*, but then i changed my whole search class
my_tree.strategy = 'A*'
return my_tree.search()
#given the current position and the direction the snake is faced it returns a list of all the possible directions the snake can take. If the current direction is still possible it will be put first in the list to be the first to be considered
def actions(self, pos, dir):
dirTemp = dir
invaliddir = [x for (x, y) in self.complement if y == dir]
validdir = [dir for dir in directions if not (dir in invaliddir)]
validdir = [dir for dir in validdir if
not (self.result(pos, dir) in self.maze.obstacles or self.result(pos, dir) in self.maze.playerpos)]
dirList = [dirTemp] if dirTemp in validdir else []
if dirList != []:
for a in range(len(validdir)):
if validdir[a] != dirTemp:
dirList.append(validdir[a])
return dirList
return validdir
#given the current position and the current direction, it returns the new position
def result(self, a, b):
n_pos = a[0] + b[0], a[1] + b[1]
if n_pos[0] == -1:
n_pos = (self.mapsize[0] - 1), a[1] + b[1]
if n_pos[1] == -1:
n_pos = a[0] + b[0], (self.mapsize[1] - 1)
if n_pos[0] == (self.mapsize[0]):
n_pos = 0, a[1] + b[1]
if n_pos[1] == (self.mapsize[1]):
n_pos = a[0] + b[0], 0
return n_pos
#given the current position and food position it returns the manhattan distance heuristic
def heuristic(self, position, foodpos):
distancex = min(abs(position[0] - foodpos[0]), self.mapsize[0] - abs(position[0] - foodpos[0]))
distancey = min(abs(position[1] - foodpos[1]), self.mapsize[1] - abs(position[1] - foodpos[1]))
return distancex + distancey
#this function is called by the main module of the game, to update the position of the snake
def updateDirection(self, maze):
# this is the brain of the snake player
self.olddir = self.direction
position = self.body[0]
self.maze = maze
# new direction can't be up if current direction is down...and so on
self.complement = [(up, down), (down, up), (right, left), (left, right)]
self.direction = self.search_food(position, self.maze.foodpos)
Bellow is the code to do the search.
I reused a file i had with some classes to do a tree search, and changed it to use jump point search. And for every jump point i find i expand a node in the tree.
class SearchDomain:
def __init__(self):
abstract
def actions(self, state):
abstract
def result(self, state, action):
abstract
def cost(self, state, action):
abstract
def heuristic(self, state, goal_state):
abstract
class SearchProblem:
def __init__(self, domain, initial, goal,dir):
self.domain = domain
self.initial = initial
self.goal = goal
self.dir = dir
def goal_test(self, state):
return state == self.goal
# class that defines the nodes in the tree. It has some attributes that are not used due to my old aproach.
class SearchNode:
def __init__(self,state,parent,heuristic,dir,cost=0,depth=0):
self.state = state
self.parent = parent
self.heuristic = heuristic
self.depth = depth
self.dir = dir
self.cost = cost
if parent!=None:
self.cost = cost + parent.cost
def __str__(self):
return "no(" + str(self.state) + "," + str(self.parent) + "," + str(self.heuristic) + ")"
def __repr__(self):
return str(self)
class SearchTree:
def __init__(self,problem, mapsize, maze, strategy='breadth'):
#attributes used to represent the map in a matrix
#represents obstacle
self.OBS = -1
#represents all the positions occupied by both snakes
self.PPOS = -2
#represents food position
self.FOODPOS = -3
#represents not explored
self.UNIN = -4
self.problem = problem
h = self.problem.domain.heuristic(self.problem.initial,self.problem.goal)
self.root = SearchNode(problem.initial, None,h,self.problem.dir)
self.open_nodes = [self.root]
self.strategy = strategy
self.blacklist = []
self.pqueue = FastPriorityQueue()
self.mapa = maze
#here i initialize the matrix to represent the map
self.field = []
for a in range(mapsize[0]):
self.field.append([])
for b in range(mapsize[1]):
self.field[a].append(self.UNIN)
for a,b in maze.obstacles:
self.field[a][b] = self.OBS
for a,b in maze.playerpos:
self.field[a][b] = self.PPOS
self.field[maze.foodpos[0]][maze.foodpos[1]] = self.FOODPOS
self.field[self.root.state[0]][self.root.state[1]] = self.UNIN
#function to add a jump point to the priority queue
def queue_jumppoint(self,node):
if node is not None:
self.pqueue.add_task(node, self.problem.domain.heuristic(node.state,self.problem.goal)+node.cost)
# given a node it returns the path until the root of the tree
def get_path(self,node):
if node.parent == None:
return [node]
path = self.get_path(node.parent)
path += [node]
return(path)
#Not used in this approach
def remove(self,node):
if node.parent != None:
a = self.problem.domain.actions(node.parent.state, node.dir)
self.blacklist+=node.state
if a == []:
self.remove(node.parent)
node = None
#Function that searches for the food
def search(self):
tempNode = self.root
self.queue_jumppoint(self.root)
count = 0
while not self.pqueue.empty():
node = self.pqueue.pop_task()
actions = self.problem.domain.actions(node.state,node.dir)
if count == 1:
tempNode = node
count+=1
#for every possible direction i call the explore function that finds a jump point in a given direction
for a in range(len(actions)):
print (a)
print (actions[a])
jumpPoint = self.explore(node,actions[a])
if jumpPoint != None:
newnode = SearchNode((jumpPoint[0],jumpPoint[1]),node,self.problem.domain.heuristic(node.state,self.problem.goal),actions[a],jumpPoint[2])
if newnode.state == self.problem.goal:
return self.get_path(newnode)[1].dir
self.queue_jumppoint(newnode)
dirTemp = tempNode.dir
return dirTemp
#Explores the given direction, starting in the position of the given node, to find a jump point
def explore(self,node,dir):
pos = node.state
cost = 0
while (self.problem.domain.result(node.state,dir)) != node.state:
pos = self.problem.domain.result(pos, dir)
cost += 1
#Marking a position as explored
if self.field[pos[0]][pos[1]] == self.UNIN or self.field[pos[0]][pos[1]] == self.PPOS:
self.field[pos[0]][pos[1]] = 20
elif pos[0] == self.problem.goal[0] and pos[1] == self.problem.goal[1]: # destination found
return pos[0],pos[1],cost
else:
return None
#if the snake is going up or down
if dir[0] == 0:
#if there is no obstacle/(or body of any snake) at the right but in the previous position there was, then this is a jump point
if (self.field [self.problem.domain.result(pos,(1,0))[0]] [pos[1]] != self.OBS and self.field [self.problem.domain.result(pos,(1,0))[0]] [self.problem.domain.result(pos,(1,-dir[1]))[1]] == self.OBS) or \
(self.field [self.problem.domain.result(pos,(1,0))[0]] [pos[1]] != self.PPOS and self.field [self.problem.domain.result(pos,(1,0))[0]] [self.problem.domain.result(pos,(1,-dir[1]))[1]] == self.PPOS):
return pos[0], pos[1],cost
#if there is no obstacle/(or body of any snake) at the left but in the previous position there was, then this is a jump point
if (self.field [self.problem.domain.result(pos,(-1,0))[0]] [pos[1]] != self.OBS and self.field [self.problem.domain.result(pos,(-1,0))[0]] [self.problem.domain.result(pos,(1,-dir[1]))[1]] == self.OBS) or \
(self.field [self.problem.domain.result(pos,(-1,0))[0]] [pos[1]] != self.PPOS and self.field [self.problem.domain.result(pos,(-1,0))[0]] [self.problem.domain.result(pos,(1,-dir[1]))[1]] == self.PPOS):
return pos[0], pos[1],cost
#if the snake is going right or left
elif dir[1] == 0:
#if there is no obstacle/(or body of any snake) at the upper part but in the previous position there was, then this is a jump point
if (self.field [pos[0]][self.problem.domain.result(pos,(1,1))[1]] != self.OBS and self.field [self.problem.domain.result(pos,(-dir[0],dir[1]))[0]] [self.problem.domain.result(pos,(1,1))[1]] == self.OBS) or \
(self.field [pos[0]][self.problem.domain.result(pos,(1,1))[1]] != self.PPOS and self.field [self.problem.domain.result(pos,(-dir[0],dir[1]))[0]] [self.problem.domain.result(pos,(1,1))[1]] == self.PPOS):
return pos[0], pos[1],cost
#if there is no obstacle/(or body of any snake) at the down part but in the previous position there was, then this is a jump point
if (self.field [pos[0]] [self.problem.domain.result(pos,(-1,-1))[1]] != self.OBS and self.field [self.problem.domain.result(pos,(-dir[0],dir[1]))[0]] [self.problem.domain.result(pos,(-1,-1))[1]] == self.OBS) or \
(self.field [pos[0]] [self.problem.domain.result(pos,(-1,-1))[1]] != self.PPOS and self.field [self.problem.domain.result(pos,(-dir[0],dir[1]))[0]] [self.problem.domain.result(pos,(-1,-1))[1]] == self.PPOS):
return pos[0], pos[1],cost
#if the food is aligned in some way with the snake head, then this is a jump point
if (pos[0] == self.mapa.foodpos[0] and node.state[0] != self.mapa.foodpos[0]) or \
(pos[1] == self.mapa.foodpos[1] and node.state[1] != self.mapa.foodpos[1]):
return pos[0], pos[1],cost
#if the food is in front of the head of the snake, right next to it, then this is a jump point
if self.field[self.problem.domain.result(pos,(dir[0],dir[1]))[0]][self.problem.domain.result(pos,(1,dir[1]))[1]] == self.FOODPOS:
return pos[0], pos[1],cost
##if an obstacle is in front of the head of the snake, right next to it, then this is a jump point
if self.field[self.problem.domain.result(pos,(dir[0],dir[1]))[0]][ self.problem.domain.result(pos,(1,dir[1]))[1]] == self.OBS:
return pos[0], pos[1],cost
return None
class FastPriorityQueue:
def __init__(self):
self.pq = [] # list of entries arranged in a heap
self.counter = 0 # unique sequence count
def add_task(self, task, priority=0):
self.counter+=1
entry = [priority, self.counter, task]
heapq.heappush(self.pq, entry)
def pop_task(self):
while self.pq:
priority, count, task = heapq.heappop(self.pq)
return task
raise KeyError('pop from an empty priority queue')
def empty(self):
return len(self.pq) == 0
This is my code. I would appreciate any help to be able to avoid dead ends.
I searched for similar problems but couldn't find any that helped me.
StackOverflow is not a coding service, so I'm not going to write your code for you, but I can most definitely tell you what steps you would need to take to solve your issue.
In your comments, you said it would be nice if you could check for dead ends before the game starts. A dead end can be classified as any point that has three or more orthogonally adjacent walls. I'm assuming you want every point leading up to a dead end that is inescapable. Here is how you would check:
Check every point starting from one corner and moving to the other, in either rows or columns, it doesn't matter. Once you reach a point that has three or more orthogonally adjacent walls, mark that point as a dead end, and go to 2.
Find the direction of the empty space next to this point (if any), and check every point in that direction. For each of those points: if it has two or more adjacent walls, mark it as a dead end. If it has only one wall, go to 3. If it has no walls, stop checking in this direction and continue with number 1.
In every direction that does not have a wall, repeat number 2.
Follow these steps until step 1 has checked every tile on the grid.
If you need a programming example, just ask for one in the comments. I didn't have time to make one, but I can make one later if needed. Also, if you need extra clarification, just ask!
So, I have an assignment which asks me to solve a maze using recursion. I will post the assignment guidelines so you can see what I am talking about. The professor didn't explain recursion that much, he gave us examples of recursion, which I will post, but I was hoping someone might be able to give me a more in depth explanation of the recursion, and how I would apply this to solving a maze. I'm not asking for anyone to write the code, I'm just hoping some explanations would put me on the right path. Thank you to anyone who answers.
Here are the examples I have:
def foo():
print("Before")
bar()
print("After")
def bar():
print("During")
def factorial(n):
"""n!"""
product = 1
for i in range(n,0,-1):
product *= i
return product
def recFac(n):
"""n! = n * (n-1)!"""
if(n == 1):
return 1
return n * recFac(n-1)
def hello():
"""Stack overflow!"""
hello()
def fib(n):
"""f(n) = f(n-1) + f(n-2)
f(0) = 0
f(1) = 1"""
if n == 0 or n == 1: #base case
return n
return fib(n-1) + fib(n-2) #recursive case
def mult(a,b):
"""a*b = a + a + a + a ..."""
#base case
if (b == 1):
return a
#recursive case
prod = mult(a,b-1)
prod *= a
return prod
def exp(a,b):
"""a ** b = a* a * a * a * a *.... 'b times'"""
#base case
if (b==0):
return 1
if (b == 1):
return a
#recursive case
return exp(a,b-1)*a
def pallindrome(word):
"""Returns True if word is a pallindrome, False otherwise"""
#base case
if word == "" or len(word)==1:
return True
#recursive case
if word[0] == word[len(word)-1]:
word = word[1:len(word)-1]
return pallindrome(word)
else:
return False
Here are the guidelines:
You are going to create a maze crawler capable of solving any maze you give it with the power of recursion!
Question 1 - Loading the maze
Before you can solve a maze you will have to load it. For this assignment you will use a simple text format for the maze. You may use this sample maze or create your own.
Your objective for this question is to load any given maze file, and read it into a 2-dimensional list.
E.g.: loadMaze("somemaze.maze") should load the somemaze.maze file and create a list like the following...
[['#','#','#','#','#','#','#','#','#'],
['#','S','#',' ',' ',' ','#','E','#'],
['#',' ','#',' ','#',' ',' ',' ','#'],
['#',' ',' ',' ','#',' ','#',' ','#'],
['#', #','#','#','#','#','#','#','#']]
Note that the lists have been stripped of all '\r' and '\n' characters. In order to make the next question simpler you may make this list a global variable.
Next write a function that prints out the maze in a much nicer format:
E.g.,
####################################
#S# ## ######## # # # # #
# # # # # # #
# # ##### ## ###### # ####### # #
### # ## ## # # # #### #
# # # ####### # ### #E#
####################################
Test your code with different mazes before proceeding.
Question 2 - Preparing to solve the maze
Before you can solve the maze you need to find the starting point! Add a function to your code called findStart() that will search the maze (character-by-character) and return the x and y coordinate of the 'S' character. You may assume that at most one such character exists in the maze. If no 'S' is found in the maze return -1 as both the x and y coordinates.
Test your code with the 'S' in multiple locations (including no location) before proceeding.
Question 3 - Solving the maze!
Finally, you are ready to solve the maze recursively! Your solution should only require a single method: solve(y,x)
A single instance of the solve method should solve a single location in your maze. The parameters y and x are the current coordinates to be solved. There are a few things your solve method should accomplish. It should check if it is currently solving the location of the 'E'. In that case your solve method has completed successfully. Otherwise it should try to recursively solve the space to the right. Note, your method should only try to solve spaces, not walls ('#'). If that recursion doesn't lead to the end, then try down, then left, and up. If all that fails, your code should backtrack a step, and try another direction.
Lastly, while solving the maze, your code should leave indicators of its progress. If it is searching to the right, the current location should have a '>' in place of the empty space. If searching down put a 'v'. If searching left '<', and if searching up '^'. If your code has to backtrack remove the direction arrow, and set the location back to a ' '.
Once your maze is solved print out the maze again. You should a see step-by-step guide to walking the maze.
E.g.,
main("somemaze.maze")
#########
#S# #E#
# # # #
# # # #
#########
S is at (1,1)
#########
#S#>>v#E#
#v#^#>>^#
#>>^# # #
#########
Test your code with different different start and end locations, and optionally over a variety of mazes.
Here is the code I have so far:
But the code is not actually printing the track in the maze, and I'm not sure why.
def loadMaze():
readIt = open('Maze.txt', 'r')
readLines = readIt.readlines()
global mazeList
mazeList = [list(i.strip()) for i in readLines]
def showMaze():
for i in mazeList:
mazeprint = ''
for j in i:
mazeprint = mazeprint + j
print(mazeprint)
print('\n')
def solve(x,y, mazeList):
mazeList[x][y] = "o"
#Base case
if y > len(mazeList) or x > len(mazeList[y]):
return False
if mazeList[y][x] == "E":
return True
if mazeList[y][x] != " ":
return False
#marking
if solve(x+1,y) == True: #right
mazeList[x][y]= '>'
elif solve(x,y+1) == True: #down
mazeList[x][y]= 'v'
elif solve(x-1,y) == True: #left
mazeList[x][y]= '<'
elif solve(x,y-1) == True: #up
mazeList[x][y]= '^'
else:
mazeList[x][y]= ' '
return (mazeList[x][y]!= ' ')
(Dating myself, I actually did this problem in COBOL, in high-school.)
You can think of solving the maze as taking steps.
When you take a step, the same rules apply every time. Because the same rules apply every time, you can use the exact same set of instructions for each step. When you take a step, you just call the same routine again, changing the parameters to indicate the new step. That's recursion. You break the problem down by taking it one step at a time.
Note: Some recursion solutions break the problem in half, solving each half independent of the other, that works when the two solutions are actually independent. It doesn't work here because each step (solution) depends on the previous steps.
If you hit a dead end, you back out of the dead end, until you find a step where there are still viable squares to check.
Helpful Hint: You don't mark the correct path on the way to the exit, because you don't know that the step you're taking right now is part of the path to the exit. You mark the path on the way back, when you know that each step is indeed part of the path. You can do this because each step remembers which square it was in before it took the next step.
Instead, you put a mark in each square you've tried that only says: I've been here, no need to check this one again. Clean those up before you print the solution.
Here is my solution of CodeEval's The Labirynth challenge:
import sys
sys.setrecursionlimit(5000)
class Maze(object):
FLOOR = ' '
WALLS = '*'
PATH = '+'
def __init__(self):
self.cols = 0
self.rows = 0
self.maze = []
def walk_forward(self, current_k, r, c):
self.maze[r][c] = current_k
next_k = current_k + 1
# up
if r > 1:
up = self.maze[r - 1][c]
if up != self.WALLS:
if up == self.FLOOR or int(up) > current_k:
self.walk_forward(next_k, r - 1, c)
# down
if r < self.rows - 1:
down = self.maze[r + 1][c]
if down != self.WALLS:
if down == self.FLOOR or int(down) > current_k:
self.walk_forward(next_k, r + 1, c)
# left
if c > 1:
left = self.maze[r][c - 1]
if left != self.WALLS:
if left == self.FLOOR or int(left) > current_k:
self.walk_forward(next_k, r, c - 1)
# right
if c < self.cols - 1:
right = self.maze[r][c + 1]
if right != self.WALLS:
if right == self.FLOOR or int(right) > current_k:
self.walk_forward(next_k, r, c + 1)
def walk_backward(self, r, c):
current_k = self.maze[r][c]
if not isinstance(current_k, int):
return False
self.maze[r][c] = self.PATH
up = self.maze[r - 1][c] if r > 0 else None
down = self.maze[r + 1][c] if r < self.rows - 1 else None
left = self.maze[r][c - 1] if c > 1 else None
right = self.maze[r][c + 1] if c < self.cols else None
passed = False
if up and isinstance(up, int) and up == current_k - 1:
self.walk_backward(r - 1, c)
passed = True
if down and isinstance(down, int) and down == current_k - 1:
self.walk_backward(r + 1, c)
passed = True
if left and isinstance(left, int) and left == current_k - 1:
self.walk_backward(r, c - 1)
passed = True
if right and isinstance(right, int) and right == current_k - 1:
self.walk_backward(r, c + 1)
def cleanup(self, cleanup_path=False):
for r in range(0, self.rows):
for c in range(0, self.cols):
if isinstance(self.maze[r][c], int):
self.maze[r][c] = self.FLOOR
if cleanup_path and self.maze[r][c] == self.PATH:
self.maze[r][c] = self.FLOOR
def solve(self, start='up', show_path=True):
# finding start and finish points
upper = lower = None
for c in range(0, self.cols):
if self.maze[0][c] == self.FLOOR:
upper = (0, c)
break
for c in range(0, self.cols):
if self.maze[self.rows - 1][c] == self.FLOOR:
lower = (self.rows - 1, c)
break
if start == 'up':
start = upper
finish = lower
else:
start = lower
finish = upper
self.cleanup(cleanup_path=True)
self.walk_forward(1, start[0], start[1])
length = self.maze[finish[0]][finish[1]]
if not isinstance(length, int):
length = 0
if show_path:
self.walk_backward(finish[0], finish[1])
self.cleanup(cleanup_path=False)
else:
self.cleanup(cleanup_path=True)
return length
def save_to_file(self, filename):
with open(filename, 'w') as f:
f.writelines(str(self))
def load_from_file(self, filename):
self.maze = []
with open(filename, 'r') as f:
lines = f.readlines()
for line in lines:
row = []
for c in line.strip():
row.append(c)
self.maze.append(row)
self.rows = len(self.maze)
self.cols = len(self.maze[0]) if self.rows > 0 else 0
def get_maze(self):
return copy.copy(self.maze)
def __str__(self):
as_string = u''
for row in self.maze:
as_string += u''.join([str(s)[-1] for s in row]) + "\n"
return as_string
maze = Maze()
maze.load_from_file(sys.argv[1])
maze.solve(show_path=True)
print str(maze)
import os
class Maze_Crawler:
def __init__(self):
self.maze = []
def load_maze(self, path):
rows = []
with open(path, 'r') as f:
rows = f.readlines()
for i in range(len(rows)):
self.maze.append([])
for j in range(len(rows[i])-1):
self.maze[i].append(rows[i][j])
return self.maze
def get_start_coor(self):
for i in range(len(self.maze)):
for j in range(len(self.maze[i])):
if self.maze[i][j] == 'S':
return i, j
return -1, -1
def solve_maze(self, coor):
x, y = coor
if self.maze[x][y] == '#' or self.maze[x][y] == 'X':
return False
if self.maze[x][y] == 'E':
return True
if self.maze[x][y] != 'S':
self.maze[x][y] = 'X'
if self.solve_maze((x+1, y)):
if self.maze[x][y] != 'S':
self.maze[x][y] = 'v'
elif self.solve_maze((x-1, y)):
if self.maze[x][y] != 'S':
self.maze[x][y] = '^'
elif self.solve_maze((x, y+1)):
if self.maze[x][y] != 'S':
self.maze[x][y] = '>'
elif self.solve_maze((x, y-1)):
if self.maze[x][y] != 'S':
self.maze[x][y] = '<'
else:
return False
return True
def show_solution(self):
for i in range(len(self.maze)):
r = ''
for j in range(len(self.maze[i])):
if self.maze[i][j] == 'X':
r += ' '
else:
r += self.maze[i][j]
print(r)
Maze solving with python shows my answer. However, if you want to do the code yourself the steps are.
1. Start at the entrance.
2. Call the function solve(x,y) with the entrance co-ordinates
3. in solve, return false if the input point has already been handled or is a wall.
4. Mark the current point as handled (tag = 'o')
5. go to the right and call solve on that point. If it returns true, set tag to '>'
6 elif do the same for left and '<'
7 elif do the same for up and '^'
8 elif do the same for down and 'v'
9 else this is a false path, set tag = ' '
10 set the current maze point to tag
11 return (tag != ' ')
Alternatively leave step 9 out and make step 11
return(tag != 'o')
Then search through the maze and replace every 'o' with ' '
You can display the maze both ways so that it will show how you tried to solve it as well as the final answer. This has been used as a Solaris screensaver with the potential paths showing in one color and the actual path in a different color so that you can see it trying and then succeeding.
Recursion is actually a simple idea: to solve a problem, you shrink the problem by one step, then solve the reduced problem. This continues until you reach a "base problem" that you know how to solve completely. You return the base solution, then add to the solution returned at each step until you have the full solution.
So to solve n!, we remember n and solve for (n-1)!. The base case is 1!, for which we return 1; then at each return step we multiply by the remembered number (2 * 1! is 2, 3 * 2! is 6, 4 * 3! is 24, 5 * 4! is 120) until we multiply by n and have the full solution. This is actually a pretty pale and anemic sort of recursion; there is only one possible decision at each step. Known as "tail recursion", this is very easy to turn inside-out and convert to an iterative solution (start at 1 and multiply by each number up to n).
A more interesting sort of recursion is where you split the problem in half, solve each half, then combine the two half-solutions; for example quicksort sorts a list by picking one item, dividing the list into "everything smaller than item" and "everything bigger than item", quicksorting each half, then returning quicksorted(smaller) + item + quicksorted(larger). The base case is "when my list is only one item, it is sorted".
For the maze, we are going to split the problem four ways - all solutions possible if I go right, left, up, and down from my current location - with the special feature that only one of the recursive searches will actually find a solution. The base case is "I am standing on E", and a failure is "I am in a wall" or "I am on a space I have already visited".
Edit: for interest's sake, here is an OO solution (compatible with both Python 2.x and 3.x):
from collections import namedtuple
Dir = namedtuple("Dir", ["char", "dy", "dx"])
class Maze:
START = "S"
END = "E"
WALL = "#"
PATH = " "
OPEN = {PATH, END} # map locations you can move to (not WALL or already explored)
RIGHT = Dir(">", 0, 1)
DOWN = Dir("v", 1, 0)
LEFT = Dir("<", 0, -1)
UP = Dir("^", -1, 0)
DIRS = [RIGHT, DOWN, LEFT, UP]
#classmethod
def load_maze(cls, fname):
with open(fname) as inf:
lines = (line.rstrip("\r\n") for line in inf)
maze = [list(line) for line in lines]
return cls(maze)
def __init__(self, maze):
self.maze = maze
def __str__(self):
return "\n".join(''.join(line) for line in self.maze)
def find_start(self):
for y,line in enumerate(self.maze):
try:
x = line.index("S")
return y, x
except ValueError:
pass
# not found!
raise ValueError("Start location not found")
def solve(self, y, x):
if self.maze[y][x] == Maze.END:
# base case - endpoint has been found
return True
else:
# search recursively in each direction from here
for dir in Maze.DIRS:
ny, nx = y + dir.dy, x + dir.dx
if self.maze[ny][nx] in Maze.OPEN: # can I go this way?
if self.maze[y][x] != Maze.START: # don't overwrite Maze.START
self.maze[y][x] = dir.char # mark direction chosen
if self.solve(ny, nx): # recurse...
return True # solution found!
# no solution found from this location
if self.maze[y][x] != Maze.START: # don't overwrite Maze.START
self.maze[y][x] = Maze.PATH # clear failed search from map
return False
def main():
maze = Maze.load_maze("somemaze.txt")
print("Maze loaded:")
print(maze)
try:
sy, sx = maze.find_start()
print("solving...")
if maze.solve(sy, sx):
print(maze)
else:
print(" no solution found")
except ValueError:
print("No start point found.")
if __name__=="__main__":
main()
and when run produces:
Maze loaded:
####################################
#S# ## ######## # # # # #
# # # # # # #
# # ##### ## ###### # ####### # #
### # ## ## # # # #### #
# # # ####### # ### #E#
####################################
solving...
####################################
#S# ## ######## # #>>>>>v# >>v# #
#v#>>v# >>>v #^# >>>>^#>>v#
#>>^#v#####^##v######^# ####### #v#
### #v##>>>^##>>>>>v#^# # ####v#
# #>>>^# #######>>^# ### #E#
####################################
Note that the assignment as given has a few unPythonic elements:
it asks for camelCase function names rather than underscore_separated
it suggests using a global variable rather than passing data explicitly
it asks for find_start to return flag values on failure rather than raising an exception