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