Ejemplo n.º 1
0
def test_sprite_sides_set(sprite_class, params: SpriteParams,
                          results: SideSetterResults):
    sprite = sprite_class(width=params.width, height=params.height)

    sprite.left = params.position.x
    expected = results.left
    assert sprite.left == expected.left
    assert sprite.right == expected.right
    assert sprite.top == expected.top
    assert sprite.bottom == expected.bottom

    sprite.position = Vector(0, 0)
    sprite.right = params.position.x
    expected = results.right
    assert sprite.left == expected.left
    assert sprite.right == expected.right
    assert sprite.top == expected.top
    assert sprite.bottom == expected.bottom

    sprite.position = Vector(0, 0)
    sprite.top = params.position.y
    expected = results.top
    assert sprite.left == expected.left
    assert sprite.right == expected.right
    assert sprite.top == expected.top
    assert sprite.bottom == expected.bottom

    sprite.position = Vector(0, 0)
    sprite.bottom = params.position.y
    expected = results.bottom
    assert sprite.left == expected.left
    assert sprite.right == expected.right
    assert sprite.top == expected.top
    assert sprite.bottom == expected.bottom
Ejemplo n.º 2
0
def test_truncate_equivalent_to_scale(x: Vector, max_length: float):
    """Vector.scale_to and truncate are equivalent when max_length <= x.length"""
    assume(max_length <= x.length)
    note(f"x.length = {x.length}")
    if max_length > 0:
        note(f"x.length = {x.length / max_length} * max_length")

    scale: Union[Vector, Type[Exception]]
    truncate: Union[Vector, Type[Exception]]

    try:
        truncate = x.truncate(max_length)
    except Exception as e:
        truncate = type(e)

    try:
        scale = x.scale_to(max_length)
    except Exception as e:
        event(f"Exception {type(e).__name__} thrown")
        scale = type(e)

    if isinstance(scale, Vector) and x.length == max_length:
        # Permit some edge-case where truncation and scaling aren't equivalent
        assert isinstance(truncate, Vector)
        assert scale.isclose(truncate, abs_tol=0, rel_tol=1e-12)

    else:
        assert scale == truncate
Ejemplo n.º 3
0
 def center(self) -> Vector:
     """
     Get the midpoint vector
     """
     if self.side in (TOP, BOTTOM):
         return Vector(self.parent.center.x, float(self))
     else:
         return Vector(float(self), self.parent.center.y)
Ejemplo n.º 4
0
def test_trig_invariance(angle: float, n: int):
    """Test that cos(θ), sin(θ) ≃ cos(θ + n*360°), sin(θ + n*360°)"""
    r_cos, r_sin = Vector._trig(angle)
    n_cos, n_sin = Vector._trig(angle + 360 * n)

    note(f"δcos: {r_cos - n_cos}")
    assert isclose(r_cos, n_cos, rel_to=[n / 1e9])
    note(f"δsin: {r_sin - n_sin}")
    assert isclose(r_sin, n_sin, rel_to=[n / 1e9])
Ejemplo n.º 5
0
def test_class_attrs():
    class TestSprite(BaseSprite):
        position = Vector(4, 2)

    sprite = TestSprite()
    assert sprite.position == Vector(4, 2)

    sprite = TestSprite(position=(2, 4))
    assert sprite.position == Vector(2, 4)
Ejemplo n.º 6
0
    def bottom_right(self, vector: Vector):
        """
        The coordinates of the bottom right corner of the object.

        Can be set to a :class:`ppb_vector.Vector`.
        """
        vector = Vector(vector)
        x = vector.x - (self.width / 2)
        y = vector.y + (self.height / 2)
        self.position = Vector(x, y)
Ejemplo n.º 7
0
def test_rectangle_shape_mixin_center():
    class TestSprite(RectangleShapeMixin, BaseSprite):
        pass

    test_sprite = TestSprite()

    assert test_sprite.center == test_sprite.position

    test_sprite.center = Vector(100, 100)

    assert test_sprite.center == test_sprite.position
    assert test_sprite.center == Vector(100, 100)
Ejemplo n.º 8
0
def test_dot_rotational_invariance(x: Vector, y: Vector, angle: float):
    """Test that rotating vectors doesn't change their dot product."""
    t = x.angle(y)
    cos_t, _ = Vector._trig(t)
    note(f"θ: {t}")
    note(f"cos θ: {cos_t}")

    # Exclude near-orthogonal test inputs
    assume(abs(cos_t) > 1e-6)
    assert isclose(x * y,
                   x.rotate(angle) * y.rotate(angle),
                   rel_to=(x, y),
                   rel_exp=2)
Ejemplo n.º 9
0
 def mouse_motion(self, event, scene):
     screen_position = Vector(*event.pos)
     camera = scene.main_camera
     scene_position = camera.translate_to_frame(screen_position)
     delta = Vector(*event.rel) * (1 / camera.pixel_ratio)
     buttons = {
         self.button_map[btn + 1]
         for btn, value in enumerate(event.buttons) if value
     }
     return events.MouseMotion(position=scene_position,
                               screen_position=screen_position,
                               delta=delta,
                               buttons=buttons)
Ejemplo n.º 10
0
def test_reflect_angle(initial: Vector, normal: Vector):
    """Test angle-related properties of Vector.reflect:

    * initial.reflect(normal) * normal == - initial * normal
    * normal.angle(initial) == 180 - normal.angle(reflected)
    """
    # Exclude initial vectors that are very small or very close to the surface.
    assume(not angle_isclose(initial.angle(normal) % 180, 90, epsilon=10))
    assume(initial.length > 1e-10)

    reflected = initial.reflect(normal)
    assert isclose((initial * normal), -(reflected * normal))
    assert angle_isclose(normal.angle(initial), 180 - normal.angle(reflected))
Ejemplo n.º 11
0
def test_rotation_stability(angle, loops):
    """Rotating loops times by angle is equivalent to rotating by loops*angle."""
    initial = Vector(1, 0)

    fellswoop = initial.rotate(angle * loops)
    note(f"One Fell Swoop: {fellswoop}")

    stepwise = initial
    for _ in range(loops):
        stepwise = stepwise.rotate(angle)
    note(f"Step-wise: {stepwise}")

    assert fellswoop.isclose(stepwise, rel_tol=1e-8)
    assert math.isclose(fellswoop.length, initial.length, rel_tol=1e-15)
Ejemplo n.º 12
0
    def __init__(self, **kwargs):
        super().__init__()

        self.position = Vector(self.position)

        # Initialize things
        for k, v in kwargs.items():
            # Abbreviations
            if k == 'pos':
                k = 'position'
            # Castings
            if k == 'position':
                v = Vector(v)
            setattr(self, k, v)
Ejemplo n.º 13
0
class RotatableMixin:
    """
    A rotation mixin. Can be included with sprites.

    .. warning:: rotation does not affect underlying shape (the corners are still in the same place), it only rotates
       the sprites image and provides a facing.
    """
    rotation = 0
    # This is necessary to make facing do the thing while also being adjustable.
    #: The baseline vector, representing the "front" of the sprite
    basis = Vector(0, -1)
    # Considered making basis private, the only reason to do so is to
    # discourage people from relying on it as data.

    @property
    def facing(self):
        """
        The direction the "front" is facing.

        Can be set to an arbitrary facing by providing a facing vector.
        """
        return Vector(*self.basis).rotate(self.rotation).normalize()

    @facing.setter
    def facing(self, value):
        self.rotation = self.basis.angle(value)

    def rotate(self, degrees):
        """
        Rotate the sprite by a given angle (in degrees).
        """
        self.rotation = (self.rotation + degrees) % 360
Ejemplo n.º 14
0
    def facing(self):
        """
        The direction the "front" is facing.

        Can be set to an arbitrary facing by providing a facing vector.
        """
        return Vector(*self.basis).rotate(self.rotation).normalize()
Ejemplo n.º 15
0
def test_dot_from_angle(x: Vector, y: Vector):
    """Test x · y == |x| · |y| · cos(θ)"""
    t = x.angle(y)
    cos_t, _ = Vector._trig(t)

    # Dismiss near-othogonal test inputs
    assume(abs(cos_t) > 1e-6)

    min_len, max_len = sorted((x.length, y.length))
    geometric = min_len * (max_len * cos_t)

    note(f"θ: {t}")
    note(f"cos θ: {cos_t}")
    note(f"algebraic: {x * y}")
    note(f"geometric: {geometric}")
    assert isclose(x * y, geometric, rel_to=(x, y), rel_exp=2)
Ejemplo n.º 16
0
    def bottom_left(self) -> Vector:
        """
        The coordinates of the bottom left corner of the object.

        Can be set to a :class:`ppb_vector.Vector`.
        """
        return Vector(self.left, self.bottom)
Ejemplo n.º 17
0
    def top_right(self) -> Vector:
        """
        The coordinates of the top right corner of the object.

        Can be set to a :class:`ppb_vector.Vector`.
        """
        return Vector(self.right, self.top)
Ejemplo n.º 18
0
class BaseSprite(EventMixin):
    """
    The base Sprite class. All sprites should inherit from this (directly or
    indirectly).

    The things that define a BaseSprite:

    * The __event__ protocol (see ppb.eventlib.EventMixin)
    * A position vector
    * A layer

    BaseSprite provides an __init__ method that sets attributes based on kwargs
    to make rapid prototyping easier.
    """
    #: (:py:class:`ppb.Vector`): Location of the sprite
    position: Vector = Vector(0, 0)
    #: The layer a sprite exists on.
    layer: int = 0

    def __init__(self, **kwargs):
        super().__init__()

        self.position = Vector(self.position)

        # Initialize things
        for k, v in kwargs.items():
            # Abbreviations
            if k == 'pos':
                k = 'position'
            # Castings
            if k == 'position':
                v = Vector(v)
            setattr(self, k, v)
Ejemplo n.º 19
0
    def __init__(self, **props):
        """
        :class:`BaseSprite` does not accept any positional arguments, and uses
        keyword arguments to set arbitrary state to the :class:`BaseSprite`
        instance. This allows rapid prototyping.

        Example: ::

           sprite = BaseSprite(speed=6)
           print(sprite.speed)

        This sample will print the numeral 6.

        You may add any arbitrary data values in this fashion. Alternatively,
        it is considered best practice to subclass :class:`BaseSprite` and set
        the default values of any required attributes as class attributes.

        Example: ::

           class Rocket(ppb.sprites.BaseSprite):
              velocity = Vector(0, 1)

              def on_update(self, update_event, signal):
                  self.position += self.velocity * update_event.time_delta
        """

        super().__init__(**props)

        # Type coercion
        self.position = Vector(self.position)
Ejemplo n.º 20
0
    def top_middle(self) -> Vector:
        """
        The coordinates of the midpoint of the top of the object.

        Can be set to a :class:`ppb_vector.Vector`.
        """
        return Vector(self.position.x, self.top)
Ejemplo n.º 21
0
    def right_middle(self) -> Vector:
        """
        The coordinates of the midpoint of the right side of the object.

        Can be set to a :class:`ppb_vector.Vector`.
        """
        return Vector(self.right, self.position.y)
Ejemplo n.º 22
0
 def translate_to_frame(self, point: Vector) -> Vector:
     """
     Converts a vector from pixel-based window to in-game coordinate space
     """
     # 1. Scale from pixels to game unites
     scaled = point / self.pixel_ratio
     # 2. Reposition relative to frame edges
     return Vector(self.frame_left + scaled.x, self.frame_top - scaled.y)
Ejemplo n.º 23
0
 def translate_to_viewport(self, point: Vector) -> Vector:
     """
     Converts a vector from in-game to pixel-based window coordinate space
     """
     # 1. Reposition based on frame edges
     # 2. Scale from game units to pixels
     return Vector(point.x - self.frame_left,
                   self.frame_top - point.y) * self.pixel_ratio
Ejemplo n.º 24
0
def test_sides_bottom_right_set(x, y, vector_type):
    sprite = Sprite()
    sprite.bottom.right = vector_type((x, y))
    bottom_right = sprite.bottom.right
    right_bottom = sprite.right.bottom
    assert bottom_right == right_bottom
    assert bottom_right == Vector(x, y)
    assert sprite.position == bottom_right + Vector(-0.5, 0.5)

    # duplicating to prove top.left and left.top are the same.
    sprite = Sprite()
    sprite.right.bottom = vector_type((x, y))
    bottom_right = sprite.bottom.right
    right_bottom = sprite.right.bottom
    assert right_bottom == bottom_right
    assert right_bottom == Vector(x, y)
    assert sprite.position == right_bottom + Vector(-0.5, 0.5)
Ejemplo n.º 25
0
def test_sides_bottom_right_plus_equals(x, y, vector_type):
    sprite = Sprite()
    sprite.bottom.right += vector_type((x, y))
    bottom_right = sprite.bottom.right
    right_bottom = sprite.right.bottom
    assert bottom_right == right_bottom
    assert bottom_right == Vector(x + 0.5, y - 0.5)
    assert sprite.position == bottom_right + Vector(-0.5, 0.5)

    # duplicating to prove bottom.left and left.bottom are the same.
    sprite = Sprite()
    sprite.bottom.left += vector_type((x, y))
    bottom_right = sprite.bottom.right
    right_bottom = sprite.right.bottom
    assert right_bottom == bottom_right
    assert right_bottom == Vector(x + 0.5, y - 0.5)
    assert sprite.position == right_bottom + Vector(-0.5, 0.5)
Ejemplo n.º 26
0
def test_rotatable_subclass():
    class TestRotatableMixin(RotatableMixin):
        _rotation = 180
        basis = Vector(0, 1)

    rotatable = TestRotatableMixin()
    assert rotatable.rotation == 180
    assert rotatable.facing == Vector(0, -1)
Ejemplo n.º 27
0
def test_sides_top_left_set(x, y, vector_type):
    sprite = Sprite()
    sprite.top.left = vector_type((x, y))
    top_left = sprite.top.left
    left_top = sprite.left.top
    assert top_left == left_top
    assert top_left == Vector(x, y)
    assert sprite.position == top_left + Vector(0.5, -0.5)

    # duplicating to prove top.left and left.top are the same.
    sprite = Sprite()
    sprite.left.top = vector_type((x, y))
    top_left = sprite.top.left
    left_top = sprite.left.top
    assert left_top == top_left
    assert left_top == Vector(x, y)
    assert sprite.position == left_top + Vector(0.5, -0.5)
Ejemplo n.º 28
0
 def mouse_motion(self, event, scene):
     motion = event.motion
     screen_position = Vector(motion.x, motion.y)
     camera = scene.main_camera
     scene_position = camera.translate_point_to_game_space(screen_position)
     delta = Vector(motion.xrel, motion.yrel) * (1 / camera.pixel_ratio)
     buttons = {
         value
         for btn, value in self.button_mask_map.items()
         if motion.state & btn
     }
     return events.MouseMotion(
         position=scene_position,
         delta=delta,
         buttons=buttons,
         # timestamp=motion.timestamp
     )
Ejemplo n.º 29
0
def test_sides_top_right_plus_equals(x, y, vector_type):
    sprite = Sprite()
    sprite.top.right += vector_type((x, y))
    top_right = sprite.top.right
    right_top = sprite.right.top
    assert top_right == right_top
    assert top_right == Vector(x + 0.5, y + 0.5)
    assert sprite.position == top_right + Vector(-0.5, -0.5)

    # duplicating to prove top.left and left.top are the same.
    sprite = Sprite()
    sprite.top.left += vector_type((x, y))
    top_right = sprite.top.right
    right_top = sprite.right.top
    assert right_top == top_right
    assert right_top == Vector(x + 0.5, y + 0.5)
    assert sprite.position == right_top + Vector(-0.5, -0.5)
Ejemplo n.º 30
0
    def _mk_update_vector_center(self, value):
        """
        Calculate the update vector, based on the given side.

        That is, handles the calculation for forms like sprite.right = number
        """
        value = Vector(value)
        # Pretty similar to ._mk_update_vector_side()
        self_dimension, self_offset = self._lookup_side(self.side)

        attr_dimension = 'y' if self_dimension == 'x' else 'x'

        fields = {
            self_dimension: value[self_dimension] - self_offset,
            attr_dimension: value[attr_dimension]
        }

        return Vector(fields)