""" This module holds functions, exceptions and constants
that are or might be used by both the game, renderer
and perhaps the agents. By putting this code in a separate
module, each of them can access it without requiring
the other modules.
"""
### IMPORTS ###
import math
import time
import copy
from pprint import pprint
from heapq import heappush, heappop
from sys import maxint
# Local libs
from libs import astar
# Shortcuts
sqrt = math.sqrt
try:
inf = float('inf')
except ValueError:
inf = 1e1000000
pi = math.pi
astar = astar.astar
### EXCEPTIONS ###
class GameInterrupt(Exception):
pass
### LISTS ###
def all_pairs(seq):
l = len(seq)
for i in range(l):
for j in range(i+1, l):
yield seq[i], seq[j]
### NUMERICAL ###
[docs]def frange(limit1, limit2 = None, increment = 1.):
""" Like xrange, but for real numbers.
"""
if limit2 is None:
limit2, limit1 = limit1, 0.
else:
limit1 = float(limit1)
count = int(math.ceil((limit2 - limit1)/increment))
return (limit1 + n*increment for n in xrange(count))
[docs]def mean(iterable):
""" Returns mean of given list or generator."""
s = 0.0
n = 0
for num in iterable:
s += num
n += 1
return s/n
[docs]def stdev(iterable):
""" Returns standard deviation of given list or generator.
>>> stdev([1,2,3])
1.0
"""
nums = list(iterable)
n = len(nums)
avg = mean(nums)
return sum((a - avg)**2 for a in nums)/float(max(n-1,1))
### GEOMETRY ###
[docs]def point_add(a, b):
""" Add the coordinates of two points
(Inline this if you can, function calls are slow)
"""
return (a[0] + b[0], a[1] + b[1])
[docs]def point_sub(a, b):
""" Subtract two 2d vectors
(Inline this if you can, function calls are slow)
"""
return (a[0] - b[0], a[1] - b[1])
[docs]def point_mul(a, f):
""" Multiply a vector by a scalar
(Inline this if you can, function calls are slow)
"""
return (a[0]*f, a[1]*f)
[docs]def point_dist(a, b):
""" Distance between two points. """
return ((a[0]-b[0]) ** 2 + (a[1]-b[1]) ** 2) ** 0.5
[docs]def line_intersects_rect(p0, p1, r):
""" Check where a line between p1 and p2 intersects
given axis-aligned rectangle r.
Returns False if no intersection found.
Uses the Liang-Barsky line clipping algorithm.
>>> line_intersects_rect((1.0,0.0),(1.0,4.0),(0.0,1.0,4.0,1.0))
((0.25, (1.0, 1.0)), (0.5, (1.0, 2.0)))
>>> line_intersects_rect((1.0,0.0),(3.0,0.0),(0.0,1.0,3.0,1.0))
False
"""
l,t,r,b = (r[0],r[1],r[0]+r[2],r[1]+r[3])
p0x,p0y = p0
q0x,q0y = p1
t0,t1 = 0.0, 1.0
dx, dy = p1[0] - p0[0], p1[1] - p0[1]
for edge in xrange(4):
if edge == 0:
p,q = -dx, -(l-p0x)
elif edge == 1:
p,q = dx, (r-p0x)
elif edge == 2:
p,q = -dy, -(t-p0y)
else:
p,q = dy, (b-p0y)
if p == 0: # Parallel line
if q < 0:
return False
else:
ti = q/float(p)
if p < 0:
if ti > t1:
return False
elif ti > t0:
t0 = ti
else:
if ti < t0:
return False
elif ti < t1:
t1 = ti
# Return (two) intersection coords
return ((t0, (p0x + t0*dx, p0y + t0*dy)), (t1, (p0x + t1*dx, p0y + t1*dy)))
[docs]def line_intersects_circ((p0x,p0y), (p1x,p1y), (cx,cy), r):
""" Computes intersections between line and circle. The line
runs between (p0x,p0y) and (p1x,p1y) and the circle
is centered at (cx,cy) with a radius r.
Returns False if no intersection is found, and one or two intersection points otherwise.
Intersection points are (t, (x, y)) where t is the distance along the line between 0-1.
(From stackoverflow.com/questions/1073336/circle-line-collision-detection)
>>> line_intersects_circ((0,0), (4,0), (2,0), 1)
[(0.25, (1.0, 0.0)), (0.75, (3.0, 0.0))]
>>> line_intersects_circ((0,0), (2,0), (2,0), 1)
[(0.5, (1.0, 0.0))]
>>> line_intersects_circ((0,0), (0,1), (2,0), 1)
False
"""
dx, dy = p1x-p0x, p1y-p0y
fx, fy = p0x-cx, p0y-cy
a = dx*dx + dy*dy
b = 2 * (dx*fx + dy*fy)
c = (fx*fx + fy*fy) - r*r
discriminant = b*b-4*a*c;
if discriminant < 0:
return False
else:
# ray didn't totally miss sphere, so there is a solution to the equation.
discriminant = sqrt( discriminant );
t1 = (-b - discriminant)/(2*a);
t2 = (-b + discriminant)/(2*a);
isects = []
if t1 >= 0 and t1 <= 1:
p1 = p0x + dx*t1, p0y + dy*t1
isects.append((t1,p1))
if t2 >= 0 and t2 <= 1:
p2 = p0x + dx*t2, p0y + dy*t2
isects.append((t2,p2))
if not isects:
return False
else:
return isects
# // use t2 for second point
[docs]def line_intersects_grid((x0,y0), (x1,y1), grid, grid_cell_size=1):
""" Performs a line/grid intersection, finding the "super cover"
of a line and seeing if any of the grid cells are occupied.
The line runs between (x0,y0) and (x1,y1), and (0,0) is the
top-left corner of the top-left grid cell.
>>> line_intersects_grid((0,0),(2,2),[[0,0,0],[0,1,0],[0,0,0]])
True
>>> line_intersects_grid((0,0),(0.99,2),[[0,0,0],[0,1,0],[0,0,0]])
False
"""
grid_cell_size = float(grid_cell_size)
x0 = x0 / grid_cell_size
x1 = x1 / grid_cell_size
y0 = y0 / grid_cell_size
y1 = y1 / grid_cell_size
dx = abs(x1 - x0)
dy = abs(y1 - y0)
x = int(math.floor(x0))
y = int(math.floor(y0))
if dx != 0:
dt_dx = 1.0 / dx
else:
dt_dx = inf
if dy != 0:
dt_dy = 1.0 / dy
else:
dt_dy = inf
t = 0.0
n = 1
if (dx == 0):
x_inc = 0
t_next_horizontal = dt_dx
elif (x1 > x0):
x_inc = 1
n += int(math.floor(x1)) - x
t_next_horizontal = (math.floor(x0) + 1 - x0) * dt_dx
else:
x_inc = -1
n += x - int(math.floor(x1))
t_next_horizontal = (x0 - math.floor(x0)) * dt_dx
if (dy == 0):
y_inc = 0
t_next_vertical = dt_dy
elif (y1 > y0):
y_inc = 1
n += int(math.floor(y1)) - y
t_next_vertical = (math.floor(y0) + 1 - y0) * dt_dy
else:
y_inc = -1
n += y - int(math.floor(y1))
t_next_vertical = (y0 - math.floor(y0)) * dt_dy
while (n > 0):
if grid[y][x] == 1:
return True
if (t_next_vertical < t_next_horizontal):
y += y_inc
t = t_next_vertical
t_next_vertical += dt_dy
else:
x += x_inc
t = t_next_horizontal
t_next_horizontal += dt_dx
n -= 1
return False
[docs]def rect_contains_point(rect, point):
""" Check if rectangle contains a point. """
if (rect[0] <= point[0] and
rect[1] <= point[1] and
rect[0] + rect[2] >= point[0] and
rect[1] + rect[3] >= point[1]):
return True
return False
[docs]def rect_offset(rect, offset):
""" Offsets (grows) a rectangle in each direction. """
return (rect[0] - offset, rect[1] - offset, rect[2]+2*offset, rect[3]+2*offset)
[docs]def rect_corners(rect):
""" Returns cornerpoints of given rectangle.
>>> rect_corners((1,2,1,3))
((1, 2), (2, 2), (2, 5), (1, 5))
"""
tl = (rect[0],rect[1])
tr = (rect[0]+rect[2],rect[1])
br = (rect[0]+rect[2],rect[1]+rect[3])
bl = (rect[0],rect[1]+rect[3])
return (tl,tr,br,bl)
[docs]def rects_bound(rects):
""" Returns a rectangle that bounds all given rectangles
>>> rects_bound([(0,0,1,1), (3,3,1,1)])
(0, 0, 4, 4)
"""
def rb((ax,ay,aw,ah), (bx,by,bw,bh)):
x = min(ax, bx)
y = min(ay, by)
w = max(ax+aw, bx+bw) - x
h = max(ay+ah, by+bh) - y
return (x,y,w,h)
return reduce(rb, rects)
[docs]def rects_merge(rects):
""" Merge a list of rectangle (xywh) tuples.
Returns a list of rectangles that cover the same
surface. This is not necessarily optimal though.
>>> rects_merge([(0,0,1,1),(1,0,1,1)])
[(0, 0, 2, 1)]
"""
def stack(rects, horizontal=False):
""" Stacks rectangles that connect in either horizontal
or vertical direction.
"""
if horizontal:
rects = [(y,x,h,w) for (x,y,w,h) in rects]
rects.sort()
newrects = []
i = 0
while i < len(rects):
(x1,y1,w1,h1) = rects[i]
# Initialize new rect to this one
nr = [x1,y1,w1,h1]
# While the next rectangle connects to this one:
while (i+1 < len(rects) and
nr[0] == rects[i+1][0] and
nr[2] == rects[i+1][2] and
nr[1]+nr[3] == rects[i+1][1]):
# Increase height of the current new rect
nr[3] += rects[i+1][3]
i += 1
i += 1
newrects.append(tuple(nr))
# Flip rects back if we were stacking horizontally
if horizontal:
newrects = [(x,y,w,h) for (y,x,h,w) in newrects]
return newrects
# Stack twice, once in each direction
return stack(stack(rects),horizontal=True)
[docs]def angle_fix(theta):
""" Fixes an angle to a value between -pi and pi.
>>> angle_fix(-2*pi)
0.0
"""
return ((theta + pi) % (2*pi)) - pi
### NAVIGATION ###
[docs]def reachable(grid, (x, y), border=1):
""" Performs a 'flood fill' operation to find
reachable areas on given tile map from (x,y).
Returns as binary grid with 1 for reachable.
:param border: can be a value or a function
indicating borders of region
>>> reachable([[0,1,0],[0,1,0]], (0,0))
[[1, 0, 0], [1, 0, 0]]
"""
w,h = len(grid[0]), len(grid)
reachability = [[0 for _ in range(w)] for _ in range(h)]
edge = [(x, y)]
# If border is not a function, convert it to a simple compare
if not hasattr(border, '__call__'):
_border = border
border = lambda x: (x == _border)
while edge:
newedge = []
for (x, y) in edge:
if 0 <= x < w and 0 <= y < h and not border(grid[y][x]) and reachability[y][x] != 1:
reachability[y][x] = 1
newedge.extend(((x+1, y), (x-1, y), (x, y+1), (x, y-1)))
edge = newedge
return reachability
def grid_path_length((x,y),(gx,gy),g):
#Path list (current coords, cost, expected cost)
p = [((x,y),0,abs(gx-x)+abs(gy-y))]
#Nodes visited
h = []
#Max values of coords
m = (len(g[0]),len(g))
while (len(p) > 0):
#Sort based on best estimate of distance, with slight advantage
#to paths already explored
p.sort(key=lambda o:0.99999*o[1]+o[2])
#Best current loc
(x,y) = p[0][0]
l = []
#Expand in all 4 directions, add if:
# 1. Not of of bounds 2. No wall present 3. Not yet visited
n = x-1
if n >= 0 and g[y][n] == 0 and (n,y) not in h:
l.append(((n,y),abs(gx-n)+abs(gy-y)))
n = x+1
if n < m[0] and g[y][n] == 0 and (n,y) not in h:
l.append(((n,y),abs(gx-n)+abs(gy-y)))
n = y-1
if n >= 0 and g[n][x] == 0 and (x,n) not in h:
l.append(((x,n),abs(gx-x)+abs(gy-n)))
n = y+1
if n < m[1] and g[n][x] == 0 and (x,n) not in h:
l.append(((x,n),abs(gx-x)+abs(gy-n)))
#Add all new valid paths to path list and history
for i in l:
if i[1] == 0:
#Goal reached
return p[0][1]+1
h.append(i[0])
p.append((i[0],p[0][1]+1,i[1]))
#Remove old path
del p[0]
return None
[docs]def make_nav_mesh(walls, bounds=None, offset=7, simplify=0.001, add_points=[]):
""" Generate an almost optimal navigation mesh
between the given walls (rectangles), within
the world bounds (a big rectangle).
Mesh is a dictionary of dictionaries:
mesh[point1][point2] = distance
"""
# If bounds not given, assume outer walls are bounds.
if bounds is None:
bounds = rects_bound(walls)
# 1) Offset walls and add nodes on corners
walls = [rect_offset(w,offset) for w in walls]
nodes = set(add_points)
for w in walls:
for point in rect_corners(w):
# 2) Remove points that are inside of other walls (or outside bounds)
other_walls = filter(lambda x: x!=w,walls)
if (rect_contains_point(bounds, point) and
not any(rect_contains_point(ow, point) for ow in other_walls)):
nodes.add((int(point[0]),int(point[1])))
# 3) Connect nodes that can "see" eachother
walls = [rect_offset(w,-0.001) for w in walls]
mesh = dict((n,{}) for n in nodes)
for n1 in nodes:
for n2 in nodes:
if n1 != n2:
if not any(line_intersects_rect(n1,n2,w) for w in walls):
mesh[n1][n2] = point_dist(n1,n2)
# 4) Remove direct connections that are not much shorter than indirect ones
def astar_path_length(m, start, end):
""" Length of a path from start to end """
neighbours = lambda n: m[n].keys()
cost = lambda n1, n2: m[n1][n2]
goal = lambda n: n == end
heuristic = lambda n: point_dist(end, n)
nodes, length = astar(start, neighbours, goal, 0, cost, heuristic)
return length
connections = []
for n1 in mesh:
for n2 in mesh[n1]:
connections.append((mesh[n1][n2],(n1,n2)))
connections.sort(reverse=True) # Start with the longest connections
for length, (n1, n2) in connections:
mesh[n1].pop(n2) # Remove connection to see best path without it
alternative_dist = astar_path_length(mesh, n1,n2)
# Put the connection back if the alternative is much worse
if alternative_dist > (1+simplify) * length:
mesh[n1][n2] = length
return mesh
[docs]def find_path(start, end, mesh, grid, tilesize=16):
""" Uses astar to find a path from start to end,
using the given mesh and tile grid.
>>> grid = [[0,0,0,0,0],[0,0,0,0,0],[0,0,1,0,0],[0,0,0,0,0],[0,0,0,0,0]]
>>> mesh = make_nav_mesh([(2,2,1,1)],(0,0,4,4),1)
>>> find_path((0,0),(4,4),mesh,grid,1)
[(4, 1), (4, 4)]
"""
# If there is a straight line, just return the end point
if not line_intersects_grid(start, end, grid, tilesize):
return [end]
# Copy mesh so we can add temp nodes
mesh = copy.deepcopy(mesh)
# Add temp notes for start
mesh[start] = dict([(n, point_dist(start,n)) for n in mesh if not line_intersects_grid(start,n,grid,tilesize)])
# Add temp nodes for end:
if end not in mesh:
endconns = [(n, point_dist(end,n)) for n in mesh if not line_intersects_grid(end,n,grid,tilesize)]
for n, dst in endconns:
mesh[n][end] = dst
neighbours = lambda n: mesh[n].keys()
cost = lambda n1, n2: mesh[n1][n2]
goal = lambda n: n == end
heuristic = lambda n: ((n[0]-end[0]) ** 2 + (n[1]-end[1]) ** 2) ** 0.5
nodes, length = astar(start, neighbours, goal, 0, cost, heuristic)
return nodes
### TIMING ###
tictocs = {}
def tic(timer_id='default'):
try:
tictocs[timer_id][0] = time.clock()
except KeyError:
tictocs[timer_id] = [time.clock(),0.0]
def toc(timer_id='default'):
try:
p, a = tictocs[timer_id]
d = time.clock() - p
tictocs[timer_id][1] *= 0.9
tictocs[timer_id][1] += 0.1 * d
return d
except KeyError:
tictocs[timer_id] = [time.clock(),0.0]
return 0.0
def toc_avg(timer_id='default'):
try:
return tictocs[timer_id][1]
except KeyError:
return 0.0
if __name__ == "__main__":
import doctest
doctest.testmod()