1

I have to perform a depth first seach to solve the 8 puzzle game: (-1 represents the empty value)

initialState1 = [[-1,1,2], [7,8,3], [6,5,4]]
finalState1 = [[1,2,3], [8,-1,4], [7,6,5]]

The code I have:

def play(puzzle, direction):
  print(puzzle)
  if direction == "up":
    for i in range(3):
      for j in range(3):
        if(puzzle[i][j]==-1):
          if i!=0:
            puzzle[i][j] = puzzle[i-1][j]
            puzzle[i-1][j] = -1
          return puzzle

  if direction=="down":
    for i in range(3):
      for j in range(3):
        if(puzzle[i][j]==-1):
          if i!=2:
            puzzle[i][j] = puzzle[i+1][j]
            puzzle[i+1][j] = -1
          return puzzle

  if direction == "left":
    for i in range(3):
      for j in range(3):
        if(puzzle[i][j] == -1):
          if j!=0:
            puzzle[i][j] = puzzle[i][j-1]
            puzzle[i][j-1] = -1
          return puzzle

  if direction == "right":
    for i in range(3):
      for j in range(3):
        if(puzzle[i][j] == -1):
          if j!=2:
            puzzle[i][j] = puzzle[i][j+1]
            puzzle[i][j+1] = -1
          return puzzle

def checkBoundaries(puzzle):
    possibilities = []
    for i in range(3):
        for j in range(3):
            if(puzzle[i][j]==-1):
                if(i == 0):
                    if(j == 0):
                        possibilities.append("down")
                        possibilities.append("right")
                    elif(j == 1):
                        possibilities.append("down")
                        possibilities.append("right")
                        possibilities.append("left")
                    elif(j == 2):
                        possibilities.append("down")
                        possibilities.append("left")
                if(i == 1):
                    if(j == 0):
                        possibilities.append("down")
                        possibilities.append("right")
                        possibilities.append("up")
                    elif(j == 1):
                        possibilities.append("down")
                        possibilities.append("right")
                        possibilities.append("left")
                        possibilities.append("up")
                    elif(j == 2):
                        possibilities.append("down")
                        possibilities.append("left")
                        possibilities.append("up")
                if(i == 2):
                    if(j == 0):
                        possibilities.append("up")
                        possibilities.append("right")
                    elif(j == 1):
                        possibilities.append("up")
                        possibilities.append("right")
                        possibilities.append("left")
                    elif(j == 2):
                        possibilities.append("up")
                        possibilities.append("left")
                        
    return random.choice(possibilities)

def depthFirstSearch():
  pathcost=0
  queue=[]
  initialFormatted=[initialState1,"none"]
  queue.append(initialFormatted)

  while(True):
    puzzle=queue.pop(0)
    pathcost=pathcost+1
    print(str(puzzle[1])+" --> "+str(puzzle[0]))
    if(puzzle[0] == finalState1):
      print("Found")
      print("Path cost -> "+str(pathcost-1))
      break
    else:
      nextMove=play(puzzle[0], checkBoundaries(puzzle[0]))
      if(nextMove!=puzzle[0]):
          nextMove=[nextMove,checkBoundaries(puzzle[0])]
          queue.insert(0, nextMove)

depthFirstSearch()

But running this code, I receive this error :

puzzle=queue.pop(0) IndexError: pop from empty list

How to handle this? What i'm doing wrong?

And is my code doing the right thing to achieve my goal ? In checkBoudaries method, I'm currently setting a random value (within the possibilities) to return.. Is it right or do I have to prefer some movement based on the last move?

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
HelloWorldd
  • 172
  • 2
  • 14
  • Why does your code for performing a depth-first search, involve making a `random.choice`? You want to systematically try all the possibilities at a given position, right? – Karl Knechtel Mar 23 '21 at 21:56
  • As I wrote i'm uncertain about how to do it.. can you make an answer to help me? – HelloWorldd Mar 23 '21 at 22:00
  • Also, where you have written `nextMove=[nextMove,checkBoundaries(puzzle[0])]`, I'm not sure what you intend for that to do. I don't think the data structure you're building makes sense for the task. – Karl Knechtel Mar 23 '21 at 22:00
  • Basically I don't think I understand your overall approach in the first place, so it's hard to tell you how to fix it. Like, I *think* what the values you're putting in the queue are supposed to be pairs of (puzzle state, move to try from this position). However, when it's time to actually make the move, you ignore the pre-calculated move to try and look for a new one. – Karl Knechtel Mar 23 '21 at 22:06
  • My queue is something like this : `[[-1, 1, 2], [7, 8, 3], [6, 5, 4], "up"]` – HelloWorldd Mar 23 '21 at 22:18
  • I figured out eventually how you are trying to structure the data, yeah. – Karl Knechtel Mar 23 '21 at 22:27

1 Answers1

0

There are multiple conceptual problems here, to the point where it's too much work to try and write out a corrected version. However, I think I now understand the general structure of your attempt well enough to fix it.

  1. You are using a queue-based approach (as opposed to a recursive one), which means you need to keep track of the path costs (or, if you want to track them, the entire paths) in the queue nodes themselves. (In a recursive approach, you would pass this information along in the recursive call.)

  2. You are using a queue-based approach. That naturally implements breadth-first search. You want a stack instead, so that new possibilities from the current position are tried before the other possibilities at the "parent" position.

  3. The particular problem you are solving makes it possible to go in a loop. You need to have some kind of detection for this. A simple approach is to maintain a set of previously seen positions.

  4. You need to search exhaustively, not just pick one move from the current position. That's the entire point of the algorithm. You should return the entire list of possibilities from checkBoundaries, and add a separate node to the stack for each possibility.


But the thing that actually causes the reported problem:

  if(nextMove!=puzzle[0]):
      nextMove=[nextMove,checkBoundaries(puzzle[0])]
      queue.insert(0, nextMove)

This check shouldn't be necessary, because you are only supposed to be trying valid moves, and valid moves should (by definition) change the board position.

But aside from that, you never insert anything into your queue because this condition is never met - the nextMove is always equal to puzzle[0], even though you changed the board position. Why? Because it is the same object. Nothing in your code ever makes a copy of the board object; so the queue contains multiple copies; the play code modifies that same, single object and returns it back to you, etc. This is a common problem in Python that manifests in many forms - nothing that's quite an exact duplicate, but it's the same problem as, for example, List of lists changes reflected across sublists unexpectedly .

Which means, even if you removed the unnecessary check, you would still have problems - because your play function affects every node in the queue. You need to rewrite play so that it creates a new list-of-lists representing the "next" board position, without modifying the input.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • Aside from this, there are many simplifications you could make to the code. It's not really necessary to `checkBoundaries` at all; just try to `play` *all four* directions, and whichever ones don't affect the board state, just skip them. Now the `nextMove!=puzzle[0]` check becomes useful again. Another thing that would help a lot is to write and use a function to search the board for the `-1`. – Karl Knechtel Mar 23 '21 at 22:25