Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modified and introduced new algorithms for 8Puzzle problem #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 198 additions & 97 deletions Python/8PUZZLE.PY
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,26 @@
# board in the following positions:
#
# -------------
# | 0 | 3 | 6 |
# | 3 | 7 | 6 |
# -------------
# | 1 | 4 | 7 |
# | 5 | 1 | 2 |
# -------------
# | 2 | 5 | 8 |
# | 4 | 0 | 8 |
# -------------
#
# The goal is defined as:
#
# -------------
# | 1 | 2 | 3 |
# | 5 | 3 | 6 |
# -------------
# | 8 | 0 | 4 |
# | 7 | 0 | 2 |
# -------------
# | 7 | 6 | 5 |
# | 4 | 1 | 8 |
# -------------
#
# Where 0 denotes the blank tile or space.
goal_state = [1, 8, 7, 2, 0, 6, 3, 4, 5]
#
# The code will read state from a file called "state.txt" where the format is
# as above but space seperated. i.e. the content for the goal state would be
# 1 8 7 2 0 6 3 4 5
goal_state = [5, 7, 4, 3, 0, 1, 6, 2, 8]
starting_state = [3, 5, 4, 7, 1, 0, 6, 2, 8]

### Code begins.
import sys
Expand All @@ -59,7 +56,7 @@ def move_up( state ):
"""Moves the blank tile up on the board. Returns a new state as a list."""
# Perform an object copy
new_state = state[:]
index = new_state.index( 0 )
index = new_state.index( 0 ) #this will return index of blank
# Sanity check
if index not in [0, 3, 6]:
# Swap the values.
Expand Down Expand Up @@ -118,113 +115,226 @@ def move_right( state ):
# Can't move, return None
return None

def create_node( state, parent, operator, depth, cost ):
return Node( state, parent, operator, depth, cost )
def create_node( state, parent, operator, depth,cost):
return Node( state, parent, operator, depth,cost)

def expand_node( node, nodes ):
def expand_node( node,open_nodes,close_nodes):
"""Returns a list of expanded nodes"""
expanded_nodes = []
expanded_nodes.append( create_node( move_up( node.state ), node, "u", node.depth + 1, 0 ) )
expanded_nodes.append( create_node( move_down( node.state ), node, "d", node.depth + 1, 0 ) )
expanded_nodes.append( create_node( move_left( node.state ), node, "l", node.depth + 1, 0 ) )
expanded_nodes.append( create_node( move_right( node.state), node, "r", node.depth + 1, 0 ) )

expanded_nodes.append( create_node( move_up( node.state ), node, "u", node.depth + 1,0) )
expanded_nodes.append( create_node( move_down( node.state ), node, "d", node.depth + 1,0) )
expanded_nodes.append( create_node( move_left( node.state ), node, "l", node.depth + 1,0) )
expanded_nodes.append( create_node( move_right( node.state), node, "r", node.depth + 1,0) )

# Filter the list and remove the nodes that are impossible (move function returned None)
expanded_nodes = [node for node in expanded_nodes if node.state != None] #list comprehension!
open_state = []
for o in open_nodes:
open_state.append(o.state)

close_state = []
for c in close_nodes:
close_state.append(c.state)

#Remove repeated nodes
#Remove the nodes that are in open list
expanded_nodes = [node for node in expanded_nodes if node.state not in open_state]
#Remove the nodes that are in close list
expanded_nodes = [node for node in expanded_nodes if node.state not in close_state]

return expanded_nodes


def solution_path(node):
moves = []
states = []
temp = node
while True:
moves.insert(0, temp.operator)
states.insert(0,temp.state)
if temp.depth <= 1: break
temp = temp.parent
states.insert(0,starting_state)
return moves,states

def bfs( start, goal ):
"""Performs a breadth first search from the start state to the goal"""
# A list (can act as a queue) for the nodes.
nodes = []
open_nodes = []
close_nodes = []
# Create the queue with the root node in it.
nodes.append( create_node( start, None, None, 0, 0 ) )
open_nodes.append( create_node( start, None, None, 0 , 0) )
while True:
# We've run out of states, no solution.
if len( nodes ) == 0: return None
if len( open_nodes ) == 0: return None,None
# take the node from the front of the queue
node = nodes.pop(0)
node = open_nodes.pop(0)
close_nodes.append(node)

# Append the move we made to moves
# if this node is the goal, return the moves it took to get here.
if node.state == goal:
moves = []
temp = node
while True:
moves.insert(0, temp.operator)
if temp.depth == 1: break
temp = temp.parent
return moves
# Expand the node and add all the expansions to the front of the stack
nodes.extend( expand_node( node, nodes ) )

def dfs( start, goal, depth=10 ):
return solution_path(node)

# Expand the node and add all the expansions to end of the queue
open_nodes.extend( expand_node( node, open_nodes,close_nodes) )


def dfs( start, goal,depth = 10):
"""Performs a depth first search from the start state to the goal. Depth param is optional."""
# NOTE: This is a limited search or else it keeps repeating moves. This is an infinite search space.
# I'm not sure if I implemented this right, but I implemented an iterative depth search below
# too that uses this function and it works fine. Using this function itself will repeat moves until
# the depth_limit is reached. Iterative depth search solves this problem, though.
#
# An attempt of cutting down on repeat moves was made in the expand_node() function.
depth_limit = depth
# A list (can act as a stack too) for the nodes.
nodes = []
# Create the queue with the root node in it.
nodes.append( create_node( start, None, None, 0, 0 ) )
open_nodes = []
close_nodes = []

depth_limit = depth
# Create the stack with the root node in it.
open_nodes.append( create_node( start, None, None, 0 ,0) )
while True:
# We've run out of states, no solution.
if len( nodes ) == 0: return None
if len( open_nodes ) == 0: return None,None
# take the node from the front of the queue
node = nodes.pop(0)
node = open_nodes.pop(0)
close_nodes.append(node)
# if this node is the goal, return the moves it took to get here.
if node.state == goal:
moves = []
temp = node
while True:
moves.insert(0, temp.operator)
if temp.depth <= 1: break
temp = temp.parent
return moves
# Add all the expansions to the beginning of the stack if we are under the depth limit
return solution_path(node)

if node.depth < depth_limit:
expanded_nodes = expand_node( node, nodes )
expanded_nodes.extend( nodes )
nodes = expanded_nodes
#Expand the nodes and add all the expansion in the front of open list
expanded_nodes = expand_node( node, open_nodes,close_nodes )
if len(expanded_nodes) != 0:
expanded_nodes.extend(open_nodes )
open_nodes = expanded_nodes


def ids( start, goal, depth=50 ):
"""Perfoms an iterative depth first search from the start state to the goal. Depth is optional."""
for i in range( depth ):
result = dfs( start, goal, i )
result,state = dfs( start, goal, i )
if result != None:
return result
return result,state
return None,None


def hill_climbing(start,goal):
""" Perform steepest Hill Climbing Approach. This method involves local minimum search"""
open_nodes = []
close_nodes = [] #Required to remove the repition

# Create the stack with the root node in it.
open_nodes.append( create_node( start, None, None, 0,0 ) )
while True:
# We've run out of states, no solution.
if len( open_nodes ) == 0: return None,None
# take the node from the front of the queue
node = open_nodes.pop(0)
close_nodes.append(node)
# if this node is the goal, return the moves it took to get here.
if node.state == goal:
return solution_path(node)

h1 = h(node.state,goal) #heuristic value of current node

#Expand the nodes and add all the expansion in the front of open list
expanded_nodes = expand_node( node, open_nodes,close_nodes )
successor_nodes = []
for succ_node in expanded_nodes:
h2 = h(succ_node.state,goal)
if(h2<h1):
successor_nodes.append(succ_node)
if(len (successor_nodes) != 0):
successor_nodes.sort( cmp2 )
successor_nodes.extend(open_nodes )
open_nodes = successor_nodes



def best_first(start,goal):
"""" Perform best first search using heuristic function"""
open_nodes = []
close_nodes = [] #Required to remove the repition

# Create the stack with the root node in it.
open_nodes.append( create_node( start, None, None, 0 ,0) )
while True:
# We've run out of states, no solution.
if len( open_nodes ) == 0: return None,None
# take the node from the front of the queue
node = open_nodes.pop(0)
close_nodes.append(node)
# if this node is the goal, return the moves it took to get here.
if node.state == goal:
return solution_path(node)

#Add successor nodes to open list and sort the list to determine global minimum
open_nodes.extend( expand_node( node, open_nodes,close_nodes) )
open_nodes.sort(cmp2)



def a_star( start, goal ):
"""Perfoms an A* heuristic search"""
# ATTEMPTED: does not work :(
nodes = []
nodes.append( create_node( start, None, None, 0, 0 ) )
open_nodes = []
close_nodes = []
open_nodes.append( create_node( start, None, None, 0,0 ) )
while True:
# We've run out of states - no solution.
if len( nodes ) == 0: return None
if len( open_nodes ) == 0: return None,None

# Sort the nodes with custom compare function.
nodes.sort( cmp )
open_nodes.sort( cmp1 )

# take the node from the front of the queue
node = nodes.pop(0)
# if this node is the goal, return the moves it took to get here.
print "Trying state", node.state, " and move: ", node.operator
if node.state == goal:
moves = []
temp = node
while True:
moves.insert( 0, temp.operator )
if temp.depth <=1: break
temp = temp.parent
return moves
#Expand the node and add all expansions to the end of the queue
nodes.extend( expand_node( node, nodes ) )
best_node = open_nodes.pop(0)

def cmp( x, y ):
# Compare function for A*. f(n) = g(n) + h(n). I use depth (number of moves) for g().
if best_node.state == goal:
return solution_path(best_node)

#Generating the successor node
successor_nodes = expand_node(best_node,[],[])

for succ in successor_nodes:
#g(successor) = g(best_node) + cost of getting successor node from best node
succ.cost = best_node.cost + (succ.depth - best_node.depth)

#if successor is a goal node then return the moves
if succ.state == goal:
return solution_path(succ)

#if already generated node but not processed i.e. node in open list but has lower cost than old in open update old
elif succ.state in open_nodes:
old = [o for o in open_nodes if o.state == succ.state][0]
succ_f = succ.cost + h(succ,goal) # f value of successor
old_f = old.cost + h(old,goal) #f value of old
if succ_f < old_f: #update open list successor duplicate with succ value
old.parent = best_node
old.cost = succ.cost
#otherwise ignore successor

#if successor is already processed but has lower cost than old node, add in open list
elif succ.state in close_nodes:
old = [c for c in close_nodes if c.state == succ.state][0]
succ_f = succ.cost + h(succ,goal) #f value of successor
old_f = old.cost + h(old,goal) #f value of old
if succ_f < old_f: #update open list successor duplicate with succ value
open_nodes.append(succ)
#otherwise ignore successor

#if node neither in open nor in close list add succ in open list
else:
open_nodes.append(succ)
close_nodes.append(best_node)


def cmp1( x, y ):
# Compare function for A*. f(n) = g(n) + h(n). I have used depth (number of moves) for g().
return (x.depth + h( x.state, goal_state )) - (y.depth + h( x.state, goal_state ))

def cmp2( x, y):
# Compare function for Hill Climbing and Best First Search i.e h(n).
return (h( x.state, goal_state ) - h( x.state, goal_state ))

def h( state, goal ):
"""Heuristic for the A* search. Returns an integer based on out of place tiles"""
score = 0
Expand All @@ -235,7 +345,7 @@ def h( state, goal ):

# Node data structure
class Node:
def __init__( self, state, parent, operator, depth, cost ):
def __init__( self, state, parent, operator, depth,cost):
# Contains the state of the node
self.state = state
# Contains the node that generated this node
Expand All @@ -244,33 +354,24 @@ class Node:
self.operator = operator
# Contains the depth of this node (parent.depth +1)
self.depth = depth
# Contains the path cost of this node from depth 0. Not used for depth/breadth first.
# Contains the cost of this node
self.cost = cost

def readfile( filename ):
f = open( filename )
data = f.read()
# Get rid of the newlines
data = data.strip( "\n" )
#Break the string into a list using a space as a seperator.
data = data.split( " " )
state = []
for element in data:
state.append( int( element ) )
return state


# Main method
def main():
starting_state = readfile( "state.txt" )
### CHANGE THIS FUNCTION TO USE bfs, dfs, ids or a_star
result = ids( starting_state, goal_state )
### CHANGE THIS FUNCTION TO USE bfs, dfs, ids, best_first, hill_climbing or a_star
result,states = ids( starting_state, goal_state,10 )
if result == None:
print "No solution found"
elif result == [None]:
print "Start node was the goal!"
else:
print result
print len(result), " moves"

#To display board content
for state in states:
display_board(state)

# A python-isim. Basically if the file is being run execute the main() function.
if __name__ == "__main__":
Expand Down