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):

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:

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.