Skip to content

Commit

Permalink
Merge pull request #219 from pygame-community/circle_collidelist_all
Browse files Browse the repository at this point in the history
Circle `collidelist() / collidelistall()`
  • Loading branch information
itzpr3d4t0r authored May 27, 2024
2 parents 37c4dac + 2718395 commit 2ddeef1
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 15 deletions.
46 changes: 46 additions & 0 deletions docs/circle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,52 @@ Circle Methods

.. ## 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 `Line`, `Circle`, `Rect`, `Polygon`, tuple or list containing the
x and y coordinates of a point, or `Vector2` objects. 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::
It is important to note that the shapes must be actual shape objects, such as
`Line`, `Circle`, `Polygon`, or `Rect` 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 type of shape represented by the coordinates cannot be determined.
For example, a tuple with the format (a, b, c, d) could represent either a `Line`
or a `Rect` object, and there is no way to determine which is which without
explicitly passing a `Line` or `Rect` object as an argument.

.. ## 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 `Line`, `Circle`, `Rect`, `Polygon`, tuple or list containing the
x and y coordinates of a point, or `Vector2` objects. 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::
It is important to note that the shapes must be actual shape objects, such as
`Line`, `Circle`, `Polygon`, or `Rect` 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 type of shape represented by the coordinates cannot be determined.
For example, a tuple with the format (a, b, c, d) could represent either a `Line`
or a `Rect` object, and there is no way to determine which is which without
explicitly passing a `Line` or `Rect` object as an argument.

.. ## Circle.collidelistall ##
.. method:: contains

| :sl:`test if a shape or point is inside the circle`
Expand Down
4 changes: 4 additions & 0 deletions docs/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ performing transformations and checking for collisions with other objects.

collideswith: Checks if the circle collides with the given object.

collidelist: Checks if the circle collides with any of the given objects.

collidelistall: Checks if the circle collides with all of the given objects.

contains: Checks if the circle fully contains the given object.

rotate: Rotates the circle by the given amount.
Expand Down
2 changes: 2 additions & 0 deletions geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ class Circle:
@overload
def colliderect(self, x: int, y: int, w: int, h: int) -> bool: ...
def collideswith(self, other: _CanBeCollided) -> bool: ...
def collidelist(self, colliders: Sequence[_CanBeCollided]) -> int: ...
def collidelistall(self, colliders: Sequence[_CanBeCollided]) -> List[int]: ...
def __copy__(self) -> Circle: ...

copy = __copy__
Expand Down
173 changes: 158 additions & 15 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -320,37 +320,44 @@ pg_circle_colliderect(pgCircleObject *self, PyObject *const *args,
return PyBool_FromLong(pgCollision_RectCircle(&temp, &self->circle));
}

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);
return pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc);
}
else if (pgRect_Check(arg)) {
result = pgCollision_RectCircle(&pgRect_AsRect(arg), scirc);
return pgCollision_RectCircle(&pgRect_AsRect(arg), scirc);
}
else if (pgLine_Check(arg)) {
result = pgCollision_LineCircle(&pgLine_AsLine(arg), scirc);
return pgCollision_LineCircle(&pgLine_AsLine(arg), scirc);
}
else if (pgPolygon_Check(arg)) {
result =
pgCollision_CirclePolygon(scirc, &pgPolygon_AsPolygon(arg), 0);
return pgCollision_CirclePolygon(scirc, &pgPolygon_AsPolygon(arg), 0);
}
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 2 numbers");
return -1;
}
result = pgCollision_CirclePoint(scirc, x, y);
return pgCollision_CirclePoint(scirc, x, y);
}
else {
return RAISE(PyExc_TypeError,
"Invalid shape argument, must be a CircleType, RectType, "
"LineType, PolygonType or a sequence of 2 numbers");

PyErr_SetString(PyExc_TypeError,
"Invalid shape argument, must be a CircleType, RectType, "
"LineType, PolygonType or a sequence of 2 numbers");
return -1;
}

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);
Expand Down Expand Up @@ -591,6 +598,140 @@ pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args,
Py_RETURN_NONE;
}

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, "Argument must be a sequence");
}

/* fast path */
if (PySequence_FAST_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_GetItem(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, **items;
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 (PySequence_FAST_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_GetItem(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 struct PyMethodDef pg_circle_methods[] = {
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
NULL},
Expand All @@ -600,6 +741,8 @@ static struct PyMethodDef pg_circle_methods[] = {
{"collideswith", (PyCFunction)pg_circle_collideswith, METH_O, NULL},
{"collidepolygon", (PyCFunction)pg_circle_collidepolygon, METH_FASTCALL,
NULL},
{"collidelist", (PyCFunction)pg_circle_collidelist, METH_O, NULL},
{"collidelistall", (PyCFunction)pg_circle_collidelistall, METH_O, NULL},
{"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS, NULL},
{"update", (PyCFunction)pg_circle_update, METH_FASTCALL, NULL},
{"move", (PyCFunction)pg_circle_move, METH_FASTCALL, NULL},
Expand Down
108 changes: 108 additions & 0 deletions test/test_circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,114 @@ def assert_approx_equal(circle1, circle2, eps=1e-12):
c.rotate_ip(angle, center)
assert_approx_equal(c, rotate_circle(c, angle, center))

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),
Line(10, 10, 4, 4),
Polygon([(10, 10), (34, 10), (4, 43)]),
]

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)]
lines = [Line(10, 10, 4, 4), Line(100, 100, 553, 553), Line(136, 110, 324, 337)]
polygons = [
Polygon([(100, 100), (34, 10), (4, 43)]),
Polygon([(20, 10), (34, 10), (4, 43)]),
Polygon([(10, 10), (34, 10), (4, 43)]),
]
expected = [1, 2, 0, 2]

for objects, expected in zip([circles, rects, lines, polygons], 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),
Line(10, 10, 4, 4),
Polygon([(10, 10), (34, 10), (4, 43)]),
]

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)]
lines = [Line(10, 10, 4, 4), Line(0, 0, 553, 553), Line(5, 5, 10, 11)]
polygons = [
Polygon([(100, 100), (34, 10), (4, 43)]),
Polygon([(20, 10), (34, 10), (4, 43)]),
Polygon([(10, 10), (34, 10), (4, 43)]),
]
expected = [[1, 2], [2], [0, 1, 2], [2]]

for objects, expected in zip([circles, rects, lines, polygons], expected):
self.assertEqual(c.collidelistall(objects), expected)


if __name__ == "__main__":
unittest.main()

0 comments on commit 2ddeef1

Please sign in to comment.