diff --git a/buildconfig/stubs/pygame/geometry.pyi b/buildconfig/stubs/pygame/geometry.pyi index 14ab6563b0..c07a87886f 100644 --- a/buildconfig/stubs/pygame/geometry.pyi +++ b/buildconfig/stubs/pygame/geometry.pyi @@ -4,6 +4,7 @@ from typing import ( Callable, Protocol, Tuple, + Sequence, List, ) @@ -95,6 +96,8 @@ class Circle: @overload def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ... def collideswith(self, other: _CanBeCollided, /) -> bool: ... + def collidelist(self, colliders: Sequence[_CanBeCollided], /) -> int: ... + def collidelistall(self, colliders: Sequence[_CanBeCollided], /) -> List[int]: ... def intersect(self, other: _CanBeIntersected, /) -> List[Tuple[float, float]]: ... def contains(self, shape: _CanBeCollided) -> bool: ... @overload diff --git a/docs/reST/ref/geometry.rst b/docs/reST/ref/geometry.rst index 57ad92f423..2186651313 100644 --- a/docs/reST/ref/geometry.rst +++ b/docs/reST/ref/geometry.rst @@ -194,7 +194,7 @@ x, y coordinates and width, height as its argument. .. versionadded:: 2.4.0 - + .. ## Circle.colliderect ## .. method:: collideswith @@ -217,6 +217,48 @@ .. ## Circle.collideswith ## + .. method:: collidelist + + | :sl:`test if a list of objects collide with the circle` + | :sg:`collidelist(colliders) -> int` + + The `collidelist` method tests whether a given list of shapes or points collides + (overlaps) with this `Circle` object. The function takes in a single argument, which + must be a list of `Circle`, `Rect`, `FRect`, or a point. The function returns the index + of the first shape or point in the list that collides with the `Circle` object, or + -1 if there is no collision. + + .. note:: + The shapes must be actual shape objects, such as `Circle`, `Rect` or `FRect` + instances. It is not possible to pass a tuple or list of coordinates representing + the shape as an argument (except for a point), because the shape type can't be + determined from the coordinates alone. + + .. versionadded:: 2.5.2 + + .. ## Circle.collidelist ## + + .. method:: collidelistall + + | :sl:`test if all objects in a list collide with the circle` + | :sg:`collidelistall(colliders) -> list` + + The `collidelistall` method tests whether a given list of shapes or points collides + (overlaps) with this `Circle` object. The function takes in a single argument, which + must be a list of `Circle`, `Rect`, `FRect`, or a point. The function returns a list + containing the indices of all the shapes or points in the list that collide with + the `Circle` object, or an empty list if there is no collision. + + .. note:: + The shapes must be actual shape objects, such as `Circle`, `Rect` or `FRect` + instances. It is not possible to pass a tuple or list of coordinates representing + the shape as an argument (except for a point), because the shape type can't be + determined from the coordinates alone. + + .. versionadded:: 2.5.2 + + .. ## Circle.collidelistall ## + .. method:: contains | :sl:`tests if a shape or point is inside the circle` diff --git a/src_c/circle.c b/src_c/circle.c index 31afb1fb2c..bb9eff0614 100644 --- a/src_c/circle.c +++ b/src_c/circle.c @@ -312,11 +312,10 @@ pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args, Py_RETURN_NONE; } -static PyObject * -pg_circle_collideswith(pgCircleObject *self, PyObject *arg) +static PG_FORCEINLINE int +_pg_circle_collideswith(pgCircleBase *scirc, PyObject *arg) { int result = 0; - pgCircleBase *scirc = &self->circle; if (pgCircle_Check(arg)) { result = pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc); } @@ -335,21 +334,168 @@ pg_circle_collideswith(pgCircleObject *self, PyObject *arg) else if (PySequence_Check(arg)) { double x, y; if (!pg_TwoDoublesFromObj(arg, &x, &y)) { - return RAISE( + PyErr_SetString( PyExc_TypeError, "Invalid point argument, must be a sequence of two numbers"); + return -1; } result = pgCollision_CirclePoint(scirc, x, y); } else { - return RAISE(PyExc_TypeError, - "Invalid shape argument, must be a Circle, Rect / FRect, " - "Line, Polygon or a sequence of two numbers"); + PyErr_SetString( + PyExc_TypeError, + "Invalid point argument, must be a sequence of 2 numbers"); + return -1; + } + + return result; +} + +static PyObject * +pg_circle_collideswith(pgCircleObject *self, PyObject *arg) +{ + int result = _pg_circle_collideswith(&self->circle, arg); + if (result == -1) { + return NULL; } return PyBool_FromLong(result); } +static PyObject * +pg_circle_collidelist(pgCircleObject *self, PyObject *arg) +{ + Py_ssize_t i; + pgCircleBase *scirc = &self->circle; + int colliding; + + if (!PySequence_Check(arg)) { + return RAISE(PyExc_TypeError, "colliders argument must be a sequence"); + } + + /* fast path */ + if (pgSequenceFast_Check(arg)) { + PyObject **items = PySequence_Fast_ITEMS(arg); + for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) { + if ((colliding = _pg_circle_collideswith(scirc, items[i])) == -1) { + /*invalid shape*/ + return NULL; + } + if (colliding) { + return PyLong_FromSsize_t(i); + } + } + return PyLong_FromLong(-1); + } + + /* general sequence path */ + for (i = 0; i < PySequence_Length(arg); i++) { + PyObject *obj = PySequence_ITEM(arg, i); + if (!obj) { + return NULL; + } + + if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) { + /*invalid shape*/ + Py_DECREF(obj); + return NULL; + } + Py_DECREF(obj); + + if (colliding) { + return PyLong_FromSsize_t(i); + } + } + + return PyLong_FromLong(-1); +} + +static PyObject * +pg_circle_collidelistall(pgCircleObject *self, PyObject *arg) +{ + PyObject *ret; + Py_ssize_t i; + pgCircleBase *scirc = &self->circle; + int colliding; + + if (!PySequence_Check(arg)) { + return RAISE(PyExc_TypeError, "Argument must be a sequence"); + } + + ret = PyList_New(0); + if (!ret) { + return NULL; + } + + /* fast path */ + if (pgSequenceFast_Check(arg)) { + PyObject **items = PySequence_Fast_ITEMS(arg); + + for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) { + if ((colliding = _pg_circle_collideswith(scirc, items[i])) == -1) { + /*invalid shape*/ + Py_DECREF(ret); + return NULL; + } + + if (!colliding) { + continue; + } + + PyObject *num = PyLong_FromSsize_t(i); + if (!num) { + Py_DECREF(ret); + return NULL; + } + + if (PyList_Append(ret, num)) { + Py_DECREF(num); + Py_DECREF(ret); + return NULL; + } + Py_DECREF(num); + } + + return ret; + } + + /* general sequence path */ + for (i = 0; i < PySequence_Length(arg); i++) { + PyObject *obj = PySequence_ITEM(arg, i); + if (!obj) { + Py_DECREF(ret); + return NULL; + } + + if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) { + /*invalid shape*/ + Py_DECREF(ret); + Py_DECREF(obj); + return NULL; + } + Py_DECREF(obj); + + if (!colliding) { + continue; + } + + PyObject *num = PyLong_FromSsize_t(i); + if (!num) { + Py_DECREF(ret); + return NULL; + } + + if (PyList_Append(ret, num)) { + Py_DECREF(num); + Py_DECREF(ret); + return NULL; + } + Py_DECREF(num); + } + + return ret; +} + static PyObject * pg_circle_as_rect(pgCircleObject *self, PyObject *_null) { @@ -461,6 +607,10 @@ static struct PyMethodDef pg_circle_methods[] = { DOC_CIRCLE_UPDATE}, {"collideswith", (PyCFunction)pg_circle_collideswith, METH_O, DOC_CIRCLE_COLLIDESWITH}, + {"collidelist", (PyCFunction)pg_circle_collidelist, METH_O, + DOC_CIRCLE_COLLIDELIST}, + {"collidelistall", (PyCFunction)pg_circle_collidelistall, METH_O, + DOC_CIRCLE_COLLIDELISTALL}, {"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS, DOC_CIRCLE_ASRECT}, {"as_frect", (PyCFunction)pg_circle_as_frect, METH_NOARGS, diff --git a/src_c/doc/geometry_doc.h b/src_c/doc/geometry_doc.h index 89a7ef6f3d..c624f2c80b 100644 --- a/src_c/doc/geometry_doc.h +++ b/src_c/doc/geometry_doc.h @@ -13,6 +13,8 @@ #define DOC_CIRCLE_COLLIDECIRCLE "collidecircle(circle, /) -> bool\ncollidecircle(x, y, radius, /) -> bool\ncollidecircle((x, y), radius, /) -> bool\ncollidecircle(vector2, radius, /) -> bool\ntests if a circle collides with this circle" #define DOC_CIRCLE_COLLIDERECT "colliderect(rect, /) -> bool\ncolliderect((x, y, width, height), /) -> bool\ncolliderect(x, y, width, height, /) -> bool\ncolliderect((x, y), (width, height), /) -> bool\ncolliderect(vector2, (width, height), /) -> bool\ntests if a rectangle collides with this circle" #define DOC_CIRCLE_COLLIDESWITH "collideswith(circle, /) -> bool\ncollideswith(rect, /) -> bool\ncollideswith((x, y), /) -> bool\ncollideswith(vector2, /) -> bool\ntests if a shape or point collides with this circle" +#define DOC_CIRCLE_COLLIDELIST "collidelist(colliders) -> int\ntest if a list of objects collide with the circle" +#define DOC_CIRCLE_COLLIDELISTALL "collidelistall(colliders) -> list\ntest if all objects in a list collide with the circle" #define DOC_CIRCLE_CONTAINS "contains(circle, /) -> bool\ncontains(rect, /) -> bool\ncontains((x, y), /) -> bool\ncontains(vector2, /) -> bool\ntests if a shape or point is inside the circle" #define DOC_CIRCLE_MOVE "move((x, y), /) -> Circle\nmove(x, y, /) -> Circle\nmove(vector2, /) -> Circle\nmoves the circle by a given amount" #define DOC_CIRCLE_MOVEIP "move_ip((x, y), /) -> None\nmove_ip(x, y, /) -> None\nmove_ip(vector2, /) -> None\nmoves the circle by a given amount, in place" diff --git a/test/geometry_test.py b/test/geometry_test.py index e95d43ba46..ddb53917a8 100644 --- a/test/geometry_test.py +++ b/test/geometry_test.py @@ -682,6 +682,102 @@ def test_collideswith(self): self.assertTrue(c.collideswith(p)) self.assertFalse(c.collideswith(p2)) + def test_collidelist_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False) + + c = Circle(10, 10, 4) + + for value in invalid_types: + with self.assertRaises(TypeError): + c.collidelist(value) + + def test_collidelist_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + c = Circle(10, 10, 4) + + circles = [(Circle(10, 10, 4), Circle(10, 10, 4))] + + with self.assertRaises(TypeError): + c.collidelist() + + with self.assertRaises(TypeError): + c.collidelist(circles, 1) + + def test_collidelist_return_type(self): + """Tests if the function returns the correct type""" + c = Circle(10, 10, 4) + + objects = [ + Circle(10, 10, 4), + Rect(10, 10, 4, 4), + ] + + for object in objects: + self.assertIsInstance(c.collidelist([object]), int) + + def test_collidelist(self): + """Ensures that the collidelist method works correctly""" + c = Circle(10, 10, 4) + + circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)] + rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)] + points = [(-10, -10), Vector2(1, 1), Vector2(10, -20), (10, 10)] + expected = [1, 2, 3] + + for objects, expected in zip([circles, rects, points], expected): + self.assertEqual(c.collidelist(objects), expected) + + def test_collidelistall_argtype(self): + """Tests if the function correctly handles incorrect types as parameters""" + + invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False) + + c = Circle(10, 10, 4) + + for value in invalid_types: + with self.assertRaises(TypeError): + c.collidelistall(value) + + def test_collidelistall_argnum(self): + """Tests if the function correctly handles incorrect number of parameters""" + c = Circle(10, 10, 4) + + circles = [(Circle(10, 10, 4), Circle(10, 10, 4))] + + with self.assertRaises(TypeError): + c.collidelistall() + + with self.assertRaises(TypeError): + c.collidelistall(circles, 1) + + def test_collidelistall_return_type(self): + """Tests if the function returns the correct type""" + c = Circle(10, 10, 4) + + objects = [ + Circle(10, 10, 4), + Rect(10, 10, 4, 4), + (10, 10), + Vector2(9, 9), + ] + + for object in objects: + self.assertIsInstance(c.collidelistall([object]), list) + + def test_collidelistall(self): + """Ensures that the collidelistall method works correctly""" + c = Circle(10, 10, 4) + + circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)] + rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)] + points = [Vector2(-10, -10), (8, 8), (10, -20), Vector2(10, 10)] + expected = [[1, 2], [2], [1, 3]] + + for objects, expected in zip([circles, rects, points], expected): + self.assertEqual(c.collidelistall(objects), expected) + def test_update(self): """Ensures that updating the circle position and dimension correctly updates position and dimension"""