Skip to content
Merged
Show file tree
Hide file tree
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
96 changes: 96 additions & 0 deletions search.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,102 @@ def h(self, node):

# ______________________________________________________________________________

class NPuzzle(Problem):
"""Generalization of the Eight Puzzle problem. The problem consists of sliding tiles numbered from 1 to N ^2 on a NxN board,
where one of the squares is a blank. The state is represented as a tuple of length N^2,
where element at index i represents the tile number at index i , where '0' denotes empty square index"""
def __init__(self, initial=None, goal=None, size=3, heuristic='hamming', shuffle=10):
"""Args:
intial: intiail state of puzzle. If not passed will default to goal state
size: number of rows and columns in puzzle
heuristic: heuristic used in astat search
shuffle: number of times to perfrom random action from the initial state.
Returns:
Problem instance modeling any size of sliding puzzle"""
self.size = size
self.heuristic = heuristic
goal = goal or (tuple(range(1,size*size, 1)) + (0, ))
initial = initial or (tuple(range(1,size*size, 1)) + (0, ))
if shuffle:
initial = self.shuffle_state(shuffle, list(initial))
assert len(goal) == size*size, "length of goal tuple should be {length}".format(length=size*size)
assert len(initial) == size * size, "length of initial tuple should be {length}".format(length=size * size)
assert self.check_solvability(initial), "The puzzle is not solvable from the given initial state!"
Problem.__init__(self, initial, goal)

def shuffle_state(self, shuffle, state):
"""Perform random action from the initial state"""
for _ in range(shuffle):
action = random.sample(self.actions(state),1)[0]
state = self.result(state,action)
return state

def find_blank_square(self, state):
"""Return the index of the blank square in a given state"""
return state.index(0)

def actions(self, state):
"""Return the actions that can be executed in the given state.
The result would be a list of maximally four directions, since there are only four possible actions
in any given state of the environment"""
possible_actions = ['UP', 'DOWN', 'LEFT', 'RIGHT']
index_blank_square = self.find_blank_square(state)
if index_blank_square % self.size == 0:
possible_actions.remove('LEFT')
if index_blank_square < self.size:
possible_actions.remove('UP')
if index_blank_square % self.size == self.size -1 :
possible_actions.remove('RIGHT')
if index_blank_square > self.size*self.size - self.size - 1:
possible_actions.remove('DOWN')
return possible_actions

def result(self, state, action):
"""Given state and action, return a new state that is the result of the action.
Action is assumed to be a valid action in the state"""
index_blank_square = self.find_blank_square(state)
new_state = list(state)
delta = {'UP': -self.size, 'DOWN': self.size, 'LEFT': -1, 'RIGHT': 1}
neighbor = index_blank_square + delta[action]
new_state[index_blank_square], new_state[neighbor] = new_state[neighbor], new_state[index_blank_square]
return tuple(new_state)

def goal_test(self, state):
"""Given a state, return True if state is a goal state or False otherwise"""
return state == self.goal

def check_solvability(self, state):
"""Checks if the given state is solvable. Whether or not puzzle is solvable depends on size being even or odd.
If N is odd, then puzzle instance is solvable if number of inversions is even in the input state.
If N is even, puzzle instance is solvable if the blank is on an even row counting from the bottom (second-last, fourth-last, etc.) and number of inversions is odd
or the blank is on an odd row counting from the bottom (last, third-last, fifth-last, etc.) and number of inversions is even."
reference : https://www.geeksforgeeks.org/check-instance-15-puzzle-solvable/"""
solvable = True
inversions = 0
for i in range(len(state)-1):
for j in range(i+1, len(state)):
if state[i] != 0 and state[j] != 0 and state[i] > state[j]:
inversions += 1
if self.size % 2 != 0:
solvable = True if (inversions % 2 == 0 ) else False
elif self.find_blank_square(state) // self.size % 2 == 0 and inversions % 2 != 0 :
solvable = True
elif self.find_blank_square(state) // self.size % 2 != 0 and inversions % 2 == 0:
solvable = True
else:
solvable = False
return solvable

def h(self, node):
"""Return the heuristic value for a given state. Default heuristic function used is
h(n) = number of misplaced tiles"""
heuristics = {'hamming':self.hamming_distance_heuristic(node)}
return heuristics.get(self.heuristic)

def hamming_distance_heuristic(self, node):
return hamming_distance(node.state, self.goal)


class TravelingSalesman(Problem):
""" The problem of finding the shortest Hamiltonian cycle (tour) that visits
every city exactly once and returns to the start."""
Expand Down
9 changes: 9 additions & 0 deletions tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ def test_pour_problem():
assert any(level == 4 for level in solution.state)


def test_n_puzzle():
# NPuzzle generalizes EightPuzzle; a one-move-from-goal 3x3 instance is solved by A*
npuzzle = NPuzzle(initial=(1, 2, 3, 4, 5, 6, 0, 7, 8), size=3, shuffle=0)
assert astar_search(npuzzle).solution() == ['RIGHT', 'RIGHT']
# a shuffled instance is always solvable and reaches the goal
solved = astar_search(NPuzzle(size=3, shuffle=15))
assert solved is not None


def test_find_blank_square():
assert eight_puzzle.find_blank_square((0, 1, 2, 3, 4, 5, 6, 7, 8)) == 0
assert eight_puzzle.find_blank_square((6, 3, 5, 1, 8, 4, 2, 0, 7)) == 7
Expand Down
Loading