astar/0000775000175000017500000000000010334262633010254 5ustar jaejaeastar/map/0000755000175000017500000000000010334260617011027 5ustar jaejaeastar/map/terrain.py0000644000175000017500000000054210332055030013033 0ustar jaejae# g = grasslands # w = woods # a = water terrain_types = { 'g':1.0, 'a':0.01, # avoid division by 0 'w':0.5} class Terrain(object): def __init__(self,ttype): object.__init__(self) self.ttype = ttype def movementSpeedModifier(self, unit): # ignore unit for now return terrain_types[self.ttype] astar/map/map.py0000644000175000017500000000251210332053634012153 0ustar jaejaefrom hex import Hex class Map(object): def __init__(self, asci_map): object.__init__(self) xsize = len(asci_map[0]) ysize = len(asci_map) self.size = ( xsize, ysize ) # initialize the array of hexes self.hexes = hexes = [ None ] * ysize for yindex in range (ysize): hexes[yindex] = [ None ] * xsize for xindex in range(xsize): hexes[yindex][xindex]=Hex(asci_map[yindex][xindex]) def getHex(self, x, y): return self.hexes[y][x] def getSize(self): return self.size def __getNeighbors__(self, hexx, hexy): if hexy % 2 == 0: return [(hexx,hexy-1,1),(hexx+1,hexy,2),(hexx,hexy+1,4), (hexx-1,hexy+1,8),(hexx-1,hexy,16),(hexx-1,hexy-1,32)] else: return [(hexx+1,hexy-1,1),(hexx+1,hexy,2),(hexx+1,hexy+1,4), (hexx,hexy+1,8),(hexx-1,hexy,16),(hexx,hexy-1,32)] def getNeighbors(self, hexx, hexy): """Returns valid neighbors as a list of (x, y, bit) tuples. The bit value starts at up-right, going clockwise.""" neighbors = self.__getNeighbors__(hexx, hexy) size = self.size return [(hx,hy,bit) for (hx,hy,bit) in neighbors if not (hx >= size[0] or hy >= size[1] or hx < 0 or hy < 0)] astar/map/README0000644000175000017500000000027110334256130011702 0ustar jaejaeThis is a simplified map/hex/terrain setup used with the test script based on a severely hacked/stripped version of some of the code from civil [1]. [1] http://civil.sourceforge.net/ astar/map/hex.py0000644000175000017500000000032610332053634012163 0ustar jaejaefrom terrain import Terrain class Hex(object): def __init__(self,terrain=None): object.__init__(self) self.terrain = Terrain(terrain) def getTerrain (self): return self.terrain astar/map/__init__.py0000644000175000017500000000000010332052467013126 0ustar jaejaeastar/heap.py0000644000175000017500000000031610332057767011551 0ustar jaejae from heapq import heappop,heappush class Heap(list): def hpush(self,cost,item,heappush=heappush): heappush(self,(cost,item)) def hpop(self,heappop=heappop): return heappop(self) astar/pathfinder.py0000644000175000017500000001152410334255577012764 0ustar jaejae# library python imports from math import floor,ceil,sqrt from operator import add from array import array from heap import Heap ### MAP_SHAPE determines which distance cost algorithms get used #MAP_SHAPE = 'diamond_v' # diamond shaped - stacked vertically #MAP_SHAPE = 'diamond_h' # diamond shaped - stacked horizonally #MAP_SHAPE = 'square_sg' # grid maps that don't allow diagonal movement #MAP_SHAPE = 'square_sd' # grid maps that allow diagonal movement MAP_SHAPE = 'rectangle' # array like layout # A* distance cost algorithms. Either C or python. from distance_cost import dc_algorithm class Pathfinder(object): """ Some documentation here. """ def __init__(self,map): """Initializes the pathfinder. """ self.map = map self._init_terrain_cost() # The static terrain cost makes 2 assumptions; first that the terrain # doesn't change, second that units have the same movement costs # the cost calculation is oversimplified. but works well for now. # # Note: This no longer works as the terrain costs are different for each # type of unit. Should there be a terrain map created for each unit type or # should these be calculated on the fly. Classic memory/speed tradeoff. # Though terrain maps don't use much memory, so I'm leading toward multiple # maps. tc_map = None def _init_terrain_cost(self): if self.tc_map: return xsize,ysize = self.map.size getHex = self.map.getHex tc_map = self.tc_map = [] # temporary hack until units can be handled class Unit: # fake infantry unit def getType(self): return 0 unit = Unit() tc_map[:] = [array('f', [0] * xsize) for i in range(ysize)] for y in range(ysize): for x in range(xsize): tc_map[y][x] = getHex(x,y).terrain.movementSpeedModifier(unit) # node = (g_cost,depth,point,parent_node) # point is both the x,y coord and the state # g_cost is cost so far def _new_node(self,p,parent,g_cost): if not parent: return (0,0,p,parent) else: return (g_cost,parent[1]+1,p,parent) # getHexAdjacent() includes hexes off the edge of the map. def _in_bounds(self,(x,y)): xedge,yedge = self.map.size return x > -1 and y > -1 and x < xedge and y < yedge def calculatePath(self,start,end,_test=0): """ Return the path as a list of coordinates. if _test, then pass back 2 lists; path and nodes checked but not used in path """ tc_map = self.tc_map p = start p1 = end # use heap as priority queue node_queue = Heap() # stores all nodes already checked - this assumes the hex will # have the same movement cost no matter how its traversed skip_nodes = {} # some micro optimizations distance_cost=dc_algorithm[MAP_SHAPE] getNeighbors = self.map.getNeighbors new_node = self._new_node # Classic A* does while loop over queue (len test) and uses a break on # the location matching test. I found it easier to reverse these 2 # because of python's handy while: else: syntax node = new_node(p,None,0) while node[2] != p1: x,y = node[2] new_points = [(x,y) for (x,y,b) in getNeighbors(x,y)] parent_cost = node[0] for pN in new_points: if skip_nodes.has_key(pN): f,g = skip_nodes[pN] else: terrain = tc_map[pN[0]][pN[1]] # h is the heuristic of future cost # assumes an average movement cost of 1 h = distance_cost(pN,p1) actual_cost = 1/terrain # g is the cost so far g = parent_cost + actual_cost f = h + g node_queue.hpush(f,new_node(pN,node,g)) skip_nodes[pN]=f,g if not node_queue: path = [] break node_cost,node = node_queue.hpop() else: # what we're here for path = [] while node: #get path path.append(node[2]) node = node[3] path.reverse() if _test: return path,skip_nodes else: return path ## pseudo-code for path smoothing. ignore for now. def simpleSmooth(self,path): assert type(path) is ListType and path, 'Bad arg type: simpleSmooth([list])' checkPoint = path[0] currentPoint = path[1] for i in len(path): if traversable(checkPoint, currentPoint): del path[1] currentPoint = path[1] else: checkPoint, currentPoint = currentPoint astar/test_path.py0000755000175000017500000001055510334260526012627 0ustar jaejae#!/usr/bin/env python import sys from random import randint from pathfinder import Pathfinder # map display formatting functions red = "\033[0;31m" green = "\033[1;32m" dgreen = "\033[0;32m" yellow = "\033[1;33m" dyellow = "\033[0;33m" magenta = "\033[0;35m" blue = "\033[0;34m" normal = "\033[0m" def hl(xy): return "%s%s%s" % (magenta,xy,normal) def sprint(text): sys.stdout.write(text) # ### methods to display map and path # class DbgPath(Pathfinder): def __init__(self,*args): self.tc_map = [] return Pathfinder.__init__(self,*args) def show_path(self,test_prob): self.path,self.skip_nodes = \ self.calculatePath(_test=1,*test_prob) self.show_map() def show_map(self): self.map_key() xsize = self.map.size[0] for x in range(xsize): sprint(" / \\") print self.show_rows() print "Path Length: %s" % len(self.path) def show_rows(self,y=0,odd=0,me=None): xsize, ysize = self.map.getSize() if ysize == y: return y if odd: print " ", sprint("|"); for x in range(xsize): # on small maps show coords if xsize > 10 or ysize > 10: self.big_coord_print(x,y) else: self.coord_print(x,y) print if odd: print " /", for x in range(xsize): sprint(" \ /") if odd: odd = 0 else: sprint(" \\") odd=1 print if not me: me = self.show_rows return me(y+1,odd,me) def coord_print(self,x,y): t_cost = self.tc_map[y][x] if t_cost > 0.9: thl = green elif t_cost > .7: thl = dgreen elif t_cost > .5: thl = yellow elif t_cost > .3: thl = dyellow elif t_cost < 0.2: thl = blue else: thl = red if (x,y) in self.path: sprint("%s%s,%s%s|" % (hl(x),thl,normal,hl(y))) else: sprint("%s%s,%s%s|" % (thl,x,y,normal)) def big_coord_print(self,x,y): t_cost = self.tc_map[x][y] if t_cost > 0.9: thl = green elif t_cost > .7: thl = dgreen elif t_cost > .5: thl = yellow elif t_cost > .3: thl = dyellow elif t_cost < 0.1: thl = blue else: thl = red if (x,y) in self.path: sprint("%s|%s%s%s|%s|" % (thl,normal,hl('*'),thl,normal)) elif self.skip_nodes.has_key((x,y)): sprint("%s|#|%s|" % (thl,normal)) else: sprint(" %s#%s |" % (thl,normal)) def map_key(self): print "------------------------------------------------------" print " Terrain costs range from 0-1 with 0 being impassable" print " %s1-.9%s " % (green,normal), print "%s.9-.7%s " % (dgreen,normal), print "%s.7-.5%s " % (yellow,normal), print "%s.5-.3%s " % (dyellow,normal), print "%s.1-.2%s " % (blue,normal), print "%s0-.1%s" % (red,normal) print " magenta asterisks %s mark the path" % hl('*') print " surrounding bards %s|#|%s mark searched nodes" % (green,normal) print "------------------------------------------------------" # These are point sets that I found useful for finding problems with my # implementation. prob1 = ((4,1),(1,3)) prob2 = ((4,1),(0,7)) prob3 = ((0,7),(4,1)) prob4 = ((2,2),(11,11)) prob5 = ((5,0),(11,11)) # create the test environ... change map size here test_prob = prob5 # 12x12 map1 = [ "ggggggggggggg", "ggggggggggggg", "ggggggggggggg", "ggaaggggggggg", "ggaaawwwggggg", "ggaaaaaaaaaaa", "ggaaaaawwgggw", "ggaaaaawwwwww", "gwwwwwwwwwwww", "gwgwwwwgwwwww", "wggwwwwwwwwww", "ggggggggggggw", "wgwwwwgwwgggw" ] maps = [] from map.map import Map for i in range(1): map = Map(map1) path = DbgPath(map) maps.append(path) # display's a ascii map # key: red|yellow|green = high/medium/low terrain costs # magenta is used to show the path path.show_path(test_prob) print path.path #import time #time.sleep(3) # profiling: just run the path calculation method #import profile #def test(maps=maps): # while maps: # i = randint(0,len(maps)-1) # path = maps[i] ## path.show_path() # path.calculatePath(*test_prob) ## print 'path length:',len(path.calculatePath(*test_prob)) # del maps[i] #profile.run("test()") astar/setup.cfg0000644000175000017500000000002610332062104012056 0ustar jaejae[build_ext] inplace=1 astar/distance_cost.py0000644000175000017500000000462210334256373013456 0ustar jaejae try: import _distance_cost # specify distance cost function with map type dc_algorithm = { 'diamond_v':_distance_cost.distance_cost_dv, 'diamond_h':_distance_cost.distance_cost_dh, 'rectangle':_distance_cost.distance_cost_r, 'square_sg':_distance_cost.distance_cost_sg, 'square_sd':_distance_cost.distance_cost_sd, } except ImportError: from math import floor,ceil # Thanks Amit! # http://www-cs-students.stanford.edu/~amitp/Articles/HexLOS.html # Note: I'm using x,y coord to represent the opposite what amit does # This is mainly to allow for more seemless working with Numeric # distance cost helper functions def same_sign(n,n1): return (n > -1) == (n1 > -1) def a2h((x,y)): return (int(x - floor(float(y)/2)),int(x+ceil(float(y)/2))) def h2a((x,y)): return (int(floor(float(x+y)/2)),y-x) # For use with rectangular hex maps - usually a 2D array # The a2h and h2a algorithms are transforms needed here and to convert back # the path list below (XXX below) def r_distance_cost(p,p1): x,y = a2h(p) x1,y1 = a2h(p1) dx = x1-x dy = y1-y if same_sign(dx,dy): return max(abs(dx),abs(dy)) else: return (abs(dx) + abs(dy)) # Diamond shaped maps # used for vertically stacked hex maps def dv_distance_cost((x,y),(x1,y1)): dx = x1-x dy = y1-y if same_sign(dx,dy): return max(abs(dx),abs(dy)) else: return (abs(dx) + abs(dy)) # used for horizonally stacked hex maps def dh_distance_cost((x,y),(x1,y1)): dx = x1-x dy = y1-y if not same_sign(dx,dy): return max(abs(dx),abs(dy)) else: return (abs(dx) + abs(dy)) # for square grid maps that don't allow diagonal movement def sg_distance_cost((x,y),(x1,y1)): dx = x1-x dy = y1-y return (abs(dx) + abs(dy)) # for square grid maps that do allow diagonal movement def sd_distance_cost((x,y),(x1,y1)): dx = x1-x dy = y1-y return max(abs(dx),abs(dy)) # specify distance cost function with map type dc_algorithm = { 'diamond_v':dv_distance_cost, 'diamond_h':dh_distance_cost, 'rectangle':r_distance_cost, 'square_sg':sg_distance_cost, 'square_sd':sd_distance_cost, } astar/setup.py0000644000175000017500000000103710332064000011747 0ustar jaejaefrom distutils.core import setup,Extension setup(name="astar", version="1.0", author="John Eikenberry", author_email="jae@zhar.net", license="Public Domain", platforms=["linux","windows","darwin","*nix"], url="http://zhar.net/projects/python/", description="Python A* implementation", long_description="""A* for hex game maps.""", py_modules=["pathfinder","distance_cost","heap","test_path", "map.map","map.hex","map.terrain",], ext_modules=[Extension("_distance_cost",["_distance_cost.c"])]) astar/_distance_cost.c0000644000175000017500000000606210332062614013376 0ustar jaejae #include #include #include #define sign(n) (n > -1) #define a2h_x(x, y) (x - floor(y/2.0)) #define a2h_y(x, y) (x + ceil(y/2.0)) #define h2a_x(x, y) (floor((x+y)/2.0)) #define h2a_y(x, y) (y - x) #define max(x,y) (x>y?x:y) /* Prototypes */ static PyObject * dc_distance_cost_r(PyObject *self, PyObject *args); static PyObject * dc_distance_cost_dv(PyObject *self, PyObject *args); static PyObject * dc_distance_cost_dh(PyObject *self, PyObject *args); static PyObject * dc_distance_cost_sg(PyObject *self, PyObject *args); static PyObject * dc_distance_cost_sd(PyObject *self, PyObject *args); void initdc(void); static PyObject * dc_distance_cost_r(self,args) PyObject *self; PyObject *args; { int *x, *y, *xx, *yy; int cost, dx, dy; if (!PyArg_ParseTuple(args,"(ii)(ii)",&x,&y,&xx,&yy)) return NULL; dx = a2h_x((int)xx,(int)yy) - a2h_x((int)x,(int)y); dy = a2h_y((int)xx,(int)yy) - a2h_y((int)x,(int)y); if (sign(dx) == sign(dy)) cost = max(abs(dx),abs(dy)); else cost = abs(dx)+abs(dy); return Py_BuildValue("i",cost); }; static PyObject * dc_distance_cost_dv(self,args) PyObject *self; PyObject *args; { int *x, *y, *xx, *yy; int cost, dx, dy; if (!PyArg_ParseTuple(args,"(ii)(ii)",&x,&y,&xx,&yy)) return NULL; dx = (int)xx - (int)x; dy = (int)yy - (int)y; if (sign(dx) == sign(dy)) cost = max(abs(dx),abs(dy)); else cost = abs(dx)+abs(dy); return Py_BuildValue("i",cost); }; static PyObject * dc_distance_cost_dh(self,args) PyObject *self; PyObject *args; { int *x, *y, *xx, *yy; int cost, dx, dy; if (!PyArg_ParseTuple(args,"(ii)(ii)",&x,&y,&xx,&yy)) return NULL; dx = (int)xx - (int)x; dy = (int)yy - (int)y; if (sign(dx) != sign(dy)) cost = max(abs(dx),abs(dy)); else cost = abs(dx)+abs(dy); return Py_BuildValue("i",cost); }; static PyObject * dc_distance_cost_sg(self,args) PyObject *self; PyObject *args; { int *x, *y, *xx, *yy; int cost, dx, dy; if (!PyArg_ParseTuple(args,"(ii)(ii)",&x,&y,&xx,&yy)) return NULL; dx = (int)xx - (int)x; dy = (int)yy - (int)y; cost = abs(dx)+abs(dy); return Py_BuildValue("i",cost); }; static PyObject * dc_distance_cost_sd(self,args) PyObject *self; PyObject *args; { int *x, *y, *xx, *yy; int cost, dx, dy; if (!PyArg_ParseTuple(args,"(ii)(ii)",&x,&y,&xx,&yy)) return NULL; dx = (int)xx - (int)x; dy = (int)yy - (int)y; cost = max(abs(dx),abs(dy)); return Py_BuildValue("i",cost); }; static PyMethodDef dcMethods[] = { {"distance_cost_r",dc_distance_cost_r,METH_VARARGS}, {"distance_cost_dv",dc_distance_cost_dv,METH_VARARGS}, {"distance_cost_dh",dc_distance_cost_dh,METH_VARARGS}, {"distance_cost_sg",dc_distance_cost_sg,METH_VARARGS}, {"distance_cost_sd",dc_distance_cost_sd,METH_VARARGS}, {NULL, NULL} /* Sentinel */ }; void init_distance_cost(void) { (void) Py_InitModule("_distance_cost", dcMethods); };