2

I am trying to develop a 15 star puzzle program in Python and its supposed to sort everything in numerical order using the a star search algorithm with the 0 being at the end.

Here is my a star algorithm I've developed so far:

"""Search the nodes with the lowest f scores first.
    You specify the function f(node) that you want to minimize; for example,
    if f is a heuristic estimate to the goal, then we have greedy best
    first search; if f is node.depth then we have breadth-first search.
    There is a subtlety: the line "f = memoize(f, 'f')" means that the f
    values will be cached on the nodes as they are computed. So after doing
    a best first search you can examine the f values of the path returned."""

    def best_first_graph_search_manhattan(root_node):
        start_time = time.time()
        f = manhattan(root_node)
        node = root_node
        frontier = []
        # how do we create this association?
        heapq.heappush(frontier, node)
        explored = set()
        z = 0

        while len(frontier) > 0:
            node = heapq.heappop(frontier)
            print(node.state.tiles)
            explored.add(node)
            if (goal_test(node.state.tiles)):
                #print('In if statement')
                path = find_path(node)
                end_time = time.time()
                z = z + f
                return path, len(explored), z, (end_time - start_time)
            for child in get_children(node):
                # calcuate total cost
                f_0 = manhattan(child)
                z = z + f_0
                print(z)
                if child not in explored and child not in frontier:
                    #print('Pushing frontier and child')
                    heapq.heappush(frontier, child)
                print('end of for loop')
        return None

    """ 
    Return the heuristic value for a given state using manhattan function
    """
    def manhattan(node):

           # Manhattan Heuristic Function
           # x1, y1 = node.state.get_location()
           # x2, y2 = self.goal

        zero_location = node.state.tiles.index('0')
        x1 = math.floor(zero_location / 4)
        y1 = zero_location % 4
        x2 = 3
        y2 = 3

        return abs(x2 - x1) + abs(y2 - y1)


    """
    astar_search() is a best-first graph searching algortithim using equation f(n) = g(n) + h(n)
    h is specified as...
    """
    def astar_search_manhattan(root_node):
        """A* search is best-first graph search with f(n) = g(n)+h(n).
        You need to specify the h function when you call astar_search, or
        else in your Problem subclass."""
        return best_first_graph_search_manhattan(root_node)

Here is the rest of my program. Assume that everything is working correctly in the following:

    import random
    import math
    import time
    import psutil
    import heapq
    #import utils.py
    import os
    import sys
    from collections import deque

    # This class defines the state of the problem in terms of board configuration
    class Board:
        def __init__(self,tiles):
            self.size = int(math.sqrt(len(tiles))) # defining length/width of the board
            self.tiles = tiles

        #This function returns the resulting state from taking particular action from current state
        def execute_action(self,action):
            new_tiles = self.tiles[:]
            empty_index = new_tiles.index('0')
            if action=='l': 
                if empty_index%self.size>0:
                    new_tiles[empty_index-1],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index-1]
            if action=='r':
                if empty_index%self.size<(self.size-1):     
                    new_tiles[empty_index+1],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index+1]
            if action=='u':
                if empty_index-self.size>=0:
                    new_tiles[empty_index-self.size],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index-self.size]
            if action=='d':
                if empty_index+self.size < self.size*self.size:
                    new_tiles[empty_index+self.size],new_tiles[empty_index] = new_tiles[empty_index],new_tiles[empty_index+self.size]
            return Board(new_tiles)


    # This class defines the node on the search tree, consisting of state, parent and previous action       
    class Node:
        def __init__(self,state,parent,action):
            self.state = state
            self.parent = parent
            self.action = action
            #self.initial = initial

        #Returns string representation of the state 
        def __repr__(self):
                return str(self.state.tiles)

        #Comparing current node with other node. They are equal if states are equal 
        def __eq__(self,other):
                return self.state.tiles == other.state.tiles

        def __hash__(self):
                return hash(self.state)

        def __lt__(self, other):
                return manhattan(self) < manhattan(other)

    # Utility function to randomly generate 15-puzzle       
    def generate_puzzle(size):
        numbers = list(range(size*size))
        random.shuffle(numbers)
        return Node(Board(numbers),None,None)

    # This function returns the list of children obtained after simulating the actions on current node
    def get_children(parent_node):
        children = []
        actions = ['l','r','u','d'] # left,right, up , down ; actions define direction of movement of empty tile
        for action in actions:
            child_state = parent_node.state.execute_action(action)
            child_node = Node(child_state,parent_node,action)
            children.append(child_node)
        return children

    # This function backtracks from current node to reach initial configuration. The list of actions would constitute a solution path
    def find_path(node):    
        path = []   
        while(node.parent is not None):
            path.append(node.action)
            node = node.parent
        path.reverse()
        return path

# Main function accepting input from console , running iterative_deepening_search and showing output    
def main():
        global nodes_expanded
        global path
        global start_time
        global cur_time
        global end_time
        nodes_expanded = 0
        process = psutil.Process(os.getpid())
        initial_memory = process.memory_info().rss / 1024.0
        initial = str(input("initial configuration: "))
        initial_list = initial.split(" ")
        root = Node(Board(initial_list),None,None)

        print(astar_search_manhattan(root))
        final_memory = process.memory_info().rss / 1024.0
        print('Directions: ', path)
        print('Total Time: ', (end_time-start_time), ' seconds')
        print('Total Memory: ',str(final_memory-initial_memory)+" KB")
        print('Total Nodes Expanded: ', nodes_expanded)

# Utility function checking if current state is goal state or not
def goal_test(cur_tiles):
    return cur_tiles == ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','0'] 

if __name__=="__main__":main()  

I've managed to narrow it down into my for loop in my best_first_graph_search_manhattan function and it appears that the infinite loop is caused if the if statement where its checking if child is not in explored and child is not in frontier. I'm unsure if its the way I'm calling my child function or the way I'm pushing frontier and child into my priority queue. I have imported heapq into my program and I've done extensive research where importing that function allows you to utilize priority queue into your program. Please don't mind other variables that are not used in my a star search.

Here is a test case: 1 0 3 4 5 2 6 8 9 10 7 11 13 14 15 12 | DRDRD

Thank you all very much for your help!

IMFeeferz
  • 21
  • 3

1 Answers1

0

Very interesting task you have, thanks! Was a great pleasure to program solution in Python A* for it from very scratch.

Although you asked to fix your own solution, I decided to do something else and implemented A* from scratch solely based on Wikipedia Article pseudo code. Wikipedia Article completely describes how algorithm works.

As function H(node) that provides lower bound estimates I took shortest distance from each 15-puzzle tile from its current position to final target position. This distance is equal to difference by X plus difference by Y from current to target position.

As a node in A* graph I use full board of tiles. Board (node) is stored as tuple of 4 rows (also tuples), each row having 4 tiles, each tile is represented as a number. Empty tile is represented as 0. To make board (node) storable inside map or set I made them hashable by storing them as nested tuples instead of nested lists.

There is a DumpPath() function that prints given path of boards VERY nicely into console. Look below after code to see console output example.

My class is generic, meaning that it can support any board size, not just 4x4, but for example 5x7, meaning that it can solve not only classic 15-puzzle but bigger boards too, and also non-square (rectangular).

In my Python code there is no dependencies on external libraries, only standard Python libraries are used: random. Random library is used to create very first random board by shuffling around tiles many steps.

See main A* function (method) at the very end of class definition (as last method). Class usage example goes right after class end.

Try it online!

class Puzzle15:
    import random
    def __init__(self):
        self.N, self.M = 4, 4
    def NumInNode(self, node, n):
        assert 0 <= n <= 15, n
        for i, row in enumerate(node):
            for j, e in enumerate(row):
                if e == n:
                    return i, j
        assert False, (node, n)
    def DupNode(self, node):
        return tuple(tuple(e for e in row) for row in node)
    def Goal(self):
        return tuple(tuple(j for j in range(self.M * i, self.M * i + self.M)) for i in range(self.N))
    def ToHashable(self, node):
        return tuple(tuple(e2 for e2 in e) for e in node)
    def ToUnHashable(self, node):
        return [[e2 for e2 in e] for e in node]
    def Neighbors(self, node):
        res = []
        zi, zj = self.NumInNode(node, 0)
        for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
            if not (0 <= zi + dy < self.N and 0 <= zj + dx < self.M):
                continue
            new_node = self.ToUnHashable(self.DupNode(node))
            new_node[zi][zj] = new_node[zi + dy][zj + dx]
            new_node[zi + dy][zj + dx] = 0
            res.append(self.ToHashable(new_node))
        return res
    def RandomBoard(self, *, nshuffles = None):
        if nshuffles is None:
            nshuffles = round(2 * self.N * self.M)
        node = self.Goal()
        for i in range(nshuffles):
            node = self.random.choice(self.Neighbors(node))
        return node
    def H(self, node):
        def Target(n):
            return n // self.M, n % self.M
        d = 0
        for i, row in enumerate(node):
            for j, e in enumerate(row):
                if e == 0:
                    continue
                ti, tj = Target(e)
                d += abs(i - ti) + abs(j - tj)
        return d
    def DumpPath(self, path):
        res = ''
        console_width = 80
        maxe_strl = len(str(self.N * self.M - 1))
        lines_block = ['' for i in range(self.N * 2 + 1)]
        for inode, node in enumerate(path):
            for i, row in enumerate(node):
                lines_block[i * 2 + 1] += '|'
            for i, row in enumerate(node):
                for j, e in enumerate(row):
                    lines_block[i * 2 + 1] += str(e if e != 0 else '').rjust(maxe_strl) + '|'
            for i in range(len(node) + 1):
                lines_block[i * 2] += '-' * (1 + len(node[0]) * (maxe_strl + 1))
            if inode + 1 < len(path):
                for i in range(len(lines_block)):
                    lines_block[i] += ' --> ' if i == self.N  else '     '
            if inode + 1 >= len(path) or len(lines_block[0]) >= console_width:
                for e in lines_block:
                    res += e + '\n'
                res += '\n'
                lines_block = ['' for i in range(self.N * 2 + 1)]
        return res
    def AStar(self, start, goal):
        # https://en.wikipedia.org/wiki/A*_search_algorithm#Pseudocode
        def ReconstructPath(came_from, current):
            total_path = [current]
            while current in came_from:
                current = came_from[current]
                total_path.insert(0, current)
            return total_path

        infinity = 10 ** 9
        open_set = {start}
        came_from = {}
        gscore = {start: 0}
        fscore = {start: self.H(start)}

        while len(open_set) > 0:
            current = None
            for e in open_set:
                if current is None or fscore[e] < fscore[current]:
                    current = e
            if current == goal:
                return ReconstructPath(came_from, current)
            open_set.remove(current)
            for neighbor in self.Neighbors(current):
                tentative_gscore = gscore[current] + 1 # d(current, neighbor)
                if tentative_gscore < (gscore[neighbor] if neighbor in gscore else infinity):
                    came_from[neighbor] = current
                    gscore[neighbor] = tentative_gscore
                    fscore[neighbor] = tentative_gscore + self.H(neighbor)
                    if neighbor not in open_set:
                        open_set.add(neighbor)
        
        assert False, 'Failure!'

p15 = Puzzle15()
print(p15.DumpPath(p15.AStar(p15.RandomBoard(), p15.Goal())))

Console output:

-------------     -------------     -------------     -------------     -------------
| 1| 5|  | 2|     | 1| 5| 2|  |     | 1| 5| 2| 3|     | 1| 5| 2| 3|     | 1| 5| 2| 3|
-------------     -------------     -------------     -------------     -------------
| 8| 4| 6| 3|     | 8| 4| 6| 3|     | 8| 4| 6|  |     | 8| 4| 6| 7|     | 8| 4| 6| 7|
------------- --> ------------- --> ------------- --> ------------- --> ------------- -->
|12|10|11| 7|     |12|10|11| 7|     |12|10|11| 7|     |12|10|11|  |     |12|10|  |11|
-------------     -------------     -------------     -------------     -------------
|13| 9|14|15|     |13| 9|14|15|     |13| 9|14|15|     |13| 9|14|15|     |13| 9|14|15|
-------------     -------------     -------------     -------------     -------------

-------------     -------------     -------------     -------------     -------------
| 1| 5| 2| 3|     | 1| 5| 2| 3|     | 1| 5| 2| 3|     | 1| 5| 2| 3|     | 1| 5| 2| 3|
-------------     -------------     -------------     -------------     -------------
| 8| 4| 6| 7|     | 8| 4| 6| 7|     | 8| 4| 6| 7|     | 8| 4| 6| 7|     |  | 4| 6| 7|
------------- --> ------------- --> ------------- --> ------------- --> ------------- -->
|12|  |10|11|     |12| 9|10|11|     |12| 9|10|11|     |  | 9|10|11|     | 8| 9|10|11|
-------------     -------------     -------------     -------------     -------------
|13| 9|14|15|     |13|  |14|15|     |  |13|14|15|     |12|13|14|15|     |12|13|14|15|
-------------     -------------     -------------     -------------     -------------

-------------     -------------     -------------
| 1| 5| 2| 3|     | 1|  | 2| 3|     |  | 1| 2| 3|
-------------     -------------     -------------
| 4|  | 6| 7|     | 4| 5| 6| 7|     | 4| 5| 6| 7|
------------- --> ------------- --> -------------
| 8| 9|10|11|     | 8| 9|10|11|     | 8| 9|10|11|
-------------     -------------     -------------
|12|13|14|15|     |12|13|14|15|     |12|13|14|15|
-------------     -------------     -------------
Arty
  • 14,883
  • 6
  • 36
  • 69