Skip to content

Commit a4eec3a

Browse files
damusssMyreMylarEmc2356andrewhong04ScriptLineStudios
authored
Add Circle.collidelist/collidelistall() (#2880)
Co-authored-by: Damus666 <97639432+Damus666@users.noreply.github.com> Co-authored-by: Dan Lawrence <danintheshed@gmail.com> Co-authored-by: Emc2356 <63981925+emc2356@users.noreply.github.com> Co-authored-by: NovialRiptide <35881688+novialriptide@users.noreply.github.com> Co-authored-by: ScriptLineStudios <scriptlinestudios@protonmail.com> Co-authored-by: Avaxar <44055981+avaxar@users.noreply.github.com> Co-authored-by: maqa41 <amehebbet41@gmail.com> Co-authored-by: itzpr3d4t0r <103119829+itzpr3d4t0r@users.noreply.github.com>
1 parent ddc900e commit a4eec3a

File tree

5 files changed

+300
-7
lines changed

5 files changed

+300
-7
lines changed

buildconfig/stubs/pygame/geometry.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ from typing import (
44
Callable,
55
Protocol,
66
Tuple,
7+
Sequence,
78
List,
89
)
910

@@ -95,6 +96,8 @@ class Circle:
9596
@overload
9697
def colliderect(self, topleft: Coordinate, size: Coordinate, /) -> bool: ...
9798
def collideswith(self, other: _CanBeCollided, /) -> bool: ...
99+
def collidelist(self, colliders: Sequence[_CanBeCollided], /) -> int: ...
100+
def collidelistall(self, colliders: Sequence[_CanBeCollided], /) -> List[int]: ...
98101
def intersect(self, other: _CanBeIntersected, /) -> List[Tuple[float, float]]: ...
99102
def contains(self, shape: _CanBeCollided) -> bool: ...
100103
@overload

docs/reST/ref/geometry.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,48 @@
217217

218218
.. ## Circle.collideswith ##
219219
220+
.. method:: collidelist
221+
222+
| :sl:`test if a list of objects collide with the circle`
223+
| :sg:`collidelist(colliders) -> int`
224+
225+
The `collidelist` method tests whether a given list of shapes or points collides
226+
(overlaps) with this `Circle` object. The function takes in a single argument, which
227+
must be a list of `Circle`, `Rect`, `FRect`, or a point. The function returns the index
228+
of the first shape or point in the list that collides with the `Circle` object, or
229+
-1 if there is no collision.
230+
231+
.. note::
232+
The shapes must be actual shape objects, such as `Circle`, `Rect` or `FRect`
233+
instances. It is not possible to pass a tuple or list of coordinates representing
234+
the shape as an argument (except for a point), because the shape type can't be
235+
determined from the coordinates alone.
236+
237+
.. versionadded:: 2.5.2
238+
239+
.. ## Circle.collidelist ##
240+
241+
.. method:: collidelistall
242+
243+
| :sl:`test if all objects in a list collide with the circle`
244+
| :sg:`collidelistall(colliders) -> list`
245+
246+
The `collidelistall` method tests whether a given list of shapes or points collides
247+
(overlaps) with this `Circle` object. The function takes in a single argument, which
248+
must be a list of `Circle`, `Rect`, `FRect`, or a point. The function returns a list
249+
containing the indices of all the shapes or points in the list that collide with
250+
the `Circle` object, or an empty list if there is no collision.
251+
252+
.. note::
253+
The shapes must be actual shape objects, such as `Circle`, `Rect` or `FRect`
254+
instances. It is not possible to pass a tuple or list of coordinates representing
255+
the shape as an argument (except for a point), because the shape type can't be
256+
determined from the coordinates alone.
257+
258+
.. versionadded:: 2.5.2
259+
260+
.. ## Circle.collidelistall ##
261+
220262
.. method:: contains
221263

222264
| :sl:`tests if a shape or point is inside the circle`

src_c/circle.c

Lines changed: 157 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,10 @@ pg_circle_rotate_ip(pgCircleObject *self, PyObject *const *args,
312312
Py_RETURN_NONE;
313313
}
314314

315-
static PyObject *
316-
pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
315+
static PG_FORCEINLINE int
316+
_pg_circle_collideswith(pgCircleBase *scirc, PyObject *arg)
317317
{
318318
int result = 0;
319-
pgCircleBase *scirc = &self->circle;
320319
if (pgCircle_Check(arg)) {
321320
result = pgCollision_CircleCircle(&pgCircle_AsCircle(arg), scirc);
322321
}
@@ -335,21 +334,168 @@ pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
335334
else if (PySequence_Check(arg)) {
336335
double x, y;
337336
if (!pg_TwoDoublesFromObj(arg, &x, &y)) {
338-
return RAISE(
337+
PyErr_SetString(
339338
PyExc_TypeError,
340339
"Invalid point argument, must be a sequence of two numbers");
340+
return -1;
341341
}
342342
result = pgCollision_CirclePoint(scirc, x, y);
343343
}
344344
else {
345-
return RAISE(PyExc_TypeError,
346-
"Invalid shape argument, must be a Circle, Rect / FRect, "
347-
"Line, Polygon or a sequence of two numbers");
345+
PyErr_SetString(
346+
PyExc_TypeError,
347+
"Invalid point argument, must be a sequence of 2 numbers");
348+
return -1;
349+
}
350+
351+
return result;
352+
}
353+
354+
static PyObject *
355+
pg_circle_collideswith(pgCircleObject *self, PyObject *arg)
356+
{
357+
int result = _pg_circle_collideswith(&self->circle, arg);
358+
if (result == -1) {
359+
return NULL;
348360
}
349361

350362
return PyBool_FromLong(result);
351363
}
352364

365+
static PyObject *
366+
pg_circle_collidelist(pgCircleObject *self, PyObject *arg)
367+
{
368+
Py_ssize_t i;
369+
pgCircleBase *scirc = &self->circle;
370+
int colliding;
371+
372+
if (!PySequence_Check(arg)) {
373+
return RAISE(PyExc_TypeError, "colliders argument must be a sequence");
374+
}
375+
376+
/* fast path */
377+
if (pgSequenceFast_Check(arg)) {
378+
PyObject **items = PySequence_Fast_ITEMS(arg);
379+
for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) {
380+
if ((colliding = _pg_circle_collideswith(scirc, items[i])) == -1) {
381+
/*invalid shape*/
382+
return NULL;
383+
}
384+
if (colliding) {
385+
return PyLong_FromSsize_t(i);
386+
}
387+
}
388+
return PyLong_FromLong(-1);
389+
}
390+
391+
/* general sequence path */
392+
for (i = 0; i < PySequence_Length(arg); i++) {
393+
PyObject *obj = PySequence_ITEM(arg, i);
394+
if (!obj) {
395+
return NULL;
396+
}
397+
398+
if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) {
399+
/*invalid shape*/
400+
Py_DECREF(obj);
401+
return NULL;
402+
}
403+
Py_DECREF(obj);
404+
405+
if (colliding) {
406+
return PyLong_FromSsize_t(i);
407+
}
408+
}
409+
410+
return PyLong_FromLong(-1);
411+
}
412+
413+
static PyObject *
414+
pg_circle_collidelistall(pgCircleObject *self, PyObject *arg)
415+
{
416+
PyObject *ret;
417+
Py_ssize_t i;
418+
pgCircleBase *scirc = &self->circle;
419+
int colliding;
420+
421+
if (!PySequence_Check(arg)) {
422+
return RAISE(PyExc_TypeError, "Argument must be a sequence");
423+
}
424+
425+
ret = PyList_New(0);
426+
if (!ret) {
427+
return NULL;
428+
}
429+
430+
/* fast path */
431+
if (pgSequenceFast_Check(arg)) {
432+
PyObject **items = PySequence_Fast_ITEMS(arg);
433+
434+
for (i = 0; i < PySequence_Fast_GET_SIZE(arg); i++) {
435+
if ((colliding = _pg_circle_collideswith(scirc, items[i])) == -1) {
436+
/*invalid shape*/
437+
Py_DECREF(ret);
438+
return NULL;
439+
}
440+
441+
if (!colliding) {
442+
continue;
443+
}
444+
445+
PyObject *num = PyLong_FromSsize_t(i);
446+
if (!num) {
447+
Py_DECREF(ret);
448+
return NULL;
449+
}
450+
451+
if (PyList_Append(ret, num)) {
452+
Py_DECREF(num);
453+
Py_DECREF(ret);
454+
return NULL;
455+
}
456+
Py_DECREF(num);
457+
}
458+
459+
return ret;
460+
}
461+
462+
/* general sequence path */
463+
for (i = 0; i < PySequence_Length(arg); i++) {
464+
PyObject *obj = PySequence_ITEM(arg, i);
465+
if (!obj) {
466+
Py_DECREF(ret);
467+
return NULL;
468+
}
469+
470+
if ((colliding = _pg_circle_collideswith(scirc, obj)) == -1) {
471+
/*invalid shape*/
472+
Py_DECREF(ret);
473+
Py_DECREF(obj);
474+
return NULL;
475+
}
476+
Py_DECREF(obj);
477+
478+
if (!colliding) {
479+
continue;
480+
}
481+
482+
PyObject *num = PyLong_FromSsize_t(i);
483+
if (!num) {
484+
Py_DECREF(ret);
485+
return NULL;
486+
}
487+
488+
if (PyList_Append(ret, num)) {
489+
Py_DECREF(num);
490+
Py_DECREF(ret);
491+
return NULL;
492+
}
493+
Py_DECREF(num);
494+
}
495+
496+
return ret;
497+
}
498+
353499
static PyObject *
354500
pg_circle_as_rect(pgCircleObject *self, PyObject *_null)
355501
{
@@ -461,6 +607,10 @@ static struct PyMethodDef pg_circle_methods[] = {
461607
DOC_CIRCLE_UPDATE},
462608
{"collideswith", (PyCFunction)pg_circle_collideswith, METH_O,
463609
DOC_CIRCLE_COLLIDESWITH},
610+
{"collidelist", (PyCFunction)pg_circle_collidelist, METH_O,
611+
DOC_CIRCLE_COLLIDELIST},
612+
{"collidelistall", (PyCFunction)pg_circle_collidelistall, METH_O,
613+
DOC_CIRCLE_COLLIDELISTALL},
464614
{"as_rect", (PyCFunction)pg_circle_as_rect, METH_NOARGS,
465615
DOC_CIRCLE_ASRECT},
466616
{"as_frect", (PyCFunction)pg_circle_as_frect, METH_NOARGS,

src_c/doc/geometry_doc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#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"
1414
#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"
1515
#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"
16+
#define DOC_CIRCLE_COLLIDELIST "collidelist(colliders) -> int\ntest if a list of objects collide with the circle"
17+
#define DOC_CIRCLE_COLLIDELISTALL "collidelistall(colliders) -> list\ntest if all objects in a list collide with the circle"
1618
#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"
1719
#define DOC_CIRCLE_MOVE "move((x, y), /) -> Circle\nmove(x, y, /) -> Circle\nmove(vector2, /) -> Circle\nmoves the circle by a given amount"
1820
#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"

test/geometry_test.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,102 @@ def test_collideswith(self):
682682
self.assertTrue(c.collideswith(p))
683683
self.assertFalse(c.collideswith(p2))
684684

685+
def test_collidelist_argtype(self):
686+
"""Tests if the function correctly handles incorrect types as parameters"""
687+
688+
invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)
689+
690+
c = Circle(10, 10, 4)
691+
692+
for value in invalid_types:
693+
with self.assertRaises(TypeError):
694+
c.collidelist(value)
695+
696+
def test_collidelist_argnum(self):
697+
"""Tests if the function correctly handles incorrect number of parameters"""
698+
c = Circle(10, 10, 4)
699+
700+
circles = [(Circle(10, 10, 4), Circle(10, 10, 4))]
701+
702+
with self.assertRaises(TypeError):
703+
c.collidelist()
704+
705+
with self.assertRaises(TypeError):
706+
c.collidelist(circles, 1)
707+
708+
def test_collidelist_return_type(self):
709+
"""Tests if the function returns the correct type"""
710+
c = Circle(10, 10, 4)
711+
712+
objects = [
713+
Circle(10, 10, 4),
714+
Rect(10, 10, 4, 4),
715+
]
716+
717+
for object in objects:
718+
self.assertIsInstance(c.collidelist([object]), int)
719+
720+
def test_collidelist(self):
721+
"""Ensures that the collidelist method works correctly"""
722+
c = Circle(10, 10, 4)
723+
724+
circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)]
725+
rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)]
726+
points = [(-10, -10), Vector2(1, 1), Vector2(10, -20), (10, 10)]
727+
expected = [1, 2, 3]
728+
729+
for objects, expected in zip([circles, rects, points], expected):
730+
self.assertEqual(c.collidelist(objects), expected)
731+
732+
def test_collidelistall_argtype(self):
733+
"""Tests if the function correctly handles incorrect types as parameters"""
734+
735+
invalid_types = (None, "1", (1,), 1, (1, 2, 3), True, False)
736+
737+
c = Circle(10, 10, 4)
738+
739+
for value in invalid_types:
740+
with self.assertRaises(TypeError):
741+
c.collidelistall(value)
742+
743+
def test_collidelistall_argnum(self):
744+
"""Tests if the function correctly handles incorrect number of parameters"""
745+
c = Circle(10, 10, 4)
746+
747+
circles = [(Circle(10, 10, 4), Circle(10, 10, 4))]
748+
749+
with self.assertRaises(TypeError):
750+
c.collidelistall()
751+
752+
with self.assertRaises(TypeError):
753+
c.collidelistall(circles, 1)
754+
755+
def test_collidelistall_return_type(self):
756+
"""Tests if the function returns the correct type"""
757+
c = Circle(10, 10, 4)
758+
759+
objects = [
760+
Circle(10, 10, 4),
761+
Rect(10, 10, 4, 4),
762+
(10, 10),
763+
Vector2(9, 9),
764+
]
765+
766+
for object in objects:
767+
self.assertIsInstance(c.collidelistall([object]), list)
768+
769+
def test_collidelistall(self):
770+
"""Ensures that the collidelistall method works correctly"""
771+
c = Circle(10, 10, 4)
772+
773+
circles = [Circle(1000, 1000, 2), Circle(5, 10, 5), Circle(16, 10, 7)]
774+
rects = [Rect(1000, 1000, 4, 4), Rect(1000, 200, 5, 5), Rect(5, 10, 7, 3)]
775+
points = [Vector2(-10, -10), (8, 8), (10, -20), Vector2(10, 10)]
776+
expected = [[1, 2], [2], [1, 3]]
777+
778+
for objects, expected in zip([circles, rects, points], expected):
779+
self.assertEqual(c.collidelistall(objects), expected)
780+
685781
def test_update(self):
686782
"""Ensures that updating the circle position
687783
and dimension correctly updates position and dimension"""

0 commit comments

Comments
 (0)