diff --git a/docs/reference/sprites.rst b/docs/reference/sprites.rst index 59d06355..4e9f84e5 100644 --- a/docs/reference/sprites.rst +++ b/docs/reference/sprites.rst @@ -4,10 +4,55 @@ All About Sprites ================= -.. autoclass:: ppb.BaseSprite +.. automodule:: ppb.sprites + +------------------ +Default Sprite +------------------ + +This is the class you should instantiate or subclass for your games unless +you are changing the defaults. + +.. autoclass:: ppb.Sprite :members: :inherited-members: +Note that ``ppb.BaseSprite`` is deprecated in favor of ppb.Sprite. Scheduled +for removal in ppb v0.8.0. + +------------------- +Feature Mixins +------------------- + +These mixins are the various features already available in Sprite. Here for +complete documentation. + +.. autoclass:: ppb.sprites.RenderableMixin + :members: + +.. autoclass:: ppb.sprites.RotatableMixin + :members: + +.. autoclass:: ppb.sprites.SquareShapeMixin + :members: + +------------------- +Base Classes +------------------- + +The base class of Sprite, use this if you need to change the low level +expectations. + +.. autoclass:: ppb.sprites.BaseSprite + :members: + :inherited-members: + + +------------------- +Internals +------------------- + +These classes are internals for various APIs included with mixins. .. autoclass:: ppb.sprites.Side :members: diff --git a/ppb/__init__.py b/ppb/__init__.py index f2e2dc1a..d4502578 100644 --- a/ppb/__init__.py +++ b/ppb/__init__.py @@ -1,22 +1,41 @@ import logging +import warnings from typing import Callable from ppb import events from ppb_vector import Vector from ppb.engine import GameEngine from ppb.scenes import BaseScene -from ppb.sprites import BaseSprite +from ppb.sprites import Sprite from ppb.systems import Image from ppb.systems import Sound __all__ = ( # Shortcuts - 'Vector', 'BaseScene', 'BaseSprite', 'Image', 'Sound', 'events', + 'Vector', 'BaseScene', 'BaseSprite', 'Image', 'Sprite', 'Sound', + 'events', # Local stuff 'run', 'make_engine', ) +class BaseSprite(Sprite): + """ + A stub that raises a deprecation warning when a user uses + ``ppb.BaseSprite.`` + """ + __warning = """Using ppb.BaseSprite is deprecated. + + You probably want ppb.Sprite. If you're wanting to use BaseSprite and + mixins to change what features your sprites have, import + ppb.sprites.BaseSprite. + """ + + def __init__(self, **kwargs): + warnings.warn(self.__warning, DeprecationWarning) + super().__init__(**kwargs) + + def _make_kwargs(setup, title, engine_opts): kwargs = { "resolution": (800, 600), diff --git a/ppb/sprites.py b/ppb/sprites.py index fe6ff222..745cf673 100644 --- a/ppb/sprites.py +++ b/ppb/sprites.py @@ -1,3 +1,12 @@ +""" +Sprites are game objects. + +In ppb all sprites are built from composition via mixins or subclassing via +traditional Python inheritance. Sprite is provided as a default expectation +used in ppb. + +If you intend to build your own set of expectation, see BaseSprite. +""" from inspect import getfile from pathlib import Path from typing import Union @@ -9,6 +18,13 @@ from ppb.eventlib import EventMixin from ppb.utils import FauxFloat +__all__ = ( + "BaseSprite", + "Sprite", + "RotatableMixin", + "SquareShapeMixin", + "RenderableMixin", +) TOP = "top" BOTTOM = "bottom" @@ -19,6 +35,114 @@ side_attribute_error_message = error_message.format +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) + + +class RenderableMixin: + """ + A class implementing the API expected by ppb.systems.renderer.Renderer. + + You should include RenderableMixin before BaseSprite in your parent + class definitions. + """ + #: (:py:class:`ppb.Image`): The image asset + image = None # TODO: Type hint appropriately + size = 1 + + def __image__(self): + """ + Returns the sprite's image attribute if provided, or sets a default + one. + """ + if self.image is None: + klass = type(self) + prefix = Path(klass.__module__.replace('.', '/')) + try: + klassfile = getfile(klass) + except TypeError: + prefix = Path('.') + else: + if Path(klassfile).name != '__init__.py': + prefix = prefix.parent + if prefix == Path('.'): + self.image = ppb.Image(f"{klass.__name__.lower()}.png") + else: + self.image = ppb.Image(f"{prefix!s}/{klass.__name__.lower()}.png") + return self.image + + +class RotatableMixin: + """ + A simple rotation mixin. Can be included with sprites. + """ + _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 + """ + return Vector(*self.basis).rotate(self.rotation).normalize() + + @facing.setter + def facing(self, value): + self.rotation = self.basis.angle(value) + + @property + def rotation(self): + """ + The amount the sprite is rotated, in degrees + """ + return self._rotation + + @rotation.setter + def rotation(self, value): + self._rotation = value % 360 + + def rotate(self, degrees): + """ + Rotate the sprite by a given angle (in degrees). + """ + self.rotation += degrees + + class Side(FauxFloat): """ Acts like a float, but also has a variety of accessors. @@ -30,7 +154,7 @@ class Side(FauxFloat): BOTTOM: ('y', -1) } - def __init__(self, parent: 'BaseSprite', side: str): + def __init__(self, parent: 'SquareShapeMixin', side: str): self.side = side self.parent = parent @@ -165,74 +289,20 @@ def _attribute_gate(self, attribute, bad_sides): raise AttributeError(message) -class Rotatable: - """ - A simple rotation mixin. Can be included with sprites. +class SquareShapeMixin: """ - _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 - """ - return Vector(*self.basis).rotate(self.rotation).normalize() - - @facing.setter - def facing(self, value): - self.rotation = self.basis.angle(value) - - @property - def rotation(self): - """ - The amount the sprite is rotated, in degrees - """ - return self._rotation - - @rotation.setter - def rotation(self, value): - self._rotation = value % 360 - - def rotate(self, degrees): - """ - Rotate the sprite by a given angle (in degrees). - """ - self.rotation += degrees - + A mixin that applies square shapes to sprites. -class BaseSprite(EventMixin, Rotatable): + You should include SquareShapeMixin before ppb.sprites.BaseSprite in + your parent classes. """ - The base Sprite class. All sprites should inherit from this (directly or - indirectly). - """ - #: (:py:class:`ppb.Image`): The image asset - image = None - #: (:py:class:`ppb.Vector`): Location of the sprite - position: Vector = Vector(0, 0) #: The width/height of the sprite (sprites are square) size: Union[int, float] = 1 - #: The layer a sprite exists on. - layer: int = 0 + #: Just here for typing and linting purposes. Your sprite should already have a position. + position: ppb_vector.Vector 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) + super().__init__(**kwargs) # Trigger some calculations self.size = self.size @@ -296,19 +366,17 @@ def bottom(self, value): def _offset_value(self): return self.size / 2 - def __image__(self): - if self.image is None: - klass = type(self) - prefix = Path(klass.__module__.replace('.', '/')) - try: - klassfile = getfile(klass) - except TypeError: - prefix = Path('.') - else: - if Path(klassfile).name != '__init__.py': - prefix = prefix.parent - if prefix == Path('.'): - self.image = ppb.Image(f"{klass.__name__.lower()}.png") - else: - self.image = ppb.Image(f"{prefix!s}/{klass.__name__.lower()}.png") - return self.image + +class Sprite(SquareShapeMixin, RenderableMixin, RotatableMixin, BaseSprite): + """ + The default Sprite class. + + Sprite includes: + + * BaseSprite + * SquareShapeMixin + * RenderableMixin + * RotatableMixin + + New in 0.7.0: Use this in place of BaseSprite in your games. + """ diff --git a/ppb/systems/renderer.py b/ppb/systems/renderer.py index 7ed2e071..b1dc9951 100644 --- a/ppb/systems/renderer.py +++ b/ppb/systems/renderer.py @@ -15,6 +15,8 @@ DEFAULT_RESOLUTION = 800, 600 +# TODO: Move Image out of the renderer so sprites can type hint +# appropriately. class Image(assets.Asset): def background_parse(self, data): return pygame.image.load(io.BytesIO(data), self.name).convert_alpha() diff --git a/tests/test_camera.py b/tests/test_camera.py index 75afcb8f..637de756 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -1,6 +1,6 @@ from math import isclose -from ppb import BaseSprite +from ppb import Sprite from ppb import Vector from ppb.camera import Camera @@ -69,7 +69,7 @@ def test_sprite_in_viewport(): # 80 is the legacy value. cam = Camera(viewport=(0, 0, 800, 600), pixel_ratio=80) - class Thing(BaseSprite): + class Thing(Sprite): def __init__(self, position=Vector(2, 2)): super().__init__() self.size = 2 diff --git a/tests/test_sprites.py b/tests/test_sprites.py index efdd9b29..ec11fcae 100644 --- a/tests/test_sprites.py +++ b/tests/test_sprites.py @@ -1,271 +1,15 @@ -from unittest import TestCase +from math import isclose from unittest.mock import patch +import warnings -from ppb import BaseSprite, Vector -from ppb.sprites import Rotatable +from hypothesis import given +from hypothesis.strategies import floats +from hypothesis.strategies import integers +import pytest - -class TestBaseSprite(TestCase): - - def setUp(self): - self.sprite = BaseSprite() - self.wide_sprite = BaseSprite(size=2, pos=(2, 2)) - - def test_pos(self): - self.assertEqual(self.sprite.position, Vector(0, 0)) - self.assertEqual(self.wide_sprite.position, Vector(2, 2)) - - def test_center(self): - self.assertEqual(self.sprite.center, self.sprite.position) - - self.sprite.center = 1, 1 - self.assertEqual(self.sprite.position.x, 1) - self.assertEqual(self.sprite.position.y, 1) - self.assertEqual(self.sprite.center, self.sprite.position) - - self.sprite.center = Vector(2, 2) - self.assertEqual(self.sprite.position.x, 2) - self.assertEqual(self.sprite.position.y, 2) - - self.sprite.center += -1, -1 - self.assertEqual(self.sprite.position.x, 1) - self.assertEqual(self.sprite.position.y, 1) - - self.sprite.center += 0, 1 - self.assertEqual(self.sprite.position.x, 1) - self.assertEqual(self.sprite.position.y, 2) - - def test_left(self): - self.assertEqual(self.sprite.left, -0.5) - self.assertEqual(self.wide_sprite.left, 1) - - self.sprite.left = 0 - self.assertEqual(self.sprite.position.x, 0.5) - self.assertEqual(self.sprite.position.y, 0) - - self.sprite.left += 2 - self.assertEqual(self.sprite.position.x, 2.5) - self.assertEqual(self.sprite.position.y, 0) - - def test_right(self): - self.assertEqual(self.sprite.right, 0.5) - self.assertEqual(self.wide_sprite.right, 3) - - self.sprite.right = 0 - self.assertEqual(self.sprite.position.x, -0.5) - self.assertEqual(self.sprite.position.y, 0) - - self.sprite.right += 2 - self.assertEqual(self.sprite.position.x, 1.5) - self.assertEqual(self.sprite.position.y, 0) - - def test_top(self): - self.assertEqual(self.sprite.top, 0.5) - self.assertEqual(self.wide_sprite.top, 3) - - self.sprite.top = 0 - self.assertEqual(self.sprite.position.x, 0) - self.assertEqual(self.sprite.position.y, -0.5) - - self.sprite.top += 2 - self.assertEqual(self.sprite.position.x, 0) - self.assertEqual(self.sprite.position.y, 1.5) - - def test_bottom(self): - self.assertEqual(self.sprite.bottom, -0.5) - self.assertEqual(self.wide_sprite.bottom, 1) - - self.sprite.bottom = 0 - self.assertEqual(self.sprite.position.x, 0) - self.assertEqual(self.sprite.position.y, 0.5) - - self.sprite.bottom += 2 - self.assertEqual(self.sprite.position.x, 0) - self.assertEqual(self.sprite.position.y, 2.5) - - def test_left_top(self): - self.assertEqual(self.sprite.left.top, Vector(-0.5, 0.5)) - - self.sprite.left.top = (2, 2) - self.assertEqual(self.sprite.left.top, Vector(2, 2)) - - self.sprite.left.top += (2, 2) - self.assertEqual(self.sprite.left.top, Vector(4, 4)) - - result = self.sprite.left.top + (3, 3) - self.assertEqual(result, Vector(7, 7)) - - self.assertEqual(self.sprite.position, Vector(4.5, 3.5)) - - def test_left_bottom(self): - self.assertEqual(self.sprite.left.bottom, Vector(-0.5, -0.5)) - - self.sprite.left.bottom = (1, 2) - self.assertEqual(self.sprite.left.bottom, Vector(1, 2)) - - self.sprite.left.bottom += (2, 1) - self.assertEqual(self.sprite.left.bottom, Vector(3, 3)) - - result = self.sprite.left.bottom + (3, 2) - self.assertEqual(result, Vector(6, 5)) - - self.assertEqual(self.sprite.position, Vector(3.5, 3.5)) - - def test_left_center(self): - self.assertEqual(self.sprite.left.center, Vector(-0.5, 0)) - - self.sprite.left.center = (1, 1) - self.assertEqual(self.sprite.left.center, Vector(1, 1)) - - self.sprite.left.center += (2, 1) - self.assertEqual(self.sprite.left.center, Vector(3, 2)) - - result = self.sprite.left.center + (2, 3) - self.assertEqual(result, Vector(5, 5)) - - self.assertEqual(self.sprite.position, Vector(3.5, 2)) - - def test_right_bottom(self): - self.assertEqual(self.sprite.right.bottom, Vector(0.5, -0.5)) - - self.sprite.right.bottom = (1, 1) - self.assertEqual(self.sprite.right.bottom, Vector(1, 1)) - - self.sprite.right.bottom += (2, 1) - self.assertEqual(self.sprite.right.bottom, Vector(3, 2)) - - result = self.sprite.right.bottom + (2, 3) - self.assertEqual(result, Vector(5, 5)) - - self.assertEqual(self.sprite.position, Vector(2.5, 2.5)) - - def test_right_top(self): - self.assertEqual(self.sprite.right.top, Vector(0.5, 0.5)) - - self.sprite.right.top = (1, 1) - self.assertEqual(self.sprite.right.top, Vector(1, 1)) - - self.sprite.right.top += (2, 1) - self.assertEqual(self.sprite.right.top, Vector(3, 2)) - - result = self.sprite.right.top + (2, 3) - self.assertEqual(result, Vector(5, 5)) - - self.assertEqual(self.sprite.position, Vector(2.5, 1.5)) - - def test_right_center(self): - self.assertEqual(self.sprite.right.center, Vector(0.5, 0)) - - self.sprite.right.center = (1, 1) - self.assertEqual(self.sprite.right.center, Vector(1, 1)) - - self.sprite.right.center += (2, 1) - self.assertEqual(self.sprite.right.center, Vector(3, 2)) - - result = self.sprite.right.center + (2, 3) - self.assertEqual(result, Vector(5, 5)) - - self.assertEqual(self.sprite.position, Vector(2.5, 2)) - - def test_left_left(self): - self.assertRaises(AttributeError, getattr, self.sprite.left, "left") - self.assertRaises(AttributeError, setattr, self.sprite.left, "left", Vector(1, 1)) - - def test_left_right(self): - self.assertRaises(AttributeError, getattr, self.sprite.left, "right") - self.assertRaises(AttributeError, setattr, self.sprite.left, "right", Vector(1, 1)) - - def test_right_right(self): - self.assertRaises(AttributeError, getattr, self.sprite.right, "right") - self.assertRaises(AttributeError, setattr, self.sprite.right, "right", Vector(1, 1)) - - def test_right_left(self): - self.assertRaises(AttributeError, getattr, self.sprite.right, "left") - self.assertRaises(AttributeError, setattr, self.sprite.right, "left", Vector(1, 1)) - - def test_top_left(self): - self.assertEqual(self.sprite.top.left, Vector(-0.5, 0.5)) - - self.sprite.top.left = (2, 2) - self.assertEqual(self.sprite.top.left, Vector(2, 2)) - - self.sprite.top.left += (2, 2) - self.assertEqual(self.sprite.top.left, Vector(4, 4)) - - result = self.sprite.top.left + (3, 3) - self.assertEqual(result, Vector(7, 7)) - - self.assertEqual(self.sprite.position, Vector(4.5, 3.5)) - - def test_top_right(self): - self.assertEqual(self.sprite.top.right, Vector(0.5, 0.5)) - - self.sprite.top.right = (1, 1) - self.assertEqual(self.sprite.top.right, Vector(1, 1)) - - self.sprite.top.right += (2, 1) - self.assertEqual(self.sprite.top.right, Vector(3, 2)) - - result = self.sprite.top.right + (2, 3) - self.assertEqual(result, Vector(5, 5)) - - self.assertEqual(self.sprite.position, Vector(2.5, 1.5)) - - def test_top_center(self): - self.assertEqual(self.sprite.top.center, Vector(0, 0.5)) - - self.sprite.top.center = (1, 1) - self.assertEqual(self.sprite.top.center, Vector(1, 1)) - - def test_top_top(self): - self.assertRaises(AttributeError, getattr, self.sprite.top, "top") - self.assertRaises(AttributeError, setattr, self.sprite.top, "top", Vector(1, 1)) - - def test_top_bottom(self): - self.assertRaises(AttributeError, getattr, self.sprite.top, "bottom") - self.assertRaises(AttributeError, setattr, self.sprite.top, "bottom", Vector(1, 1)) - - def test_bottom_left(self): - self.assertEqual(self.sprite.bottom.left, Vector(-0.5, -0.5)) - - self.sprite.bottom.left = (2, 2) - self.assertEqual(self.sprite.bottom.left, Vector(2, 2)) - - self.sprite.bottom.left += (2, 2) - self.assertEqual(self.sprite.bottom.left, Vector(4, 4)) - - result = self.sprite.bottom.left + (3, 3) - self.assertEqual(result, Vector(7, 7)) - - self.assertEqual(self.sprite.position, Vector(4.5, 4.5)) - - def test_bottom_right(self): - self.assertEqual(self.sprite.bottom.right, Vector(0.5, -0.5)) - - self.sprite.bottom.right = (1, 1) - self.assertEqual(self.sprite.bottom.right, Vector(1, 1)) - - self.sprite.bottom.right += (2, 1) - self.assertEqual(self.sprite.bottom.right, Vector(3, 2)) - - result = self.sprite.bottom.right + (2, 3) - self.assertEqual(result, Vector(5, 5)) - - self.assertEqual(self.sprite.position, Vector(2.5, 2.5)) - - def test_bottom_center(self): - self.assertEqual(self.sprite.bottom.center, Vector(0, -0.5)) - - self.sprite.bottom.center = (1, 1) - self.assertEqual(self.sprite.bottom.center, Vector(1, 1)) - - def test_bottom_top(self): - self.assertRaises(AttributeError, getattr, self.sprite.bottom, "top") - self.assertRaises(AttributeError, setattr, self.sprite.bottom, "top", Vector(1, 1)) - - def test_bottom_bottom(self): - self.assertRaises(AttributeError, getattr, self.sprite.bottom, "bottom") - self.assertRaises(AttributeError, setattr, self.sprite.bottom, "bottom", Vector(1, 1)) +from ppb import BaseSprite as DeprecatedBaseSprite +from ppb.sprites import * +from ppb_vector import Vector def test_class_attrs(): @@ -280,37 +24,37 @@ class TestSprite(BaseSprite): def test_offset(): - class TestSprite(BaseSprite): + class TestSprite(Sprite): size = 1.1 assert TestSprite().left < -0.5 def test_rotatable_instatiation(): - rotatable = Rotatable() + rotatable = RotatableMixin() assert rotatable.rotation == 0 def test_rotatable_subclass(): - class TestRotatable(Rotatable): + class TestRotatableMixin(RotatableMixin): _rotation = 180 basis = Vector(0, 1) - rotatable = TestRotatable() + rotatable = TestRotatableMixin() assert rotatable.rotation == 180 assert rotatable.facing == Vector(0, -1) def test_rotatable_rotation_setter(): - rotatable = Rotatable() + rotatable = RotatableMixin() rotatable.rotation = 405 assert rotatable.rotation == 45 def test_rotatable_rotate(): - rotatable = Rotatable() + rotatable = RotatableMixin() assert rotatable.rotation == 0 rotatable.rotate(180) @@ -322,18 +66,518 @@ def test_rotatable_rotate(): def test_rotatable_base_sprite(): - test_sprite = BaseSprite() + test_sprite = Sprite() test_sprite.rotate(1) assert test_sprite.rotation == 1 +@given(y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_bottom(y): + sprite = Sprite(position=(0, y)) + assert isclose(sprite.bottom, y - 0.5) + + +def test_sides_bottom_invalid_access(): + sprite = Sprite() + with pytest.raises(AttributeError): + unknown = sprite.bottom.bottom + + with pytest.raises(AttributeError): + unknown = sprite.bottom.top + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@given(y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_bottom_set(y): + sprite = Sprite() + sprite.bottom = y + assert sprite.bottom == y + assert sprite.position.y == y + 0.5 + + +@given(y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_bottom_plus_equals(y): + sprite = Sprite() + sprite.bottom += y + assert sprite.bottom == y - 0.5 + assert sprite.position.y == sprite.bottom + 0.5 + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_bottom_center(x, y): + sprite = Sprite(position=(x, y)) + bottom_center = sprite.bottom.center + assert isclose(bottom_center.y, y - 0.5) + assert isclose(bottom_center.x, x) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_bottom_center_set(x, y, vector_type): + sprite = Sprite() + sprite.bottom.center = vector_type((x, y)) + bottom_center = sprite.bottom.center + assert bottom_center == Vector(x, y) + assert sprite.position == bottom_center + Vector(0, 0.5) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_bottom_center_plus_equals(x, y, vector_type): + sprite = Sprite() + sprite.bottom.center += vector_type((x, y)) + bottom_center = sprite.bottom.center + assert bottom_center == Vector(x, y - 0.5) + assert sprite.position == bottom_center + Vector(0, 0.5) + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_bottom_left(x, y): + sprite = Sprite(position=(x, y)) + bottom_left = sprite.bottom.left + left_bottom = sprite.left.bottom + assert bottom_left == left_bottom + assert isclose(bottom_left.y, y - 0.5) + assert isclose(bottom_left.x, x - 0.5) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_bottom_left_set(x, y, vector_type): + sprite = Sprite() + sprite.bottom.left = vector_type((x, y)) + bottom_left = sprite.bottom.left + left_bottom = sprite.left.bottom + assert bottom_left == left_bottom + assert bottom_left == Vector(x, y) + assert sprite.position == bottom_left + Vector(0.5, 0.5) + + # duplicating to prove top.left and left.top are the same. + sprite = Sprite() + sprite.left.bottom = vector_type((x, y)) + bottom_left = sprite.bottom.left + left_bottom = sprite.left.bottom + assert left_bottom == bottom_left + assert left_bottom == Vector(x, y) + assert sprite.position == left_bottom + Vector(0.5, 0.5) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_bottom_left_plus_equals(x, y, vector_type): + sprite = Sprite() + sprite.bottom.left += vector_type((x, y)) + bottom_left = sprite.bottom.left + left_bottom = sprite.left.bottom + assert bottom_left == left_bottom + assert bottom_left == Vector(x - 0.5, y - 0.5) + assert sprite.position == bottom_left + 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_left = sprite.bottom.left + left_bottom = sprite.left.bottom + assert left_bottom == bottom_left + assert left_bottom == Vector(x +- 0.5, y - 0.5) + assert sprite.position == left_bottom + Vector(0.5, 0.5) + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_bottom_right(x, y): + sprite = Sprite(position=(x, y)) + bottom_right = sprite.bottom.right + right_bottom = sprite.right.bottom + assert bottom_right == right_bottom + assert isclose(bottom_right.y, y - 0.5) + assert isclose(bottom_right.x, x + 0.5) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +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) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +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) + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_center_equals_position(x, y): + sprite = Sprite(position=(x, y)) + assert sprite.center == sprite.position + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_center_setting(x, y, vector_type): + sprite = Sprite() + sprite.center = vector_type((x, y)) + assert sprite.center.x == x + assert sprite.center.y == y + assert sprite.position == sprite.center + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=floats(allow_nan=False, allow_infinity=False), + y=floats(allow_nan=False, allow_infinity=False), + delta_x=floats(allow_nan=False, allow_infinity=False), + delta_y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_center_plus_equals(x, y, delta_x, delta_y, vector_type): + sprite = Sprite(position=(x, y)) + sprite.center += vector_type((delta_x, delta_y)) + assert sprite.position.x == x + delta_x + assert sprite.position.y == y + delta_y + assert sprite.position == sprite.center + + +@given(x=floats(allow_nan=False, allow_infinity=False)) +def test_sides_left(x): + sprite = Sprite(position=(x, 0)) + assert isclose(sprite.left, x - 0.5) + + + +def test_sides_left_invalid_access(): + sprite = Sprite() + with pytest.raises(AttributeError): + unknown = sprite.left.right + + with pytest.raises(AttributeError): + unknown = sprite.left.left + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@given(x=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_left_set(x): + sprite = Sprite() + sprite.left = x + print(float(sprite.left)) + assert sprite.left == x + assert sprite.position.x == x + 0.5 + + +@given(x=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_left_plus_equals(x): + sprite = Sprite() + sprite.left += x + assert sprite.left == x - 0.5 + assert sprite.position.x == sprite.left + 0.5 + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_left_center(x, y): + sprite = Sprite(position=(x, y)) + left_center = sprite.left.center + assert isclose(left_center.y, y) + assert isclose(left_center.x, x - 0.5) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_left_center_set(x, y, vector_type): + sprite = Sprite() + sprite.left.center = vector_type((x, y)) + left_center = sprite.left.center + assert left_center == Vector(x, y) + assert sprite.position == left_center + Vector(0.5, 0) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_left_center_plus_equals(x, y, vector_type): + sprite = Sprite() + sprite.left.center += vector_type((x, y)) + left_center = sprite.left.center + assert left_center == Vector(x - 0.5, y) + assert sprite.position == left_center + Vector(0.5, 0) + + +@given(x=floats(allow_nan=False, allow_infinity=False)) +def test_sides_right(x): + sprite = Sprite(position=(x, 0)) + assert isclose(sprite.right, x + 0.5) + + +def test_sides_right_invalid_access(): + sprite = Sprite() + with pytest.raises(AttributeError): + unknown = sprite.right.right + + with pytest.raises(AttributeError): + unknown = sprite.right.left + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@given(x=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_right_set(x): + sprite = Sprite() + sprite.right = x + print(float(sprite.left)) + assert sprite.right == x + assert sprite.position.x == x - 0.5 + + +@given(x=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_right_plus_equals(x): + sprite = Sprite() + sprite.right += x + assert sprite.right == x + 0.5 + assert sprite.position.x == sprite.right - 0.5 + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_right_center(x, y): + sprite = Sprite(position=(x, y)) + right_center = sprite.right.center + assert isclose(right_center.y, y) + assert isclose(right_center.x, x + 0.5) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_right_center_set(x, y, vector_type): + sprite = Sprite() + sprite.right.center = vector_type((x, y)) + right_center = sprite.right.center + assert right_center == Vector(x, y) + assert sprite.position == right_center + Vector(-0.5, 0) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_right_center_plus_equals(x, y, vector_type): + sprite = Sprite() + sprite.right.center += vector_type((x, y)) + right_center = sprite.right.center + assert right_center == Vector(x + 0.5, y) + assert sprite.position == right_center + Vector(-0.5, 0) + + +@given(y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_top(y): + sprite = Sprite(position=(0, y)) + assert isclose(sprite.top, y + 0.5) + + +def test_sides_top_invalid_access(): + sprite = Sprite() + with pytest.raises(AttributeError): + unknown = sprite.top.bottom + + with pytest.raises(AttributeError): + unknown = sprite.top.top + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@given(y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_top_set(y): + sprite = Sprite() + sprite.top = y + assert sprite.top == y + assert sprite.position.y == y - 0.5 + + +@given(y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_top_plus_equals(y): + sprite = Sprite() + sprite.top += y + assert sprite.top == y + 0.5 + assert sprite.position.y == sprite.top - 0.5 + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_top_center(x, y): + sprite = Sprite(position=(x, y)) + top_center = sprite.top.center + assert isclose(top_center.y, y + 0.5) + assert isclose(top_center.x, x) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_top_center_set(x, y, vector_type): + sprite = Sprite() + sprite.top.center = vector_type((x, y)) + top_center = sprite.top.center + assert top_center == Vector(x, y) + assert sprite.position == top_center + Vector(0, -0.5) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_top_center_plus_equals(x, y, vector_type): + sprite = Sprite() + sprite.top.center += vector_type((x, y)) + top_center = sprite.top.center + assert top_center == Vector(x, y + 0.5) + assert sprite.position == top_center + Vector(0, -0.5) + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_top_left(x, y): + sprite = Sprite(position=(x, y)) + top_left = sprite.top.left + left_top = sprite.left.top + assert top_left == left_top + assert isclose(top_left.y, y + 0.5) + assert isclose(top_left.x, x - 0.5) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +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) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_top_left_plus_equals(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 - 0.5, y + 0.5) + assert sprite.position == top_left + 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_left = sprite.top.left + left_top = sprite.left.top + assert left_top == top_left + assert left_top == Vector(x - 0.5, y + 0.5) + assert sprite.position == left_top + Vector(0.5, -0.5) + + +@given(x=floats(allow_nan=False, allow_infinity=False), y=floats(allow_nan=False, allow_infinity=False)) +def test_sides_top_right(x, y): + sprite = Sprite(position=(x, y)) + top_right = sprite.top.right + right_top = sprite.right.top + assert top_right == right_top + assert isclose(top_right.y, y + 0.5) + assert isclose(top_right.x, x + 0.5) + + +# ints because the kinds of floats hypothesis generates aren't realistic +# to our use case. +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +def test_sides_top_right_set(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, y) + assert sprite.position == top_right + Vector(-0.5, -0.5) + + # duplicating to prove top.left and left.top are the same. + sprite = Sprite() + sprite.right.top = vector_type((x, y)) + top_right = sprite.top.right + right_top = sprite.right.top + assert right_top == top_right + assert right_top == Vector(x, y) + assert sprite.position == right_top + Vector(-0.5, -0.5) + + +@pytest.mark.parametrize("vector_type", [tuple, Vector]) +@given(x=integers(max_value=10_000_000, min_value=-10_000_000), y=integers(max_value=10_000_000, min_value=-10_000_000)) +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) + + def test_sprite_in_main(): """ Test that Sprite.__resource_path__ returns a meaningful value inside REPLs where __main__ doesn't have a file. """ - class TestSprite(BaseSprite): + class TestSprite(Sprite): pass s = TestSprite() @@ -341,3 +585,15 @@ class TestSprite(BaseSprite): with patch("ppb.sprites.getfile", side_effect=TypeError): # This patch simulates what happens when TestSprite was defined in the REPL assert s.__image__() # We don't care what it is, as long as it's something + + +def test_deprecated_base_sprite_warns(): + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + sprite = DeprecatedBaseSprite() + # Verify some things + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated" in str(w[-1].message)