From 2d2ff4994ad313d7a4db1bcecc9c9ce3d11cb5de Mon Sep 17 00:00:00 2001 From: davey Date: Fri, 2 Jun 2017 17:21:07 +0100 Subject: [PATCH] Friday Fun: travelling salesperson contest sample. --- Malmo/samples/Python_examples/CMakeLists.txt | 1 + Malmo/samples/Python_examples/tsp_race.py | 886 +++++++++++++++++++ 2 files changed, 887 insertions(+) create mode 100755 Malmo/samples/Python_examples/tsp_race.py diff --git a/Malmo/samples/Python_examples/CMakeLists.txt b/Malmo/samples/Python_examples/CMakeLists.txt index dba76ab62..d8527e613 100755 --- a/Malmo/samples/Python_examples/CMakeLists.txt +++ b/Malmo/samples/Python_examples/CMakeLists.txt @@ -71,6 +71,7 @@ set( MULTI_AGENT_TESTS team_reward_test.py multi_agent_test.py agent_visibility_test.py + tsp_race.py turn_based_test.py MultiMaze.py ) diff --git a/Malmo/samples/Python_examples/tsp_race.py b/Malmo/samples/Python_examples/tsp_race.py new file mode 100755 index 000000000..45e94527f --- /dev/null +++ b/Malmo/samples/Python_examples/tsp_race.py @@ -0,0 +1,886 @@ +# ------------------------------------------------------------------------------------------------ +# Copyright (c) 2016 Microsoft Corporation +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +# associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# ------------------------------------------------------------------------------------------------ + +# Travelling Salesperson Contest +# Place one or more agents (at the same starting location) in a field with a random distribution of waypoints. +# The first agent to visit all the waypoints is the winner! +# All calculation must be done once the mission has started - ie a brute-force algorithm might come up with a better +# solution, but the other agents will be half way home before it's left the starting block... + +# There are six different approximate solutions implemented here, and any combination of them can be raced +# against each other. It's about the least efficient way imaginable of comparing TSP algorithms... +# but it's fun. + +import MalmoPython +import os +import random +import sys +import time +import json +import random +import math +import threading + +from Tkinter import * + +################################################################################################################### +# General code for all approaches +################################################################################################################### + +class point_node(object): + def __init__(self, x, y): + self.x = x + self.y = y + self.neighbours=[] + + def get_position(self): + return (self.x, self.y) + + def add_neighbour(self, neighbour): + self.neighbours.append(neighbour) + +distance = lambda p1, p2: math.sqrt((p1.x - p2.x) * (p1.x - p2.x)) + math.sqrt((p1.y - p2.y) * (p1.y - p2.y)) + +def path_length(points): + tot_dist = 0 + p_old = points[0] + for p_new in points: + tot_dist += distance(p_new, p_old) + p_old = p_new + return tot_dist + +################################################################################################################### +# Code to support Minimum Spanning Tree approach +################################################################################################################### + +class disjoint_set_forest_node(object): + def __init__(self, data=None, parent=None): + self.data = data + self.parent = parent + + def get_data(self): + return self.data + + def get_parent(self): + return self.parent + + def set_parent(self, parent): + self.parent = parent + + def get_root(self): + if self.parent == None: + return self + self.parent = self.parent.get_root() + return self.parent + + def combine_sets(self, new_node): + root_node_a = self.get_root() + root_node_b = new_node.get_root() + if root_node_a != root_node_b: + root_node_b.set_parent(root_node_a) + +class edge(object): + def __init__(self, point1, point2): + self.point1 = point1 + self.point2 = point2 + self.squared_length = (point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y) + + def get_squared_length(self): + return self.squared_length + +def merge_sort_edges(edges): + if len(edges) > 1: + m = len(edges) // 2 + left = edges[:m] + right = edges[m:] + + merge_sort_edges(left) + merge_sort_edges(right) + + l = 0 + r = 0 + i = 0 + while l < len(left) and r < len(right): + if left[l].get_squared_length() < right[r].get_squared_length(): + edges[i] = left[l] + l += 1 + else: + edges[i] = right[r] + r += 1 + i += 1 + while l < len(left): + edges[i] = left[l] + l += 1 + i += 1 + while r < len(right): + edges[i] = right[r] + r += 1 + i += 1 + +def min_span_tree(edges, points): + # Build a MST using Kruskal's algorithm. + # Make each point the start of a disjoint set forest: + for p in points: + p.disjoint_set_forest_node = disjoint_set_forest_node() + # Sort the edges from shortest to longest: + merge_sort_edges(edges) + # Build the tree: + tree=[] + i = 0 + finished = False + while not finished and i < len(edges): + e = edges[i] + i += 1 + if e.point1.disjoint_set_forest_node.get_root() != e.point2.disjoint_set_forest_node.get_root(): + # This edge will connect two different trees, so add it to the MST: + tree.append(e) + e.point1.disjoint_set_forest_node.combine_sets(e.point2.disjoint_set_forest_node) + e.point1.add_neighbour(e.point2) + e.point2.add_neighbour(e.point1) + + return tree + +def get_MST_route(points): + # Create a rough approximation of the optimal TSP route using a depth-first search on the minimum spanning tree: + # First, create a fully connected graph: + edges = [] + for i in xrange(len(points)): + for j in xrange(i + 1, len(points)): + edges.append(edge(points[i], points[j])) + # Get the MST: + tree = min_span_tree(edges, points) + # Perform the search: + stack=[points[0]] + order=[] + for p in points: + p.visited = False + while len(stack) > 0: + current_node = stack.pop() + current_node.visited = True + order.append(current_node) + for neighbour in current_node.neighbours: + if not neighbour.visited: + stack.append(neighbour) + return order + +################################################################################################################### +# Code to support Divide and conquer approach +################################################################################################################### + +def generate_orders(n): + # Recursively generate all the permutations of n cities, as a list of colon-delimited strings. + # Eg "generate_orders(3)" will return ['0:1:2:', '0:2:1:', '1:0:2:', '1:2:0:', '2:0:1:', '2:1:0:'] + values = [True for x in xrange(n)] + perms = [] + fill_next_value(0, n, values, perms, "") + return perms + +def fill_next_value(digit, num_digits, available_values, perms, seq_so_far): + # Recursively generate perumutations. + if digit == num_digits: + perms.append(seq_so_far) + else: + for i in xrange(num_digits): + if available_values[i]: + values_available_now=list(available_values) + values_available_now[i] = False + fill_next_value(digit + 1, num_digits, values_available_now, perms, seq_so_far + str(i) + ":") + +def assignKMeans(centroids, points): + #K-means: + i = 0 + for centroid in centroids: + centroid.index = i + i += 1 + + # Give all points a cluster index, and find bounding box: + minx = maxx = points[0].x + miny = maxy = points[0].y + for p in points: + p.k_index = random.randint(0, len(centroids) - 1) + if p.x < minx: + minx = p.x + if p.y < miny: + miny = p.y + if p.x > maxx: + maxx = p.x + if p.y > maxy: + maxy = p.y + + changed = True + while (changed): + # Step 2: move centroids: + counts = [0 for c in centroids] + for c in centroids: + c.x = 0 + c.y = 0 + for p in points: + c = centroids[p.k_index] + counts[p.k_index] += 1 + c.x += p.x + c.y += p.y + for x in xrange(len(centroids)): + if counts[x] == 0: + centroids[x].x = random.randint(minx, maxx) + centroids[x].y = random.randint(miny, maxy) + else: + centroids[x].x /= counts[x] + centroids[x].y /= counts[x] + # Step 1: allocate each point to a cluster: + changed = False + for p in points: + mindist = 0 + nearest_centroid = None + for c in centroids: + dist = distance(p, c) + if dist < mindist or nearest_centroid == None: + nearest_centroid = c + mindist = dist + if p.k_index != nearest_centroid.index: + changed = True + p.k_index = nearest_centroid.index + +# Pre-calculate the permutations for n cities, up to n = 8 +perm_tables = [] +for i in xrange(9): + perm_tables.append(generate_orders(i)) + +def brute_force_best_perm(points, perms): + # Get the best route by brute force - can do this up to about eight cities before execution time becomes prohibitive. + # (NB we don't do anything clever here, like caching partial routes etc. Why be clever when you are being a brute?) + best_length = 0 + best_order = [] + for p in perms: + length = 0 + indices = p.split(":") + indices.remove('') + p1 = points[int(indices[0])] + for i in indices: + p2 = points[int(i)] + dx = (p2.x - p1.x) * (p2.x - p1.x) + dz = (p2.y - p1.y) * (p2.y - p1.y) + length += dx + dz + p1 = p2 + if best_length == 0 or length < best_length: + best_order = indices + best_length = length + + route = [None for x in points] + i = 0 + for ind in best_order: + route[i] = points[int(ind)] + i += 1 + return route + +def get_divide_and_conquer_route(points): + route = [] + divide_and_generate_route(points, route) + return route + +BRANCH_FACTOR=7 # Split any task of >7 cities into sub-cities. +def divide_and_generate_route(points_in, route, level=0): + points = [point_node(p.x, p.y) for p in points_in] # Copy + if len(points) == 0: + return + elif len(points) > BRANCH_FACTOR: + # Too many points for brute-force to work. + # Use k-means to split into BRANCH_FACTOR groups. + centroids = [point_node(0,0) for x in xrange(BRANCH_FACTOR)] + assignKMeans(centroids, points) + # Find the best route through the centroids: + perms = perm_tables[BRANCH_FACTOR] + centroid_route = brute_force_best_perm(centroids, perms) + # And recurse: + for c in centroid_route: + cluster = [point_node(p.x, p.y) for p in points if p.k_index == c.index] + divide_and_generate_route(cluster, route, level+1) + elif len(points) == 1: + route += points + else: + # This group is small enough to solve by brute-force: + perms = perm_tables[len(points)] + route += brute_force_best_perm(points, perms) + +################################################################################################################### +# Nearest neighbour approach +################################################################################################################### + +def get_nearest_neighbour_route(in_points): + # Simply sort by greedily choosing closest point next. Works surprisingly well in our toy case (but open to attack - + # eg consider the 1d case of these points: [0,1,-2,5,-10,21...]) + points = list(in_points) + for n in xrange(len(points)-1): + p1 = points[n] + dists = [distance(p1, p) for p in points[n+1:]] + nn_ind = n+1 + dists.index(min(dists)) + tmp = points[n+1] + points[n+1] = points[nn_ind] + points[nn_ind] = tmp + n+=1 + return points + +################################################################################################################### +# Convex hull approach +################################################################################################################### + +def get_spiral_route(points): + # Just for fun - construct a spiral route by modifying the Jarvis March convex hull algorithm such that it is + # not allowed to return to any points that have already been visited. + # Definitely not the shortest route, but (aside from the fudging needed to fix the start point) it creates + # a route that looks pretty (never crosses itself) and only ever requires the agent to turn left... + + # Useful lambda: Use determinant to quickly calculate whether point p is to the left of line p1-p2 + left_of_line = lambda p1, p2, p: (p.x-p1.x)*(p2.y-p1.y) > (p.y-p1.y)*(p2.x-p1.x) + + # remove required starting point; add it back in later. + required_start_point = points.pop(0) + # find left-most point: + for p in points: + p.on_hull = False + xvalues = [p.x for p in points] + startpoint = points[xvalues.index(min(xvalues))] + bestcandidate = startpoint + hull = [] + hull.append(startpoint) + startpoint.on_hull = True + while True: + for p in points: + if (not p.on_hull) and (left_of_line(startpoint, bestcandidate, p) or bestcandidate == startpoint): + bestcandidate = p + #if bestcandidate == hull[0] and len(hull) > 1: + if len(hull) == len(points): + points.insert(0, required_start_point) + hull.append(required_start_point) + hull.reverse() + return hull + hull.append(bestcandidate) + bestcandidate.on_hull = True + startpoint = bestcandidate + +################################################################################################################### +# Genetic algorithm approach +################################################################################################################### + +def shuffle(points): + for i in xrange(1, len(points)): # Always leave starting point in place. + ind = random.randint(i, len(points) - 1) + (points[i], points[ind]) = (points[ind], points[i]) + +def get_genetic_algorithm_route(progress_callback, points, k, iters, mutation_probability, crossover_probability): + # Seems to work best with tournament selection. Never really gets particularly close to optimum solution, + # even with fancy self-adapting mutation/crossover probabilites, large generations, many iterations, etc. + # But it's cute. + + # Choose k random starting points: + generation = [] + total_fitness = 0 + roulette = False + tournament_size = int(2 * math.ceil(math.sqrt(k))) + + for i in xrange(k): + sample = list(points) + shuffle(sample) + score = 1.0 / path_length(sample) + total_fitness += score + generation.append((sample, score)) + + for it in xrange(iters): + if progress_callback != None: + progress_callback(100 * float(it) / float(iters)) + + # Create next generation: + next_gen = [] + for i in xrange(int(math.ceil(float(k) / 2.0))): + # Choose two parents: + if roulette: + # Choose by roulette: + r = random.random() * total_fitness + t = generation[0][1] + c = 0 + while t < r and c < len(generation): + c += 1 + t += generation[c][1] + parent1 = generation[c][0] + r = random.random() * total_fitness + t = generation[0][1] + c = 0 + while t < r and c < len(generation): + c += 1 + t += generation[c][1] + parent2 = generation[c][0] + else: + # Choose by tournament: + tournament=[] + for t in xrange(tournament_size): + tournament.append(random.choice(generation)) + parent1 = max(tournament, key=lambda p:p[1])[0] + tournament=[] + for t in xrange(tournament_size): + tournament.append(random.choice(generation)) + parent2 = max(tournament, key=lambda p:p[1])[0] + + # Perform crossover? + p_crossover = random.random() * crossover_probability + if random.random() <= p_crossover: + left = random.randint(0, len(points)-1) + right = left + while (right == left): + right = random.randint(0, len(points)-1) + if left > right: + (left, right) = (right, left) + subsection1 = parent1[left:right] + subsection2 = parent2[left:right] + remainder1 = [] + remainder2 = [] + for s in parent2: + if not s in subsection1: + remainder1.append(s) + for s in parent1: + if not s in subsection2: + remainder2.append(s) + child1 = remainder2[:left] + subsection2 + remainder2[left:] + child2 = remainder1[:left] + subsection1 + remainder1[left:] + else: + # No crossover - leave parents unchanged. + child1 = list(parent1) + child2 = list(parent2) + + # Perform mutation (taking care to leave starting point untouched): + p_mutation = random.random() * mutation_probability + if random.random() < p_mutation: + ma = random.randint(1, len(child1)-1) + mb = random.randint(1, len(child1)-1) + (child1[ma], child1[mb]) = (child1[mb], child1[ma]) + if random.random() < p_mutation: + ma = random.randint(1, len(child2)-1) + mb = random.randint(1, len(child2)-1) + (child2[ma], child2[mb]) = (child2[mb], child2[ma]) + + # Add to generation: + next_gen.append((child1, 1.0/path_length(child1))) + if len(next_gen) < k: # Deal with case where k is an odd number. + next_gen.append((child2, 1.0/path_length(child2))) + + # Generation completed - adjust crossover and mutation probabilites + generation = list(next_gen) + print "Gen", it, ": best path = ", path_length(max(generation, key=lambda p:p[1])[0]) + total_fitness = sum(p[1] for p in generation) + return max(generation, key=lambda p:p[1])[0] + +################################################################################################################### +# Simulated annealing approach +################################################################################################################### + +def get_simulated_annealing_route(input_points): + # Possibly the most succesful route in our toy example, relatively quick to compute for smallish numbers of cities. + points = list(input_points) + initial_temperature = math.sqrt(len(input_points)) + temperature = initial_temperature + t = 0 + alpha = 0.9 + while temperature > 0.25: + kept_bad = 0 + print "Temp: ", temperature, + dist_before = path_length(points) + for i in xrange(len(points)*len(points)): + i_from = random.randint(1, len(points)-1) + p = points.pop(i_from) + i_to = random.randint(1, len(points)) + points.insert(i_to, p) + dist_after = path_length(points) + delta = dist_before - dist_after + if delta < 0: + # Did not improve things. + prop = math.exp(delta/temperature) + if random.random() > prop: + # Reject this move. + points.pop(i_to) + points.insert(i_from, p) + #print "rejected" + else: + # Kept this move. + dist_before = dist_after + kept_bad += 1 + #print "dn:", delta + else: + # Kept this move. + dist_before = dist_after + #print "up:", delta + t += 1 + temperature = 10 * (alpha**t) + print "length: ", dist_before, + print "bad moves kept:", kept_bad + return points + + +################################################################################################################### +# Drawing code +################################################################################################################### + +def clear_screen(w): + w.delete("all") + +def draw_points(w, points, r=4, c=None): + fills=["#ff00ff", "#ffff00", "#00ffff", "#ff0000", "#00ff00", "#0000ff", "#880088", "#888800", "#008888", "#880000", "#008800", "#000088"] + + for p in points: + x = p.get_position()[0] + y = p.get_position()[1] + + if c != None: + fill = c + elif hasattr(p, 'k_index'): + fill = fills[p.k_index % len(fills)] + elif hasattr(p, 'index'): + fill = fills[p.index % len(fills)] + else: + fill = "#dddddd" + w.create_oval(4*(x + 50)-r, 4*(y + 50)-r, 4*(x + 50)+r, 4*(y + 50)+r, fill=fill) + +def draw_tree(w, tree, line_width=2, line_colour = "#000000"): + for e in tree: + x1 = e.point1.get_position()[0] + y1 = e.point1.get_position()[1] + x2 = e.point2.get_position()[0] + y2 = e.point2.get_position()[1] + w.create_line(4*(x1 + 50), 4*(y1 + 50), 4*(x2 + 50), 4*(y2 + 50), width=line_width, fill=line_colour) + +def draw_path(w, points, line_width=2, line_colour = "#00ff00"): + x = points[0].get_position()[0] + y = points[0].get_position()[1] + for o in points: + nx = o.get_position()[0] + ny = o.get_position()[1] + w.create_line(4*(x+50), 4*(y+50), 4*(nx+50), 4*(ny+50), width=line_width, fill=line_colour) + x = nx + y = ny + +master = Tk() +w = Canvas(master, width=400, height=400) +w.pack() + +def GetMissionXML(summary, agentnames): + xml = ''' + + + ''' + summary + ''' + + + + 20 + + + + + + + + + ''' + getCitiesDrawingXML(points) + ''' + + + ''' + + for an in agentnames: + xml += ''' + + ''' + an + ''' + + + + + + + + + + + + ''' + return xml + '' + +def getCitiesDrawingXML(points): + ''' Build an XML string that contains a square for each city''' + xml = "" + for p in points: + x = str(p.x) + z = str(p.y) + xml += '' + xml += '' + return xml + +class RouteGenerators: + NearestNeighbour, Genetic, DivideAndConquer, MinSpanTree, Spiral, Annealing = range(6) + Generators = {NearestNeighbour:lambda progress_callback, points : get_nearest_neighbour_route(points), + Genetic:lambda progress_callback, points : get_genetic_algorithm_route(None, points, 20, 3000, 0.7, 0.9), + DivideAndConquer:lambda progress_callback, points : get_divide_and_conquer_route(points), + MinSpanTree:lambda progress_callback, points : get_MST_route(points), + Spiral:lambda progress_callback, points : get_spiral_route(points), + Annealing:lambda progress_callback, points : get_simulated_annealing_route(points)} + DisplayNames = ["Nearest Neighbour", "Genetic Algorithm", "Divide-and-conquer", "Minimum-Span-Tree", "Modified Convex Hull", "Simulated Annealing"] + AgentNames = ["NearestN", "GenAl", "DivConq", "MinSpan", "ConvHull", "SimAnn"] + +class Manager(object): + def __init__(self, root, canvas, points): + self.data = {} + self.points = points + self.valid = True + self.canvas = canvas + self.root = root + draw_points(self.canvas, points, 4, "#ff00dd") + self.update() + self.finished_agents = 0 + + def newPosition(self, agent, point, text): + if not agent in self.data: + self.data[agent]={"pos":point_node(0,0), "target":point_node(0,0)} + self.data[agent]["last_pos"] = self.data[agent]["pos"] + self.data[agent]["pos"] = point + self.data[agent]["text"] = text + self.valid = False + + def finished(self, agent): + self.finished_agents += 1 + if self.finished_agents == len(self.data): + self.root.quit() + + def draw(self): + for agent in self.data: + fill = ["#ff0000","#00ff00","#0000ff","#ff00ff","#ffff00","#00ffff","#000000","#ffffff"][agent % 8] + if "canvas_item" in self.data[agent]: + self.canvas.delete(self.data[agent]["canvas_item"]) + if "canvas_text_item" in self.data[agent]: + self.canvas.delete(self.data[agent]["canvas_text_item"]) + if "last_pos" in self.data[agent]: + point = self.data[agent]["last_pos"] + if point != None: + x, y = point.x, point.y + r = 1 + self.canvas.create_oval(4*(x + 50)-r, 4*(y + 50)-r, 4*(x + 50)+r, 4*(y + 50)+r, fill=fill, outline=fill) + self.data[agent]["last_pos"] = None + point = self.data[agent]["pos"] + x, y = point.x, point.y + r = 3 + self.data[agent]["canvas_item"]=self.canvas.create_oval(4*(x + 50)-r, 4*(y + 50)-r, 4*(x + 50)+r, 4*(y + 50)+r, fill=fill) + self.data[agent]["canvas_text_item"]=self.canvas.create_text(4*(x + 50)+10, 4*(y + 50)+10, text=self.data[agent]["text"]) + + self.root.update() + self.valid = True + + def update(self): + if not self.valid: + self.draw() + self.root.after(50, self.update) + +class SalesmanAgent(threading.Thread): + def __init__(self, role, routeType, clientPool, missionXML, points, manager): + threading.Thread.__init__(self) + self.role = role + self.route_generator = routeType + self.client_pool = clientPool + self.mission_xml = missionXML + self.agent_host = MalmoPython.AgentHost() + self.mission = MalmoPython.MissionSpec(missionXML, True) + self.mission_record = MalmoPython.MissionRecordSpec("tsp_" + str(self.role) + ".tgz") + self.mission_record.recordCommands() + + self.points = [point_node(p.x, p.y) for p in points] # take a copy + self.manager = manager + + def run(self): + used_attempts = 0 + max_attempts = 5 + while True: + try: + # Attempt to start the mission: + self.agent_host.startMission(self.mission, self.client_pool, self.mission_record, self.role, "TSPExperiment") + break + except MalmoPython.MissionException as e: + errorCode = e.details.errorCode + if errorCode == MalmoPython.MissionErrorCode.MISSION_SERVER_WARMING_UP: + # Server not quite ready yet - waiting... + time.sleep(2) + elif errorCode == MalmoPython.MissionErrorCode.MISSION_INSUFFICIENT_CLIENTS_AVAILABLE: + # Not enough available Minecraft instances running. + used_attempts += 1 + if used_attempts < max_attempts: + time.sleep(2) + elif errorCode == MalmoPython.MissionErrorCode.MISSION_SERVER_NOT_FOUND: + # Server not found - has the mission with role 0 been started yet? + used_attempts += 1 + if used_attempts < max_attempts: + time.sleep(2) + else: + print "Other error:", e.message + print "Bailing immediately." + exit(1) + if used_attempts == max_attempts: + print "Failed to start mission - bailing now." + exit(1) + + start_time = time.time() + world_state = self.agent_host.getWorldState() + while not world_state.has_mission_begun: + time.sleep(0.1) + world_state = self.agent_host.getWorldState() + if len(world_state.errors) > 0: + for err in world_state.errors: + print err + exit(1) + if time.time() - start_time > 120: + print "Mission failed to begin within two minutes - did you forget to start the other agent?" + exit(1) + self.route = self.calculateRoute(self.points) + self.runMissionLoop() + print RouteGenerators.DisplayNames[self.route_generator], "agent has finished!" + + def calculateRoute(self, points): + return RouteGenerators.Generators[self.route_generator](self, points) + + def onRouteCalculationProgress(self, percentage): + pass + + def angvel(self, target, current, scale): + '''Use sigmoid function to choose a delta that will help smoothly steer from current angle to target angle.''' + delta = target - current + while delta < -180: + delta += 360; + while delta > 180: + delta -= 360; + return (2.0 / (1.0 + math.exp(-delta/scale))) - 1.0 + + def runMissionLoop(self): + turn_key = "" + yawToPoint = lambda point, x, y, z: -180 * math.atan2(point.x-x, point.y-z) / math.pi + self.currentCity = 0 + self.agent_host.sendCommand("move 1") # Full speed ahead... + while (True): + world_state = self.agent_host.getWorldState() + if not world_state.is_mission_running: + return + if world_state.number_of_observations_since_last_state > 0: + obvsText = world_state.observations[-1].text + data = json.loads(obvsText) # observation comes in as a JSON string... + current_x = data.get(u'XPos', 0) + current_z = data.get(u'ZPos', 0) + current_y = data.get(u'YPos', 0) + self.manager.newPosition(self.role, point_node(current_x, current_z), RouteGenerators.DisplayNames[self.route_generator]) + yaw = data.get(u'Yaw', 0) + pitch = data.get(u'Pitch', 0) + if distance(point_node(current_x-0.5, current_z-0.5), self.route[self.currentCity]) < 1: + self.currentCity += 1 + chat_string = "Reached " + str(self.currentCity) + "/" + str(len(self.route)) + self.agent_host.sendCommand("chat " + chat_string) + if self.currentCity >= len(self.route): + self.agent_host.sendCommand("turn 0") + self.agent_host.sendCommand("move 0") + self.agent_host.sendCommand("jump 1") + self.agent_host.sendCommand("quit") + self.manager.finished(self.role) + return # Finished! + yaw_to_next_point = yawToPoint(self.route[self.currentCity], current_x-0.5, current_y, current_z-0.5) + turn_speed = self.angvel(yaw_to_next_point, yaw, 16.0) + move_speed = (1.0 - abs(turn_speed)) * (1.0 - abs(turn_speed)) + self.agent_host.sendCommand("turn " + str(turn_speed)) + self.agent_host.sendCommand("move " + str(move_speed)) + time.sleep(0.001) + +sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) # flush print output immediately + +# Create a pool of Minecraft Mod clients. +# By default, mods will choose consecutive mission control ports, starting at 10000, +# so running four mods locally should produce the following pool by default (assuming nothing else +# is using these ports): +my_client_pool = MalmoPython.ClientPool() +my_client_pool.add(MalmoPython.ClientInfo("127.0.0.1", 10000)) +my_client_pool.add(MalmoPython.ClientInfo("127.0.0.1", 10001)) +my_client_pool.add(MalmoPython.ClientInfo("127.0.0.1", 10002)) +my_client_pool.add(MalmoPython.ClientInfo("127.0.0.1", 10003)) +my_client_pool.add(MalmoPython.ClientInfo("127.0.0.1", 10004)) +my_client_pool.add(MalmoPython.ClientInfo("127.0.0.1", 10005)) + +# Create one agent host for parsing: +parser = MalmoPython.AgentHost() +options = [("nn", "n", RouteGenerators.NearestNeighbour, True), + ("gen-al", "g", RouteGenerators.Genetic, False), + ("div-and-conq", "d", RouteGenerators.DivideAndConquer, False), + ("mst", "m", RouteGenerators.MinSpanTree, True), + ("conv-hull", "c", RouteGenerators.Spiral, False), + ("sa", "s", RouteGenerators.Annealing, True)] + +for opt in options: + parser.addOptionalFlag(opt[0] + "," + opt[1], "Add " + RouteGenerators.DisplayNames[opt[2]] + " agent") + +parser.addOptionalIntArgument("points,p", "Number of points to use", 50) + +try: + parser.parse( sys.argv ) +except RuntimeError as e: + print 'ERROR:',e + print parser.getUsage() + exit(1) +if parser.receivedArgument("help"): + print parser.getUsage() + exit(0) + +# Parse the command-line options: +TOTAL_POINTS = parser.getIntArgument("points") +INTEGRATION_TEST_MODE = parser.receivedArgument("test") + +agentnames = [RouteGenerators.AgentNames[x[2]] for x in options if parser.receivedArgument(x[0])] +if not len(agentnames): + agentnames = [RouteGenerators.AgentNames[x[2]] for x in options if x[3]] + +# Create some data: +points = [point_node(0,0)] # Fix start point at the centre +for i in xrange(TOTAL_POINTS-1): + points.append(point_node(random.randint(-50,50), random.randint(-50,50))) + +# Create mission xml: +xml = GetMissionXML("Travelling Salesfolk!", agentnames) + +# Create the agents: +manager = Manager(master, w, points) +agents = [] + +role = 0 +for opt in options: + if parser.receivedArgument(opt[0]): + agents.append(SalesmanAgent(role, opt[2], my_client_pool, xml, points, manager)) + role += 1 +if not len(agents): + print "No agents specified - using defaults" + for opt in options: + if opt[3]: + agents.append(SalesmanAgent(role, opt[2], my_client_pool, xml, points, manager)) + role += 1 + +# Start them all off... +for agent in agents: + agent.start() + +# And wait. +mainloop() + +for agent in agents: + agent.join() + +# Allow user time to admire the finished plot: +if not INTEGRATION_TEST_MODE: + nb = raw_input('Press enter to quit')