Skip to content

Commit 2ddeef1

Browse files
authored
Merge pull request #219 from pygame-community/circle_collidelist_all
Circle `collidelist() / collidelistall()`
2 parents 37c4dac + 2718395 commit 2ddeef1

File tree

5 files changed

+318
-15
lines changed

5 files changed

+318
-15
lines changed

docs/circle.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,52 @@ Circle Methods
298298

299299
.. ## Circle.collideswith ##
300300
301+
.. method:: collidelist
302+
303+
| :sl:`test if a list of objects collide with the circle`
304+
| :sg:`collidelist(colliders) -> int`
305+
306+
The `collidelist` method tests whether a given list of shapes or points collides
307+
(overlaps) with this `Circle` object. The function takes in a single argument, which
308+
must be a list of `Line`, `Circle`, `Rect`, `Polygon`, tuple or list containing the
309+
x and y coordinates of a point, or `Vector2` objects. The function returns the index
310+
of the first shape or point in the list that collides with the `Circle` object, or
311+
-1 if there is no collision.
312+
313+
.. note::
314+
It is important to note that the shapes must be actual shape objects, such as
315+
`Line`, `Circle`, `Polygon`, or `Rect` instances. It is not possible to pass a tuple
316+
or list of coordinates representing the shape as an argument(except for a point),
317+
because the type of shape represented by the coordinates cannot be determined.
318+
For example, a tuple with the format (a, b, c, d) could represent either a `Line`
319+
or a `Rect` object, and there is no way to determine which is which without
320+
explicitly passing a `Line` or `Rect` object as an argument.
321+
322+
.. ## Circle.collidelist ##
323+
324+
.. method:: collidelistall
325+
326+
| :sl:`test if all objects in a list collide with the circle`
327+
| :sg:`collidelistall(colliders) -> list`
328+
329+
The `collidelistall` method tests whether a given list of shapes or points collides
330+
(overlaps) with this `Circle` object. The function takes in a single argument, which
331+
must be a list of `Line`, `Circle`, `Rect`, `Polygon`, tuple or list containing the
332+
x and y coordinates of a point, or `Vector2` objects. The function returns a list
333+
containing the indices of all the shapes or points in the list that collide with
334+
the `Circle` object, or an empty list if there is no collision.
335+
336+
.. note::
337+
It is important to note that the shapes must be actual shape objects, such as
338+
`Line`, `Circle`, `Polygon`, or `Rect` instances. It is not possible to pass a tuple
339+
or list of coordinates representing the shape as an argument(except for a point),
340+
because the type of shape represented by the coordinates cannot be determined.
341+
For example, a tuple with the format (a, b, c, d) could represent either a `Line`
342+
or a `Rect` object, and there is no way to determine which is which without
343+
explicitly passing a `Line` or `Rect` object as an argument.
344+
345+
.. ## Circle.collidelistall ##
346+
301347
.. method:: contains
302348

303349
| :sl:`test if a shape or point is inside the circle`

docs/geometry.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ performing transformations and checking for collisions with other objects.
5858

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

61+
collidelist: Checks if the circle collides with any of the given objects.
62+
63+
collidelistall: Checks if the circle collides with all of the given objects.
64+
6165
contains: Checks if the circle fully contains the given object.
6266

6367
rotate: Rotates the circle by the given amount.

geometry.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ class Circle:
176176
@overload
177177
def colliderect(self, x: int, y: int, w: int, h: int) -> bool: ...
178178
def collideswith(self, other: _CanBeCollided) -> bool: ...
179+
def collidelist(self, colliders: Sequence[_CanBeCollided]) -> int: ...
180+
def collidelistall(self, colliders: Sequence[_CanBeCollided]) -> List[int]: ...
179181
def __copy__(self) -> Circle: ...
180182

181183
copy = __copy__

src_c/circle.c

Lines changed: 158 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -320,37 +320,44 @@ pg_circle_colliderect(pgCircleObject *self, PyObject *const *args,
320320
return PyBool_FromLong(pgCollision_RectCircle(&temp, &self->circle));
321321
}
322322

323-
static PyObject *
324-
pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
323+
static PG_FORCEINLINE int
324+
_pg_circle_collideswith(pgCircleBase *scirc, PyObject *arg)
325325
{
326-
int result = 0;
327-
pgCircleBase *scirc = &self->circle;
328326
if (pgCircle_Check(arg)) {
329-
result = pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc);
327+
return pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc);
330328
}
331329
else if (pgRect_Check(arg)) {
332-
result = pgCollision_RectCircle(&pgRect_AsRect(arg), scirc);
330+
return pgCollision_RectCircle(&pgRect_AsRect(arg), scirc);
333331
}
334332
else if (pgLine_Check(arg)) {
335-
result = pgCollision_LineCircle(&pgLine_AsLine(arg), scirc);
333+
return pgCollision_LineCircle(&pgLine_AsLine(arg), scirc);
336334
}
337335
else if (pgPolygon_Check(arg)) {
338-
result =
339-
pgCollision_CirclePolygon(scirc, &pgPolygon_AsPolygon(arg), 0);
336+
return pgCollision_CirclePolygon(scirc, &pgPolygon_AsPolygon(arg), 0);
340337
}
341338
else if (PySequence_Check(arg)) {
342339
double x, y;
343340
if (!pg_TwoDoublesFromObj(arg, &x, &y)) {
344-
return RAISE(
341+
PyErr_SetString(
345342
PyExc_TypeError,
346343
"Invalid point argument, must be a sequence of 2 numbers");
344+
return -1;
347345
}
348-
result = pgCollision_CirclePoint(scirc, x, y);
346+
return pgCollision_CirclePoint(scirc, x, y);
349347
}
350-
else {
351-
return RAISE(PyExc_TypeError,
352-
"Invalid shape argument, must be a CircleType, RectType, "
353-
"LineType, PolygonType or a sequence of 2 numbers");
348+
349+
PyErr_SetString(PyExc_TypeError,
350+
"Invalid shape argument, must be a CircleType, RectType, "
351+
"LineType, PolygonType or a sequence of 2 numbers");
352+
return -1;
353+
}
354+
355+
static PyObject *
356+
pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
357+
{
358+
int result = _pg_circle_collideswith(&self->circle, arg);
359+
if (result == -1) {
360+
return NULL;
354361
}
355362

356363
return PyBool_FromLong(result);
@@ -591,6 +598,140 @@ pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args,
591598
Py_RETURN_NONE;
592599
}
593600

601+
static PyObject *
602+
pg_circle_collidelist(pgCircleObject *self, PyObject *arg)
603+
{
604+
Py_ssize_t i;
605+
pgCircleBase *scirc = &self->circle;
606+
int colliding;
607+
608+
if (!PySequence_Check(arg)) {
609+
return RAISE(PyExc_TypeError, "Argument must be a sequence");
610+
}
611+
612+
/* fast path */
613+
if (PySequence_FAST_CHECK(arg)) {
614+
PyObject **items = PySequence_Fast_ITEMS(arg);
615+
for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) {
616+
if ((colliding = _pg_circle_collideswith(scirc, items[i])) == -1) {
617+
/*invalid shape*/
618+
return NULL;
619+
}
620+
if (colliding) {
621+
return PyLong_FromSsize_t(i);
622+
}
623+
}
624+
return PyLong_FromLong(-1);
625+
}
626+
627+
/* general sequence path */
628+
for (i = 0; i < PySequence_Length(arg); i++) {
629+
PyObject *obj = PySequence_GetItem(arg, i);
630+
if (!obj) {
631+
return NULL;
632+
}
633+
634+
if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) {
635+
/*invalid shape*/
636+
Py_DECREF(obj);
637+
return NULL;
638+
}
639+
Py_DECREF(obj);
640+
641+
if (colliding) {
642+
return PyLong_FromSsize_t(i);
643+
}
644+
}
645+
646+
return PyLong_FromLong(-1);
647+
}
648+
649+
static PyObject *
650+
pg_circle_collidelistall(pgCircleObject *self, PyObject *arg)
651+
{
652+
PyObject *ret, **items;
653+
Py_ssize_t i;
654+
pgCircleBase *scirc = &self->circle;
655+
int colliding;
656+
657+
if (!PySequence_Check(arg)) {
658+
return RAISE(PyExc_TypeError, "Argument must be a sequence");
659+
}
660+
661+
ret = PyList_New(0);
662+
if (!ret) {
663+
return NULL;
664+
}
665+
666+
/* fast path */
667+
if (PySequence_FAST_CHECK(arg)) {
668+
PyObject **items = PySequence_Fast_ITEMS(arg);
669+
670+
for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) {
671+
if ((colliding = _pg_circle_collideswith(scirc, items[i])) == -1) {
672+
/*invalid shape*/
673+
Py_DECREF(ret);
674+
return NULL;
675+
}
676+
677+
if (!colliding) {
678+
continue;
679+
}
680+
681+
PyObject *num = PyLong_FromSsize_t(i);
682+
if (!num) {
683+
Py_DECREF(ret);
684+
return NULL;
685+
}
686+
687+
if (PyList_Append(ret, num)) {
688+
Py_DECREF(num);
689+
Py_DECREF(ret);
690+
return NULL;
691+
}
692+
Py_DECREF(num);
693+
}
694+
695+
return ret;
696+
}
697+
698+
/* general sequence path */
699+
for (i = 0; i < PySequence_Length(arg); i++) {
700+
PyObject *obj = PySequence_GetItem(arg, i);
701+
if (!obj) {
702+
Py_DECREF(ret);
703+
return NULL;
704+
}
705+
706+
if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) {
707+
/*invalid shape*/
708+
Py_DECREF(ret);
709+
Py_DECREF(obj);
710+
return NULL;
711+
}
712+
Py_DECREF(obj);
713+
714+
if (!colliding) {
715+
continue;
716+
}
717+
718+
PyObject *num = PyLong_FromSsize_t(i);
719+
if (!num) {
720+
Py_DECREF(ret);
721+
return NULL;
722+
}
723+
724+
if (PyList_Append(ret, num)) {
725+
Py_DECREF(num);
726+
Py_DECREF(ret);
727+
return NULL;
728+
}
729+
Py_DECREF(num);
730+
}
731+
732+
return ret;
733+
}
734+
594735
static struct PyMethodDef pg_circle_methods[] = {
595736
{"collidecircle", (PyCFunction)pg_circle_collidecircle, METH_FASTCALL,
596737
NULL},
@@ -600,6 +741,8 @@ static struct PyMethodDef pg_circle_methods[] = {
600741
{"collideswith", (PyCFunction)pg_circle_collideswith, METH_O, NULL},
601742
{"collidepolygon", (PyCFunction)pg_circle_collidepolygon, METH_FASTCALL,
602743
NULL},
744+
{"collidelist", (PyCFunction)pg_circle_collidelist, METH_O, NULL},
745+
{"collidelistall", (PyCFunction)pg_circle_collidelistall, METH_O, NULL},
603746
{"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS, NULL},
604747
{"update", (PyCFunction)pg_circle_update, METH_FASTCALL, NULL},
605748
{"move", (PyCFunction)pg_circle_move, METH_FASTCALL, NULL},

test/test_circle.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,114 @@ def assert_approx_equal(circle1, circle2, eps=1e-12):
13741374
c.rotate_ip(angle, center)
13751375
assert_approx_equal(c, rotate_circle(c, angle, center))
13761376

1377+
def test_collidelist_argtype(self):
1378+
"""Tests if the function correctly handles incorrect types as parameters"""
1379+
1380+
invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)
1381+
1382+
c = Circle(10, 10, 4)
1383+
1384+
for value in invalid_types:
1385+
with self.assertRaises(TypeError):
1386+
c.collidelist(value)
1387+
1388+
def test_collidelist_argnum(self):
1389+
"""Tests if the function correctly handles incorrect number of parameters"""
1390+
c = Circle(10, 10, 4)
1391+
1392+
circles = [(Circle(10, 10, 4), Circle(10, 10, 4))]
1393+
1394+
with self.assertRaises(TypeError):
1395+
c.collidelist()
1396+
1397+
with self.assertRaises(TypeError):
1398+
c.collidelist(circles, 1)
1399+
1400+
def test_collidelist_return_type(self):
1401+
"""Tests if the function returns the correct type"""
1402+
c = Circle(10, 10, 4)
1403+
1404+
objects = [
1405+
Circle(10, 10, 4),
1406+
Rect(10, 10, 4, 4),
1407+
Line(10, 10, 4, 4),
1408+
Polygon([(10, 10), (34, 10), (4, 43)]),
1409+
]
1410+
1411+
for object in objects:
1412+
self.assertIsInstance(c.collidelist([object]), int)
1413+
1414+
def test_collidelist(self):
1415+
"""Ensures that the collidelist method works correctly"""
1416+
c = Circle(10, 10, 4)
1417+
1418+
circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)]
1419+
rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)]
1420+
lines = [Line(10, 10, 4, 4), Line(100, 100, 553, 553), Line(136, 110, 324, 337)]
1421+
polygons = [
1422+
Polygon([(100, 100), (34, 10), (4, 43)]),
1423+
Polygon([(20, 10), (34, 10), (4, 43)]),
1424+
Polygon([(10, 10), (34, 10), (4, 43)]),
1425+
]
1426+
expected = [1, 2, 0, 2]
1427+
1428+
for objects, expected in zip([circles, rects, lines, polygons], expected):
1429+
self.assertEqual(c.collidelist(objects), expected)
1430+
1431+
def test_collidelistall_argtype(self):
1432+
"""Tests if the function correctly handles incorrect types as parameters"""
1433+
1434+
invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)
1435+
1436+
c = Circle(10, 10, 4)
1437+
1438+
for value in invalid_types:
1439+
with self.assertRaises(TypeError):
1440+
c.collidelistall(value)
1441+
1442+
def test_collidelistall_argnum(self):
1443+
"""Tests if the function correctly handles incorrect number of parameters"""
1444+
c = Circle(10, 10, 4)
1445+
1446+
circles = [(Circle(10, 10, 4), Circle(10, 10, 4))]
1447+
1448+
with self.assertRaises(TypeError):
1449+
c.collidelistall()
1450+
1451+
with self.assertRaises(TypeError):
1452+
c.collidelistall(circles, 1)
1453+
1454+
def test_collidelistall_return_type(self):
1455+
"""Tests if the function returns the correct type"""
1456+
c = Circle(10, 10, 4)
1457+
1458+
objects = [
1459+
Circle(10, 10, 4),
1460+
Rect(10, 10, 4, 4),
1461+
Line(10, 10, 4, 4),
1462+
Polygon([(10, 10), (34, 10), (4, 43)]),
1463+
]
1464+
1465+
for object in objects:
1466+
self.assertIsInstance(c.collidelistall([object]), list)
1467+
1468+
def test_collidelistall(self):
1469+
"""Ensures that the collidelistall method works correctly"""
1470+
c = Circle(10, 10, 4)
1471+
1472+
circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)]
1473+
rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)]
1474+
lines = [Line(10, 10, 4, 4), Line(0, 0, 553, 553), Line(5, 5, 10, 11)]
1475+
polygons = [
1476+
Polygon([(100, 100), (34, 10), (4, 43)]),
1477+
Polygon([(20, 10), (34, 10), (4, 43)]),
1478+
Polygon([(10, 10), (34, 10), (4, 43)]),
1479+
]
1480+
expected = [[1, 2], [2], [0, 1, 2], [2]]
1481+
1482+
for objects, expected in zip([circles, rects, lines, polygons], expected):
1483+
self.assertEqual(c.collidelistall(objects), expected)
1484+
13771485

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

0 commit comments

Comments
 (0)