Skip to content

Commit e46e3c7

Browse files
committed
Add collision detection algorithms for basic shapes
Implements collision detection between: - Circles - Rectangles (using AABB) - Circle and Rectangle Includes: - Well-documented implementation - Comprehensive unit tests - Type hints and docstrings Fixes #1
1 parent f528ce3 commit e46e3c7

File tree

3 files changed

+228
-0
lines changed

3 files changed

+228
-0
lines changed

Diff for: geometry/__init__.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
"""Geometry module containing various geometric shapes and algorithms."""
3+
from geometry.geometry import Circle, Rectangle
4+
from geometry.collision_detection import (
5+
detect_circle_collision,
6+
detect_aabb_collision,
7+
detect_circle_rectangle_collision,
8+
)
9+
10+
__all__ = [
11+
'Circle',
12+
'Rectangle',
13+
'detect_circle_collision',
14+
'detect_aabb_collision',
15+
'detect_circle_rectangle_collision',
16+
]

Diff for: geometry/collision_detection.py

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"""
2+
Collision detection algorithms for basic geometric shapes.
3+
Supports collision detection between:
4+
- Circles
5+
- Rectangles (Axis-Aligned Bounding Boxes)
6+
- Circle and Rectangle
7+
8+
Example:
9+
>>> from geometry import Circle, Rectangle
10+
>>> circle1 = Circle(5) # circle with radius 5
11+
>>> circle2 = Circle(3) # circle with radius 3
12+
>>> detect_circle_collision(circle1, circle2, (0, 0), (7, 0))
13+
True # circles overlap at x=7 (distance less than sum of radii 5+3=8)
14+
"""
15+
from __future__ import annotations
16+
17+
import math
18+
from dataclasses import dataclass
19+
from typing import Tuple
20+
21+
from geometry import Circle, Rectangle
22+
23+
Point = Tuple[float, float]
24+
25+
26+
@dataclass
27+
class AABB:
28+
"""
29+
Axis-Aligned Bounding Box representation of a rectangle.
30+
Stores the minimum and maximum coordinates of the box.
31+
32+
>>> box = AABB.from_rectangle(Rectangle(2, 3), (0, 0))
33+
>>> box.min_x, box.min_y, box.max_x, box.max_y
34+
(-1.0, -1.5, 1.0, 1.5)
35+
"""
36+
37+
min_x: float
38+
min_y: float
39+
max_x: float
40+
max_y: float
41+
42+
@classmethod
43+
def from_rectangle(cls, rect: Rectangle, center: Point) -> AABB:
44+
"""
45+
Create an AABB from a Rectangle and its center point.
46+
47+
>>> box = AABB.from_rectangle(Rectangle(4, 6), (1, 2))
48+
>>> box.min_x, box.min_y, box.max_x, box.max_y
49+
(-1.0, -1.0, 3.0, 5.0)
50+
"""
51+
half_width = rect.short_side.length / 2
52+
half_height = rect.long_side.length / 2
53+
return cls(
54+
center[0] - half_width,
55+
center[1] - half_height,
56+
center[0] + half_width,
57+
center[1] + half_height,
58+
)
59+
60+
61+
def detect_circle_collision(circle1: Circle, circle2: Circle, pos1: Point, pos2: Point) -> bool:
62+
"""
63+
Detect collision between two circles at given positions.
64+
Returns True if circles overlap or touch, False otherwise.
65+
66+
>>> detect_circle_collision(Circle(5), Circle(3), (0, 0), (7, 0))
67+
True
68+
>>> detect_circle_collision(Circle(5), Circle(3), (0, 0), (9, 0))
69+
False
70+
>>> detect_circle_collision(Circle(5), Circle(3), (0, 0), (8, 0)) # touching
71+
True
72+
"""
73+
dx = pos2[0] - pos1[0]
74+
dy = pos2[1] - pos1[1]
75+
distance = math.sqrt(dx * dx + dy * dy)
76+
return distance <= (circle1.radius + circle2.radius) # Changed < to <=
77+
78+
79+
def detect_aabb_collision(rect1: Rectangle, rect2: Rectangle, pos1: Point, pos2: Point) -> bool:
80+
"""
81+
Detect collision between two rectangles using AABB method.
82+
Returns True if rectangles overlap, False otherwise.
83+
84+
>>> detect_aabb_collision(Rectangle(2, 3), Rectangle(2, 2), (0, 0), (1, 1))
85+
True
86+
>>> detect_aabb_collision(Rectangle(2, 3), Rectangle(2, 2), (0, 0), (3, 3))
87+
False
88+
"""
89+
box1 = AABB.from_rectangle(rect1, pos1)
90+
box2 = AABB.from_rectangle(rect2, pos2)
91+
92+
return (
93+
box1.min_x <= box2.max_x
94+
and box1.max_x >= box2.min_x
95+
and box1.min_y <= box2.max_y
96+
and box1.max_y >= box2.min_y
97+
)
98+
99+
100+
def detect_circle_rectangle_collision(
101+
circle: Circle, rect: Rectangle, circle_pos: Point, rect_pos: Point
102+
) -> bool:
103+
"""
104+
Detect collision between a circle and a rectangle.
105+
Returns True if shapes overlap, False otherwise.
106+
107+
>>> detect_circle_rectangle_collision(Circle(2), Rectangle(4, 4), (0, 0), (3, 0))
108+
True
109+
>>> detect_circle_rectangle_collision(Circle(2), Rectangle(4, 4), (0, 0), (5, 0))
110+
False
111+
"""
112+
box = AABB.from_rectangle(rect, rect_pos)
113+
114+
# Find the closest point on the rectangle to the circle's center
115+
closest_x = max(box.min_x, min(circle_pos[0], box.max_x))
116+
closest_y = max(box.min_y, min(circle_pos[1], box.max_y))
117+
118+
# Calculate distance between the closest point and circle center
119+
dx = circle_pos[0] - closest_x
120+
dy = circle_pos[1] - closest_y
121+
distance = math.sqrt(dx * dx + dy * dy)
122+
123+
return distance < circle.radius
124+
125+
126+
if __name__ == "__main__":
127+
import doctest
128+
129+
doctest.testmod()

Diff for: geometry/test_collision_detection.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Unit tests for collision detection algorithms."""
2+
import unittest
3+
4+
from geometry import Circle, Rectangle
5+
from geometry.collision_detection import (
6+
AABB,
7+
detect_aabb_collision,
8+
detect_circle_collision,
9+
detect_circle_rectangle_collision,
10+
)
11+
12+
13+
class TestCollisionDetection(unittest.TestCase):
14+
def test_aabb_from_rectangle(self):
15+
"""Test AABB creation from Rectangle."""
16+
rect = Rectangle(4, 6) # width=4, height=6
17+
box = AABB.from_rectangle(rect, (1, 2)) # centered at (1,2)
18+
19+
self.assertEqual(box.min_x, -1) # 1 - 4/2
20+
self.assertEqual(box.max_x, 3) # 1 + 4/2
21+
self.assertEqual(box.min_y, -1) # 2 - 6/2
22+
self.assertEqual(box.max_y, 5) # 2 + 6/2
23+
24+
def test_circle_collision(self):
25+
"""Test circle-circle collision detection."""
26+
circle1 = Circle(5)
27+
circle2 = Circle(3)
28+
29+
# Overlapping circles
30+
self.assertTrue(detect_circle_collision(circle1, circle2, (0, 0), (7, 0)))
31+
self.assertTrue(detect_circle_collision(circle1, circle2, (0, 0), (0, 7)))
32+
33+
# Non-overlapping circles
34+
self.assertFalse(detect_circle_collision(circle1, circle2, (0, 0), (9, 0)))
35+
self.assertFalse(detect_circle_collision(circle1, circle2, (0, 0), (0, 9)))
36+
37+
# Touching circles
38+
self.assertTrue(detect_circle_collision(circle1, circle2, (0, 0), (8, 0)))
39+
40+
# Diagonal positions
41+
self.assertTrue(detect_circle_collision(circle1, circle2, (0, 0), (5, 5)))
42+
self.assertFalse(detect_circle_collision(circle1, circle2, (0, 0), (7, 7)))
43+
44+
def test_rectangle_collision(self):
45+
"""Test rectangle-rectangle collision detection using AABB."""
46+
rect1 = Rectangle(4, 6) # 4x6 rectangle
47+
rect2 = Rectangle(2, 2) # 2x2 rectangle
48+
49+
# Overlapping rectangles
50+
self.assertTrue(detect_aabb_collision(rect1, rect2, (0, 0), (1, 1)))
51+
self.assertTrue(detect_aabb_collision(rect1, rect2, (0, 0), (-1, -1)))
52+
53+
# Non-overlapping rectangles
54+
self.assertFalse(detect_aabb_collision(rect1, rect2, (0, 0), (5, 5)))
55+
self.assertFalse(detect_aabb_collision(rect1, rect2, (0, 0), (-5, -5)))
56+
57+
# Touching rectangles
58+
self.assertTrue(detect_aabb_collision(rect1, rect2, (0, 0), (3, 0)))
59+
self.assertTrue(detect_aabb_collision(rect1, rect2, (0, 0), (0, 4)))
60+
61+
def test_circle_rectangle_collision(self):
62+
"""Test circle-rectangle collision detection."""
63+
circle = Circle(2)
64+
rect = Rectangle(4, 4)
65+
66+
# Overlapping
67+
self.assertTrue(detect_circle_rectangle_collision(circle, rect, (0, 0), (3, 0)))
68+
self.assertTrue(detect_circle_rectangle_collision(circle, rect, (0, 0), (0, 3)))
69+
70+
# Non-overlapping
71+
self.assertFalse(detect_circle_rectangle_collision(circle, rect, (0, 0), (5, 0)))
72+
self.assertFalse(detect_circle_rectangle_collision(circle, rect, (0, 0), (0, 5)))
73+
74+
# Circle inside rectangle
75+
self.assertTrue(detect_circle_rectangle_collision(circle, rect, (0, 0), (0, 0)))
76+
77+
# Circle touching rectangle corner
78+
self.assertTrue(detect_circle_rectangle_collision(circle, rect, (0, 0), (3, 3)))
79+
self.assertFalse(detect_circle_rectangle_collision(circle, rect, (0, 0), (4, 4)))
80+
81+
82+
if __name__ == "__main__":
83+
unittest.main()

0 commit comments

Comments
 (0)