Quick answer: Pygame Rect uses half-open ranges — the right and bottom edges are outside the rect. For inclusive edge tests, inflate the rect by 1 pixel or write the comparison manually with <=.
A UI button at (100, 100) sized 80×30 is supposed to accept clicks anywhere inside its bounds. Players report clicks near the right edge fail. You log mouse coordinates — click at (180, 115) returns False from button.collidepoint((180, 115)). The point looks “inside” visually, but Pygame considers it outside.
Half-Open Interval Convention
Pygame Rect’s containment is defined as:
x ≤ p_x < x + width AND y ≤ p_y < y + height
For a rect (100, 100, 80, 30):
- Left edge (100): included.
- Right edge (100 + 80 = 180): excluded.
- Top edge (100): included.
- Bottom edge (100 + 30 = 130): excluded.
This is mathematically tidy — a 80-pixel-wide rect contains exactly 80 distinct x coordinates (100..179). But it surprises developers who expect (180, *) to count as “the right edge”.
Fix 1: Inflate by 1
def click_in_button(button_rect, pos):
return button_rect.inflate(1, 1).collidepoint(pos)
inflate(1, 1) grows the rect by 0.5 pixels on each side (Pygame inflates symmetrically). The new effective bounds include the original right and bottom edges. Visually: a clickable area exactly matching the drawn button.
Fix 2: Manual Comparison
For full control, write the test yourself:
def point_in_rect_inclusive(rect, p):
return rect.left <= p[0] <= rect.right and rect.top <= p[1] <= rect.bottom
Note <= on both bounds. Pygame Rect properties:
rect.right=rect.x + rect.width— the first x not in the rect (by half-open).rect.bottom= same for y.
So p[0] <= rect.right includes the exclusive edge by intent.
For Rect-Rect Collision
The same convention applies to colliderect. Two rects touching at an edge don’t collide:
a = pygame.Rect(0, 0, 10, 10)
b = pygame.Rect(10, 0, 10, 10) # a.right == b.left
a.colliderect(b) # False
For most game purposes — tilemaps with non-overlapping tiles, separated entities — this is correct. It only surprises you when you intended adjacency to count as overlap.
For Grid-Aligned Tilemaps
If tiles are 16 pixels and tile (0,0) occupies (0,0)–(16,16) and tile (1,0) occupies (16,0)–(32,16), the half-open convention means a body at exactly x=16 is in tile (1,0), not (0,0). This is the right answer — otherwise the body would belong to two tiles simultaneously. Don’t change this for tilemaps.
Verifying
Print collidepoint results around the rect’s edges:
r = pygame.Rect(10, 10, 20, 20)
for p in [(10,10), (29,29), (30,30), (30,29)]:
print(p, r.collidepoint(p))
Output: (10,10) True; (29,29) True; (30,30) False; (30,29) False. Confirms the right and bottom edges (x=30, y=30) are outside. With r.inflate(1,1).collidepoint(p), the (30, 29) and similar edge points are True.
“Half-open intervals are correct. Whether they’re what you want depends on the context. Buttons want inclusive; tiles want half-open.”
Wrap UI hit-tests in a single hit(rect, pos) helper that does the inflate, so the convention is centralized.