Movement supplies positions. A game rule decides what those positions mean. In this catch game, a ball either crosses the paddle’s catch plane within its horizontal catch interval or eventually falls completely below the board.
We keep before and moved worlds so the rule can detect a transition. “The ball is below the paddle” is a state and can remain true for many frames. “It crossed from above this frame” can resolve one outcome and spawn the next ball.
The catch plane is paddle_y − radius. For the default board that is 288 − 9 = 279. We accept a crossing when old_y < 279 <= new_y. Horizontal acceptance uses abs(ball_x − moved_paddle_x) <= half_width + radius.
This is a deliberately simplified game rule. It uses the paddle’s end-of-step position and an expanded horizontal interval. It is not exact circle-versus-rectangle geometry, continuous moving-body physics or a claim that every frame partition gives identical outcomes. Making the policy explicit is more useful than calling it a perfect collision solver.
plane, paddle_x, half, radius = 279, 240, 45, 9
for old_y, new_y, ball_x in [(278,280,240),(278,280,294),(280,281,240)]:
crossing = old_y < plane <= new_y
caught = crossing and abs(ball_x - paddle_x) <= half + radius
print(crossing, caught)A miss waits until the ball’s top is strictly below the bottom: moved_y − radius > height. At equality the ball is not yet a miss. A catch has priority if a directly supplied large movement satisfies both tests; ordinary bounded motion is much smaller.
On an outcome, increment turn exactly once and increment score for a catch or misses otherwise. Spawn the next ball from the new turn index. Keep the moved paddle. When misses reaches the configured lives, set mode to over; otherwise continue running.
Returning moved when neither event occurs preserves position without manufacturing points. The supplied validation rejects a motion pair that secretly changed counters, mode or ball x. Tests exercise exact boundaries and a just-outside horizontal position, because happy-path center catches alone do not establish the rule.
height, radius = 320, 9
for center_y in [328, 329, 330]:
top = center_y - radius
print("center", center_y, "top", top, "miss", top > height)Implement the transition, then test: exact plane equality, exact horizontal touch, just outside the interval, bottom equality, the final life and a ball already below the catch plane. Check both input worlds remain unchanged.
Your model can now score a full round. Next we decide how pause, restart and quit interact without letting a command frame accidentally move the ball.