Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Circle.collidelist/collidelistall() #2880

Merged
merged 10 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions buildconfig/stubs/pygame/geometry.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from typing import (
Protocol,
Tuple,
Sequence,
List,
)

from pygame import Rect, FRect
Expand Down Expand Up @@ -94,6 +95,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 contains(self, shape: _CanBeCollided) -> bool: ...
@overload
def update(self, circle: _CircleValue, /) -> None: ...
Expand Down
42 changes: 42 additions & 0 deletions docs/reST/ref/geometry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,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.0

.. ## Circle.collidelist ##
damusss marked this conversation as resolved.
Show resolved Hide resolved

.. 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.0

.. ## Circle.collidelistall ##

.. method:: contains

| :sl:`check if a shape or point is inside the circle`
Expand Down
164 changes: 157 additions & 7 deletions src_c/circle.c
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,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);
}
Expand All @@ -334,21 +333,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);
damusss marked this conversation as resolved.
Show resolved Hide resolved
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)
{
Expand Down Expand Up @@ -438,6 +584,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,
Expand Down
2 changes: 2 additions & 0 deletions src_c/doc/geometry_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#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"
#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\nchecks if a rectangle intersects the circle"
#define DOC_CIRCLE_COLLIDESWITH "collideswith(circle, /) -> bool\ncollideswith(rect, /) -> bool\ncollideswith((x, y), /) -> bool\ncollideswith(vector2, /) -> bool\ncheck if a shape or point collides with the 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\ncheck if a shape or point is inside the circle"
#define DOC_CIRCLE_UPDATE "update((x, y), radius, /) -> None\nupdate(x, y, radius, /) -> None\nupdates the circle position and radius"
#define DOC_CIRCLE_ROTATE "rotate(angle, rotation_point=Circle.center, /) -> Circle\nrotate(angle, /) -> Circle\nrotates the circle"
Expand Down
96 changes: 96 additions & 0 deletions test/geometry_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,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"""
Expand Down
Loading