336 lines
14 KiB
Python
336 lines
14 KiB
Python
# gamelocus.py
|
|
|
|
import math as _mt
|
|
|
|
class Locus(object):
|
|
"""Abstract base class for locus objects."""
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Base locus constructor."""
|
|
pass
|
|
|
|
def __contains__(self, item):
|
|
"""Is this point within the locus?"""
|
|
return False
|
|
|
|
def __iter__(self):
|
|
return iter(())
|
|
|
|
def __repr__(self):
|
|
return "Locus()"
|
|
|
|
class PointLocus(Locus):
|
|
"""A locus that defines a single tile."""
|
|
|
|
def __init__(self, x, y, **kwargs):
|
|
"""Take the coordinates for a point."""
|
|
self.x = x
|
|
self.y = y
|
|
|
|
def __contains__(self, item):
|
|
if not isinstance(item, tuple) and not isinstance(item, list):
|
|
raise ValueError("Item must be a tuple or a list.")
|
|
return item[0] == self.x and item[1] == self.y
|
|
|
|
def __iter__(self):
|
|
return iter(((self.x, self.y),)) # just an iterator of the one tile
|
|
|
|
def __repr__(self):
|
|
return "PointLocus({}, {})".format(self.x, self.y)
|
|
|
|
class LineLocus(Locus):
|
|
"""A locus that defines all square tiles that intersect a given line segment."""
|
|
|
|
def __init__(self, x1, y1, x2, y2, thick = False, **kwargs):
|
|
"""Take the coordinates for the two endpoints."""
|
|
self.x1 = x1
|
|
self.y1 = y1
|
|
self.x2 = x2
|
|
self.y2 = y2
|
|
self.thick = thick
|
|
|
|
def __contains__(self, item):
|
|
if not isinstance(item, tuple) and not isinstance(item, list):
|
|
raise ValueError("Item must be a tuple or a list.")
|
|
x, y = item
|
|
# Is it outside the bounding box for this line?
|
|
if x > max(self.x1, self.x2) or x < min(self.x1, self.x2) or y > max(self.y1, self.y2) or y < min(self.y1, self.y2):
|
|
return False
|
|
# Is this line straight up and down, or left and right?
|
|
elif self.x1 == self.x2 or self.y1 == self.y2:
|
|
# In this case, the line takes up the whole bounding box, so it must
|
|
# be true since it passed the first step to get here.
|
|
return True
|
|
else:
|
|
slope = (self.y2 - self.y1) / (self.x2 - self.x1)
|
|
intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope
|
|
f = lambda z: z * slope + intercept
|
|
if self.thick:
|
|
if slope > 0:
|
|
return _mt.ceil(f(x)-1) <= y and _mt.floor(f(x+1)+1) > y
|
|
else:
|
|
return _mt.floor(f(x)+1) > y and _mt.ceil(f(x+1)-1) <= y
|
|
else:
|
|
if slope > 0:
|
|
return _mt.floor(f(x)) <= y and _mt.ceil(f(x+1)) > y
|
|
else:
|
|
return _mt.ceil(f(x)) > y and _mt.floor(f(x+1)) <= y
|
|
|
|
def __iter__(self):
|
|
# avoid infinite slope case
|
|
if self.x1 == self.x2:
|
|
miny, maxy = min(self.y1, self.y2), max(self.y1, self.y2) + 1
|
|
ret = [(self.x1, y) for y in range(miny, maxy)]
|
|
return iter(ret)
|
|
# for convenience: it's easy to calculate if it's horizontal too.
|
|
elif self.y1 == self.y2:
|
|
minx, maxx = min(self.x1, self.x2), max(self.x1, self.x2) + 1
|
|
ret = [(x, self.y1) for x in range(minx, maxx)]
|
|
return iter(ret)
|
|
else:
|
|
slope = (self.y2 - self.y1) / (self.x2 - self.x1)
|
|
intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope
|
|
f = lambda x: x * slope + intercept
|
|
lx, ly = min((self.x1, self.y1), (self.x2, self.y2))
|
|
rx, ry = max((self.x1, self.y1), (self.x2, self.y2))
|
|
ret = []
|
|
# edge case 1: the first half column
|
|
if slope > 0:
|
|
maxy = _mt.ceil(f(lx+1))
|
|
if self.thick:
|
|
maxy = _mt.floor(f(lx+1)+1)
|
|
for y in range(ly, maxy):
|
|
ret.append((lx, y))
|
|
else:
|
|
maxy = _mt.floor(f(lx+1))-1
|
|
if self.thick:
|
|
maxy = _mt.ceil(f(lx+1)-2)
|
|
for y in range(ly, maxy, -1):
|
|
ret.append((lx, y))
|
|
# Usual case: the line between the end points
|
|
for x in range(lx+1, rx):
|
|
if slope > 0:
|
|
miny = _mt.floor(f(x))
|
|
maxy = _mt.ceil(f(x+1))
|
|
if self.thick:
|
|
miny = _mt.ceil(f(x)-1)
|
|
maxy = _mt.floor(f(x+1)+1)
|
|
for y in range(miny, maxy):
|
|
ret.append((x, y))
|
|
else:
|
|
miny = _mt.ceil(f(x)-1)
|
|
maxy = _mt.floor(f(x+1))-1
|
|
if self.thick:
|
|
miny = _mt.floor(f(x))
|
|
maxy = _mt.ceil(f(x+1)-2)
|
|
for y in range(miny, maxy, -1):
|
|
ret.append((x, y))
|
|
# edge case 2: the last half column
|
|
if slope > 0:
|
|
miny = _mt.floor(f(rx))
|
|
if self.thick:
|
|
miny = _mt.ceil(f(rx)-1)
|
|
for y in range(miny, ry+1):
|
|
ret.append((rx, y))
|
|
else:
|
|
miny = _mt.ceil(f(rx)-1)
|
|
if self.thick:
|
|
miny = _mt.floor(f(rx))
|
|
for y in range(miny, ry-1, -1):
|
|
ret.append((rx, y))
|
|
return iter(ret)
|
|
|
|
def __repr__(self):
|
|
return "LineLocus({}, {}, {}, {}, thick={})".format(self.x1, self.y1, self.x2, self.y2, self.thick)
|
|
|
|
class RectLocus(Locus):
|
|
"""A locus that defines the outline of a rectangle."""
|
|
|
|
def __init__(self, x1, y1, x2, y2, **kwargs):
|
|
self.lx = min(x1, x2)
|
|
self.rx = max(x1, x2)
|
|
self.ly = min(y1, y2)
|
|
self.ry = max(y1, y2)
|
|
|
|
def __contains__(self, item):
|
|
if not isinstance(item, tuple) and not isinstance(item, list):
|
|
raise ValueError("Item must be a tuple or a list.")
|
|
x, y = item
|
|
if x < self.lx or x > self.rx or y < self.ly or y > self.ry:
|
|
return False
|
|
elif x == self.lx or x == self.rx or y == self.ry or y == self.ly:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def __iter__(self):
|
|
ret = [(x, self.ly) for x in range(self.lx, self.rx)]
|
|
ret.extend([(self.rx, y) for y in range(self.ly, self.ry)])
|
|
ret.extend([(x, self.ry) for x in range(self.rx, self.lx, -1)])
|
|
ret.extend([(self.lx, y) for y in range(self.ry, self.ly, -1)])
|
|
return iter(ret)
|
|
|
|
def __repr__(self):
|
|
return "RectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry)
|
|
|
|
class FilledRectLocus(Locus):
|
|
"""A locus that defines all points within a rectangle."""
|
|
|
|
def __init__(self, x1, y1, x2, y2, **kwargs):
|
|
self.lx = min(x1, x2)
|
|
self.rx = max(x1, x2)
|
|
self.ly = min(y1, y2)
|
|
self.ry = max(y1, y2)
|
|
|
|
def __contains__(self, item):
|
|
if not isinstance(item, tuple) and not isinstance(item, list):
|
|
raise ValueError("Item must be a tuple or a list.")
|
|
x, y = item
|
|
return x >= self.lx and x <= self.rx and y >= self.ly and y <= self.ry
|
|
|
|
def __iter__(self):
|
|
return iter([(x, y) for y in range(self.ly, self.ry+1) for x in range(self.lx, self.rx+1)])
|
|
|
|
def __repr__(self):
|
|
return "FilledRectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry)
|
|
|
|
class CircleLocus(Locus):
|
|
"""A locus that defines the outline of a circle."""
|
|
|
|
def __init__(self, x, y, radius, **kwargs):
|
|
self.x = x + 0.5
|
|
self.y = y + 0.5
|
|
self.r = radius
|
|
|
|
def __contains__(self, item):
|
|
if not isinstance(item, tuple) and not isinstance(item, list):
|
|
raise ValueError("Item must be a tuple or a list.")
|
|
x, y = item
|
|
if self.x == x + 0.5 and (y == _mt.floor(self.y + self.r) or y == _mt.floor(self.y - self.r)):
|
|
return True
|
|
elif self.y == y + 0.5 and (x == _mt.floor(self.x + self.r) or x == _mt.floor(self.x - self.r)):
|
|
return True
|
|
else:
|
|
# Edge case: very small circle
|
|
if self.r <= _mt.sqrt(2) * 0.5:
|
|
# In this case, the circle is small enough that the previous
|
|
# checks would have cought every possible tile other than the center.
|
|
return x == _mt.floor(self.x) and y == _mt.floor(self.y)
|
|
if y + 0.5 > self.y:
|
|
if x + 0.5 > self.x:
|
|
return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) > self.r
|
|
else:
|
|
return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) > self.r
|
|
else:
|
|
if x + 0.5 > self.x:
|
|
return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) > self.r
|
|
else:
|
|
return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y - self.y)**2) > self.r
|
|
|
|
def __iter__(self):
|
|
# Edge case: very small circle
|
|
if self.r <= _mt.sqrt(2) * 0.5:
|
|
if self.r > 0.5:
|
|
ret = ((_mt.floor(self.x), _mt.floor(self.y)),
|
|
(_mt.floor(self.x) + 1, _mt.floor(self.y)),
|
|
(_mt.floor(self.x), _mt.floor(self.y) + 1),
|
|
(_mt.floor(self.x) - 1, _mt.floor(self.y)),
|
|
(_mt.floor(self.x), _mt.floor(self.y) - 1))
|
|
return iter(ret)
|
|
else:
|
|
return iter(((_mt.floor(self.x), _mt.floor(self.y)),))
|
|
else:
|
|
f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y
|
|
# The topmost square: the 'keystone' if you will.
|
|
ret = [(_mt.floor(self.x), _mt.floor(self.y + self.r))]
|
|
# All squares between the keystone and the rightmost column.
|
|
for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)):
|
|
ly = _mt.floor(f(x))
|
|
ry = _mt.floor(f(x+1)) - 1
|
|
for y in range(ly, ry, -1):
|
|
ret.append((x, y))
|
|
# The last column, minus the bottom square
|
|
x = _mt.floor(self.x + self.r)
|
|
for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1):
|
|
ret.append((x, y))
|
|
# Finish the circle by copying the coordinates we already have.
|
|
ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret]
|
|
return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret])
|
|
|
|
def __repr__(self):
|
|
return "CircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r)
|
|
|
|
class FilledCircleLocus(Locus):
|
|
"""A locus that defines all points within a circle."""
|
|
|
|
def __init__(self, x, y, radius, **kwargs):
|
|
self.x = x + 0.5
|
|
self.y = y + 0.5
|
|
self.r = radius
|
|
|
|
def __contains__(self, item):
|
|
if not isinstance(item, tuple) and not isinstance(item, list):
|
|
raise ValueError("Item must be a tuple or a list.")
|
|
x, y = item
|
|
if self.x == x + 0.5 and (y <= _mt.floor(self.y + self.r) and y >= _mt.floor(self.y - self.r)):
|
|
return True
|
|
elif self.y == y + 0.5 and (x <= _mt.floor(self.x + self.r) and x >= _mt.floor(self.x - self.r)):
|
|
return True
|
|
else:
|
|
# Edge case: very small circle
|
|
if self.r <= _mt.sqrt(2) * 0.5:
|
|
# In this case, the circle is small enough that the previous
|
|
# checks would have cought every possible tile other than the center.
|
|
return x == _mt.floor(self.x) and y == _mt.floor(self.y)
|
|
if y + 0.5 > self.y:
|
|
if x + 0.5 > self.x:
|
|
return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r
|
|
else:
|
|
return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r
|
|
else:
|
|
if x + 0.5 > self.x:
|
|
return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r
|
|
else:
|
|
return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r
|
|
|
|
def __iter__(self):
|
|
# Edge case: very small circle
|
|
if self.r <= _mt.sqrt(2) * 0.5:
|
|
if self.r > 0.5:
|
|
ret = ((_mt.floor(self.x), _mt.floor(self.y)),
|
|
(_mt.floor(self.x) + 1, _mt.floor(self.y)),
|
|
(_mt.floor(self.x), _mt.floor(self.y) + 1),
|
|
(_mt.floor(self.x) - 1, _mt.floor(self.y)),
|
|
(_mt.floor(self.x), _mt.floor(self.y) - 1))
|
|
return iter(ret)
|
|
else:
|
|
return iter(((_mt.floor(self.x), _mt.floor(self.y)),))
|
|
else:
|
|
f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y
|
|
# The topmost square: the 'keystone' if you will.
|
|
ret = [(_mt.floor(self.x), y) for y in range(_mt.floor(self.y + self.r), _mt.floor(self.y), -1)]
|
|
# All squares between the keystone and the rightmost column.
|
|
ry = _mt.floor(self.y)
|
|
for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)):
|
|
ly = _mt.floor(f(x))
|
|
for y in range(ly, ry, -1):
|
|
ret.append((x, y))
|
|
# The last column, minus the bottom square
|
|
x = _mt.floor(self.x + self.r)
|
|
for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1):
|
|
ret.append((x, y))
|
|
# Finish the circle by copying the coordinates we already have.
|
|
ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret]
|
|
return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret] + [(_mt.floor(self.x), _mt.floor(self.y))])
|
|
|
|
def __repr__(self):
|
|
return "FilledCircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r)
|
|
|
|
class SetLocus(set, Locus):
|
|
"""A locus that defines a set of given arbitrary points."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(SetLocus, self).__init__(*args, **kwargs)
|
|
|
|
def __repr__(self):
|
|
return "SetLocus({})".format(tuple(self))
|