from __future__ import division from math import atan2, pi import time class Positionable(object): """ Parent class for any object with a position. """ def __init__(self, x=None, y=None): self.x = x self.y = y def __str__(self): return '<%s at (%s, %s)>' % (self.__class__.__name__, self.x, self.y) def set_position(self, x, y): self.x = x self.y = y def get_position(self): return self.x, self.y @property def xy(self): """ Shortcut getter for (x, y) position. """ return self.x, self.y def distance_to(self, positionable): """ Calculate the Pythagorian distance from this positionable to another. """ x, y = positionable.get_position() return ((x - self.x) ** 2 + (y - self.y) ** 2) ** .5 class MovingPositionable(Positionable): """ Parent class for positionable objects that need movement calculations. For these calculations, the previous position is also saved. """ def __init__(self, x=None, y=None): super(MovingPositionable, self).__init__(x, y) self.prev = Positionable(x, y) def set_position(self, x, y): """ Set a new position and save the current position as the precious position. If no previous position has been set, set is to the new position so that the movement is zero. """ if self.x is None or self.y is None: self.prev.set_position(x, y) else: self.prev.set_position(self.x, self.y) Positionable.set_position(self, x, y) def get_previous_position(self): return self.prev def rotation_around(self, center): """ Calculate rotation of this positionable relative to a center positionable. """ cx, cy = center.get_position() px, py = self.prev.get_position() prev_angle = atan2(px - cx, py - cy) current_angle = atan2(self.x - cx, self.y - cy) rotation = current_angle - prev_angle if rotation >= pi: return 2 * pi - rotation if rotation <= -pi: return -2 * pi - rotation return rotation def translation(self): """ Calculate the movement relative to the last position as a vector positionable. """ px, py = self.prev.get_position() return Positionable(self.x - px, self.y - py) def movement_distance(self): return self.distance_to(self.prev) class AcceleratedPositionable(MovingPositionable): """ Parent class for positionable objects that need acceleration calculations. For these calculations, timestamps are saved for both the current and previous position. """ def __init__(self, x=None, y=None): super(AcceleratedPositionable, self).__init__(x, y) self.prev_timestamp = self.current_timestamp = None def set_position(self, x, y): MovingPositionable.set_position(self, x, y) if self.current_timestamp is None: self.prev_timestamp = self.current_timestamp = time.time() return self.prev_timestamp = self.current_timestamp self.current_timestamp = time.time() def movement_time(self): return self.timestamp - self.prev_timestamp def acceleration(self): """ Calculate the acceleration in pixels/second. """ return self.movement_distance() / self.movement_time() class Surface(Positionable): """ Interface class for surfaces with a position. Defines a function 'contains', which calculates whether a position is located in the surface. """ def contains(self, point): raise NotImplemented class RectangularSurface(Surface): """ Rectangle, represented by a left-top position (x, y) and a size (width, height). """ def __init__(self, x, y, width, height): super(RectangularSurface, self).__init__(x, y) self.set_size(width, height) def __str__(self): return '<%s at (%s, %s) size=(%s, %s)>' \ % (self.__class__.__name__, self.x, self.y, self.width, self.height) def set_size(self, width, height): self.width = width self.height = height def get_size(self): return self.width, self.height def contains(self, point): x, y = point.get_position() return self.x <= x <= self.x + self.width \ and self.y <= y <= self.y + self.height class CircularSurface(Surface): """ Circle, represented by a center position (x, y) and a radius. """ def __init__(self, x, y, radius): super(CircularSurface, self).__init__(x, y) self.set_radius(radius) def __str__(self): return '<%s at (%s, %s) size=(%s, %s)>' \ % (self.__class__.__name__, self.x, self.y, self.width, self.height) def set_radius(self, radius): self.radius = radius def get_radius(self): return self.radius def contains(self, point): return self.distance_to(point) <= self.radius