Skip to content

Commit

Permalink
Move to python 3, start using unittest, implemented pairwise distance…
Browse files Browse the repository at this point in the history
… for color distance heuristic (but it's slow)
  • Loading branch information
alexander-jiang committed Oct 17, 2019
1 parent 7c35255 commit 554fe6a
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 297 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
*.pyc
*.pyc
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# kami2
Solver for the mobile puzzle game KAMI 2
# kami2
Solver for the mobile puzzle game KAMI 2
30 changes: 15 additions & 15 deletions deterministic_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ def solve(self, problem, find_all_solutions=False):
visited.append(state)
self.num_states_explored += 1

# print "---"
# print "state: (prev actions were: %s)" % (prev_actions, )
# print " ", state.graph
# print " ", state.node_colors
# print " moves left: %d" % (state.moves_left,)
# print("---")
# print("state: (prev actions were: %s)" % (prev_actions, ))
# print(" ", state.graph)
# print(" ", state.node_colors)
# print(" moves left: %d" % (state.moves_left,))
if problem.is_terminal_state(state) == 1:
print "solution found! ", prev_actions
print "num states explored: ", self.num_states_explored
print("solution found! ", prev_actions)
print("num states explored: ", self.num_states_explored)
if find_all_solutions: continue
else: break
if problem.is_terminal_state(state) == -1:
Expand Down Expand Up @@ -56,24 +56,24 @@ def solve(self, problem):
frontier.update(start_state, 0)
while not frontier.empty():
state, g_cost = frontier.remove_min()
# print "min cost state from frontier:"
# print state.graph
# print state.node_colors
# print g_cost
# print("min cost state from frontier:")
# print(state.graph)
# print(state.node_colors)
# print(g_cost)
visited.append(state)
self.num_states_explored += 1
if self.num_states_explored % 250 == 0:
print "explored", self.num_states_explored, "states"
print("explored", self.num_states_explored, "states")

if problem.is_terminal_state(state) == 1:
print "solution found!"
print("solution found!")
self.total_cost = g_cost
self.actions = []
while state != start_state:
action, state = predecessor[state]
self.actions.insert(0, action)
print "actions: ", self.actions
print "num states explored: ", self.num_states_explored
print("actions: ", self.actions)
print("num states explored: ", self.num_states_explored)
break
if problem.is_terminal_state(state) == -1:
continue
Expand Down
38 changes: 37 additions & 1 deletion informed_search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import copy
import itertools

import statespace as kami2
import deterministic_search as search
import copy

def num_colors_heuristic(state):
"""
Expand All @@ -17,6 +19,40 @@ def num_colors_heuristic(state):
else:
return state.num_colors() - 1

def color_distance_heuristic(state):
"""
Consider all of the nodes of each color. If you know the maximum of the
pairwise distances between any two nodes of the same color, then the
optimistic estimate is that the color can be contracted to a single node
in half (rounded up) of that max distance. But you should pick the color
that is easiest to contract first, as doing so could help contract other
colors.
"""
raise NotImplementedError("This heuristic is *really* slow...")
nodes = state.nodes()
# how many moves to contract one color to a single node?
moves_to_contract = state.moves_left

pairwise_distances = state.get_pairwise_distances()
for color in state.colors:
max_dist_for_color = -float('inf')
color_nodes = []
for node in nodes:
if state.get_color(node) == color:
color_nodes.append(node)

for (node1, node2) in itertools.combinations(color_nodes, 2):
distance = pairwise_distances[node1][node2]
# print("distance between %d & %d = %d" % (node1, node2, distance))
optimistic_num_moves = (distance + 1) // 2
if optimistic_num_moves > max_dist_for_color:
max_dist_for_color = optimistic_num_moves
# print("moves to contract color %s = %d" % (color, max_dist_for_color))
if max_dist_for_color < moves_to_contract:
moves_to_contract = max_dist_for_color
return moves_to_contract + state.num_colors() - 1


# Returns a transformed problem such that running UCS on the transformed problem
# is equivalent to running A* on the original problem with the given heuristic.
def transform_a_star_to_ucs(problem, heuristic):
Expand Down
32 changes: 31 additions & 1 deletion statespace.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import copy

class Kami2Puzzle:
Expand Down Expand Up @@ -30,7 +31,7 @@ def actions_and_costs(self, state):
for new_color in colors:
if new_color == state.get_color(node):
continue
# print "setting node %d to color %s" % (node, new_color)
# print("setting node %d to color %s" % (node, new_color))
new_state = state.set_color(node, new_color)
cost = 1
results.append(((node, new_color), new_state, cost))
Expand Down Expand Up @@ -61,13 +62,16 @@ def __init__(self, graph, node_colors, moves_left):
self.colors - set of strings, the colors in the graph state
self.node_colors - dict from node to string, the colors of each node
self.moves_left - int, the number of moves left to solve the puzzle
self.pairwise_distances - 2D dict, the pairwise distance between nodes
(see get_pairwise_distances function below)
"""
self._validate_init(graph, node_colors, moves_left)

self.graph = graph
self.colors = set(node_colors.values())
self.node_colors = node_colors
self.moves_left = moves_left
self.pairwise_distances = None

def _validate_init(self, graph, node_colors, moves_left):
"""
Expand Down Expand Up @@ -155,3 +159,29 @@ def set_color(self, node, new_color):
new_node_colors[node] = new_color

return PuzzleState(new_graph, new_node_colors, self.moves_left - 1)

def get_pairwise_distances(self):
"""
Gets the pairwise distances between nodes in the graph (memoized).
"""
def floyd_warshall(graph):
distances = {}
num_nodes = len(graph.keys())
for node in graph:
distances[node] = {}
for node2 in graph:
distances[node][node2] = float('inf')
for node in graph:
for neighbor in graph[node]:
distances[node][neighbor] = 1
distances[node][node] = 0
for midnode in graph:
for src in graph:
for sink in graph:
if distances[src][sink] > distances[src][midnode] + distances[midnode][sink]:
distances[src][sink] = distances[src][midnode] + distances[midnode][sink]
return distances

if self.pairwise_distances is None:
self.pairwise_distances = floyd_warshall(self.graph)
return self.pairwise_distances
183 changes: 99 additions & 84 deletions statespace_test.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,103 @@
import copy
import unittest

import statespace as kami2
import deterministic_search as search
import informed_search
import copy

puzzle6_graph = {
1: frozenset([2]),
2: frozenset([1, 3, 4, 5]),
3: frozenset([2, 4, 6]),
4: frozenset([2, 3, 5, 6]),
5: frozenset([2, 4, 6]),
6: frozenset([3, 4, 5, 7]),
7: frozenset([6]),
}
puzzle6_node_colors = {
1: 'b',
2: 'r',
3: 'y',
4: 'b',
5: 'y',
6: 'r',
7: 'b',
}
puzzle6_moves_left = 3
print "### Puzzle 6 initial state:"
print puzzle6_graph
print puzzle6_node_colors

puzzle6_step0 = kami2.PuzzleState(puzzle6_graph, puzzle6_node_colors, puzzle6_moves_left)
puzzle6 = kami2.Kami2Puzzle(puzzle6_step0)

assert puzzle6_step0.num_colors() == 3, "Puzzle should have 3 colors, actually has: %d" % puzzle6_step0.num_colors()
assert puzzle6_step0.moves_left == 3, "Puzzle should have 3 moves left, actually has: %d" % puzzle6_step0.moves_left
assert not puzzle6.is_terminal_state(puzzle6_step0), "Puzzle should not be in a terminal state!"

def testSetColor(step_num, node_to_update, new_color, prev_state,
num_colors, new_neighbors, moves_left, is_terminal):
print "### %d. Setting color of node %d to '%s'" % (step_num, node_to_update, new_color)
save_prev_node_colors = copy.deepcopy(prev_state.node_colors)
save_prev_graph = copy.deepcopy(prev_state.graph)
new_state = prev_state.set_color(node_to_update, new_color)

assert puzzle6.start_state() == puzzle6_step0, "Puzzle's start state should not have changed!"
assert new_state.moves_left == moves_left, "Puzzle should have %d moves left, actually has: %d" % (moves_left, new_state.moves_left)
assert prev_state.node_colors == save_prev_node_colors, "Previous state's node_colors should be unchanged!"
assert prev_state.graph == save_prev_graph, "Previous state's graph should be unchanged!"

assert new_state.num_colors() == num_colors, "Puzzle should have %d colors, actually has: %d" % (num_colors, new_state.num_colors())
assert new_state.node_colors[node_to_update] == new_color, "Contracted node has wrong color!"
assert new_state.graph[node_to_update] == new_neighbors, "Contracted node has wrong neighbors!"

print new_state.graph
print new_state.node_colors
if is_terminal:
assert puzzle6.is_terminal_state(new_state), "Puzzle should be in a terminal state!"
if len(new_state.colors) == 1:
print "### Solved!"
else:
assert not puzzle6.is_terminal_state(new_state), "Puzzle should not be in a terminal state!"

return new_state

step_num = 1
node_to_update = 4
new_color = 'y'
puzzle6_step1 = testSetColor(step_num, node_to_update, new_color, puzzle6_step0,
3, set([2, 6]), 2, False)

step_num += 1
node_to_update = 4
new_color = 'r'
puzzle6_step2 = testSetColor(step_num, node_to_update, new_color, puzzle6_step1,
2, set([1, 7]), 1, False)

step_num += 1
node_to_update = 4
new_color = 'b'
puzzle6_step3 = testSetColor(step_num, node_to_update, new_color, puzzle6_step2,
1, set([]), 0, True)

print "Solving using DFS:"
search.DepthFirstSearch().solve(puzzle6)

print "Solving using UCS:"
search.UniformCostSearch().solve(puzzle6)

print "Solving using A*:"
informed_search.AStarSearch(informed_search.num_colors_heuristic).solve(puzzle6)
class TestStateSpace(unittest.TestCase):
def setUp(self):
puzzle6_graph = {
1: frozenset([2]),
2: frozenset([1, 3, 4, 5]),
3: frozenset([2, 4, 6]),
4: frozenset([2, 3, 5, 6]),
5: frozenset([2, 4, 6]),
6: frozenset([3, 4, 5, 7]),
7: frozenset([6]),
}
puzzle6_node_colors = {
1: 'b',
2: 'r',
3: 'y',
4: 'b',
5: 'y',
6: 'r',
7: 'b',
}
puzzle6_moves_left = 3

print("### Puzzle 6 initial state:")
print(puzzle6_graph)
print(puzzle6_node_colors)

self.puzzle6_step0 = kami2.PuzzleState(puzzle6_graph, puzzle6_node_colors, puzzle6_moves_left)
self.puzzle6 = kami2.Kami2Puzzle(self.puzzle6_step0)

def test_state_space(self):
self.assertEqual(self.puzzle6_step0.num_colors(), 3, msg="Puzzle should have 3 colors, actually has: %d" % self.puzzle6_step0.num_colors())
self.assertEqual(self.puzzle6_step0.moves_left, 3, msg="Puzzle should have 3 moves left, actually has: %d" % self.puzzle6_step0.moves_left)
self.assertFalse(self.puzzle6.is_terminal_state(self.puzzle6_step0), msg="Puzzle should not be in a terminal state!")

def test_set_color(step_num, node_to_update, new_color, prev_state,
num_colors, new_neighbors, moves_left, is_terminal):
print("### %d. Setting color of node %d to '%s'" % (step_num, node_to_update, new_color))
save_prev_node_colors = copy.deepcopy(prev_state.node_colors)
save_prev_graph = copy.deepcopy(prev_state.graph)
new_state = prev_state.set_color(node_to_update, new_color)

self.assertEqual(self.puzzle6.start_state(), self.puzzle6_step0, msg="Puzzle's start state should not have changed!")
self.assertEqual(new_state.moves_left, moves_left, msg="Puzzle should have %d moves left, actually has: %d" % (moves_left, new_state.moves_left))
self.assertEqual(prev_state.node_colors, save_prev_node_colors, msg="Previous state's node_colors should be unchanged!")
self.assertEqual(prev_state.graph, save_prev_graph, msg="Previous state's graph should be unchanged!")

self.assertEqual(new_state.num_colors(), num_colors, msg="Puzzle should have %d colors, actually has: %d" % (num_colors, new_state.num_colors()))
self.assertEqual(new_state.node_colors[node_to_update], new_color, msg="Contracted node has wrong color!")
self.assertEqual(new_state.graph[node_to_update], new_neighbors, msg="Contracted node has wrong neighbors!")

print(new_state.graph)
print(new_state.node_colors)
if is_terminal:
self.assertTrue(self.puzzle6.is_terminal_state(new_state), msg="Puzzle should be in a terminal state!")
if len(new_state.colors) == 1:
print("### Solved!")
else:
self.assertFalse(self.puzzle6.is_terminal_state(new_state), msg="Puzzle should not be in a terminal state!")

return new_state

step_num = 1
node_to_update = 4
new_color = 'y'
puzzle6_step1 = test_set_color(step_num, node_to_update, new_color, self.puzzle6_step0,
3, set([2, 6]), 2, False)

step_num += 1
node_to_update = 4
new_color = 'r'
puzzle6_step2 = test_set_color(step_num, node_to_update, new_color, puzzle6_step1,
2, set([1, 7]), 1, False)

step_num += 1
node_to_update = 4
new_color = 'b'
puzzle6_step3 = test_set_color(step_num, node_to_update, new_color, puzzle6_step2,
1, set([]), 0, True)

def test_puzzle6_solve(self):
print("Solving using DFS:")
search.DepthFirstSearch().solve(self.puzzle6)

print("Solving using UCS:")
search.UniformCostSearch().solve(self.puzzle6)

print("Solving using A* (# colors heuristic):")
informed_search.AStarSearch(informed_search.num_colors_heuristic).solve(self.puzzle6)

# informed_search.color_distance_heuristic(self.puzzle6_step0)

print("Solving using A* (color distance heuristic):")
informed_search.AStarSearch(informed_search.color_distance_heuristic).solve(self.puzzle6)

if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 554fe6a

Please sign in to comment.