Thirty-two bricks, one paddle, three lives. Fixed-timestep loop, analytic AABB resolution, side-aware reflections — rendered entirely to a 400×600 canvas at 60 FPS.
The ball is advanced on X, resolved, then advanced on Y and resolved. Because it moves only ~1.9 px per sub-step against a 3 px radius, tunnelling through a 20 px brick is impossible — even on a 120 Hz phone.
For each candidate brick we compute horizontal and vertical overlap. ox < oy means the ball entered from a left/right face → flip dx; otherwise it landed on a top/bottom face → flip dy. The ball is then pushed out by exactly that penetration depth, so it can never end a frame inside the brick.
Only the deepest overlapping brick is processed each step; every other brick touched in the same frame is deferred. Destroyed bricks carry a cooldown counter so a grazed corner can't register twice.
rel = (ball.x − paddleCentre) / 30 drives a ±58° launch vector, and a fraction of paddle velocity is added as sidespin. The resulting vector is renormalised to the constant speed, so |v| never drifts across a long rally.
After every reflection the velocity is clamped to a minimum vertical and horizontal component — no infinite horizontal ping-pong, no dead-vertical stall. Brick queries index a 2D grid by cell rather than iterating all 32 bricks.