gameshell/gamelocus.py

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))