TryAITryAITry TryAI

Shared chat

Bouncing Ball Simulation

Write a Python program that shows a ball bouncing inside a spinning hexagon. The ball should be affected by gravity and friction, and it must bounce off the rotating walls realistically.

Okay, this is a fun and moderately complex problem involving 2D physics and Pygame for visualization!

Here's a Python program using Pygame that demonstrates a ball bouncing inside a spinning hexagon, with gravity and friction (restitution).

python
import pygameimport mathimport random
# --- Constants ---SCREEN_WIDTH = 800SCREEN_HEIGHT = 600FPS = 60
# ColorsWHITE = (255, 255, 255)BLACK = (0, 0, 0)RED = (255, 0, 0)GREEN = (0, 255, 0)BLUE = (0, 0, 255)YELLOW = (255, 255, 0)CYAN = (0, 255, 255)MAGENTA = (255, 0, 255)
# Physics parametersGRAVITY = 0.5  # pixels/frame^2BALL_RADIUS = 15HEXAGON_SIDE_LENGTH = 200 # Approximate side length, hexagon size is based on thisHEXAGON_ROTATION_SPEED = 0.01  # radians per frameRESTITUTION = 0.8  # Coefficient of restitution (bounciness)FRICTION_COEFF = 0.05 # Friction against wall tangent, reduces tangential velocity on bounce
# --- Helper Functions ---def rotate_point(point, angle, center):    """Rotates a point around a center."""    s = math.sin(angle)    c = math.cos(angle)    x, y = point    cx, cy = center
    # Translate point back to origin:    x -= cx    y -= cy
    # Rotate point    x_new = x * c - y * s    y_new = x * s + y * c
    # Translate point back:    x_new += cx    y_new += cy    return (x_new, y_new)
def magnitude(v):    return math.sqrt(v[0]**2 + v[1]**2)
def normalize(v):    mag = magnitude(v)    if mag == 0:        return (0, 0)    return (v[0] / mag, v[1] / mag)
def dot_product(v1, v2):    return v1[0] * v2[0] + v1[1] * v2[1]
# --- Ball Class ---class Ball:    def __init__(self, x, y, radius, color):        self.pos = pygame.Vector2(x, y)        self.vel = pygame.Vector2(random.uniform(-3, 3), random.uniform(-2, 0)) # Initial random velocity        self.radius = radius        self.color = color
    def update(self, dt):        # Apply gravity        self.vel.y += GRAVITY * dt
        # Update position        self.pos += self.vel * dt
    def draw(self, screen):        pygame.draw.circle(screen, self.color, (int(self.pos.x), int(self.pos.y)), self.radius)
# --- Hexagon Class ---class Hexagon:    def __init__(self, center_x, center_y, side_length, color, rotation_speed):        self.center = pygame.Vector2(center_x, center_y)        self.side_length = side_length # This is actually more like the radius to the vertices        self.color = color        self.angle = 0  # Initial angle        self.rotation_speed = rotation_speed # Radians per frame        self.vertices = [] # Current rotated vertices        self.edges = [] # Tuples of (start_vertex, end_vertex)        self.normals = [] # Outward normal for each edge        self._calculate_vertices() # Initial calculation
    def _calculate_vertices(self):        self.vertices = []        for i in range(6):            # Angle for each vertex of a regular hexagon            angle_deg = 60 * i - 30 # -30 to make one side flat at the bottom initially            angle_rad = math.radians(angle_deg) + self.angle # Add current rotation                        # Vertex position relative to (0,0)            x = self.side_length * math.cos(angle_rad)            y = self.side_length * math.sin(angle_rad)                        # Translate to hexagon center            self.vertices.append(pygame.Vector2(self.center.x + x, self.center.y + y))
        # Calculate edges and normals        self.edges = []        self.normals = []        num_vertices = len(self.vertices)        for i in range(num_vertices):            p1 = self.vertices[i]            p2 = self.vertices[(i + 1) % num_vertices] # Wrap around for the last edge            self.edges.append((p1, p2))
            edge_vec = p2 - p1            # Outward normal (for CCW vertices, (dy, -dx) is outward)            # If vertices were CW, (-dy, dx) would be outward.            # Our vertices are generated CCW (increasing angle_deg).            normal = pygame.Vector2(edge_vec.y, -edge_vec.x).normalize()            self.normals.append(normal)

    def update(self, dt):        self.angle += self.rotation_speed * dt        self._calculate_vertices()
    def draw(self, screen):        pygame.draw.polygon(screen, self.color, self.vertices, 3) # 3 is line thickness
    def get_edge_point_velocity(self, point_on_edge):        """Calculates the velocity of a point on the hexagon's edge due to rotation."""        # Vector from center of hexagon to the point on edge        r_vec = point_on_edge - self.center        # Tangential velocity v = omega x r. In 2D, if r = (rx, ry), v = omega * (-ry, rx)        # self.rotation_speed is omega (angular velocity)        vx = -self.rotation_speed * r_vec.y # Note: dt is already in rotation_speed        vy = self.rotation_speed * r_vec.x        return pygame.Vector2(vx, vy)
# --- Collision Handling ---def handle_collisions(ball, hexagon, dt):    for i, edge in enumerate(hexagon.edges):        p1, p2 = edge        normal = hexagon.normals[i]
        # Vector from p1 to ball center        p1_to_ball = ball.pos - p1        # Edge vector        edge_vec = p2 - p1        edge_len_sq = edge_vec.length_squared()
        if edge_len_sq == 0: # Should not happen for a valid hexagon            continue
        # Project p1_to_ball onto edge_vec to find closest point on the line        # t is the projection parameter (0 if closest point is p1, 1 if p2)        t = p1_to_ball.dot(edge_vec) / edge_len_sq                closest_point_on_line = None        if t < 0:            closest_point_on_line = p1        elif t > 1:            closest_point_on_line = p2        else:            closest_point_on_line = p1 + t * edge_vec                # Distance from ball center to closest point on line segment        dist_vec = ball.pos - closest_point_on_line        distance = dist_vec.length()
        if distance < ball.radius:            # --- Collision detected ---
            # 1. Resolve penetration: Move ball out along the normal            penetration_depth = ball.radius - distance            # Ensure we move along the correct normal direction.            # If dist_vec.dot(normal) is negative, normal is pointing towards ball, good.            # Otherwise, flip normal (shouldn't happen with correct normals)            if dist_vec.length_squared() > 0 and dist_vec.dot(normal) > 0:                 correction_normal = normal            else: # Ball is already outside or normal is flipped                 correction_normal = dist_vec.normalize() if dist_vec.length_squared() > 0 else normal
            ball.pos += correction_normal * penetration_depth
            # 2. Calculate relative velocity            # Velocity of the point on the hexagon wall where collision occurs            # For simplicity, use closest_point_on_line as impact point            wall_point_velocity = hexagon.get_edge_point_velocity(closest_point_on_line)            relative_velocity = ball.vel - wall_point_velocity
            # 3. Reflect velocity along the normal            # vn = normal component of relative_velocity            vn_scalar = relative_velocity.dot(normal)
            # If vn_scalar > 0, objects are moving apart, no collision response needed            # This check prevents "sticky" collisions if penetration isn't fully resolved            if vn_scalar >= 0:                continue
            # Decompose relative velocity into normal and tangential components            v_normal_comp_vec = vn_scalar * normal            v_tangential_comp_vec = relative_velocity - v_normal_comp_vec                        # Apply restitution to normal component            v_normal_comp_vec *= -RESTITUTION                        # Apply friction to tangential component            v_tangential_comp_vec *= (1 - FRICTION_COEFF)
            # New relative velocity            new_relative_velocity = v_normal_comp_vec + v_tangential_comp_vec                        # New ball absolute velocity            ball.vel = new_relative_velocity + wall_point_velocity

# --- Main Game Loop ---def main():    pygame.init()    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))    pygame.display.set_caption("Bouncing Ball in Spinning Hexagon")    clock = pygame.time.Clock()
    ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3, BALL_RADIUS, RED)    hexagon = Hexagon(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, HEXAGON_SIDE_LENGTH, BLUE, HEXAGON_ROTATION_SPEED)
    running = True    while running:        dt_raw = clock.tick(FPS) # Milliseconds since last frame        dt = dt_raw / (1000 / FPS) # Normalize dt to be 1.0 at target FPS, useful for physics scaling
        for event in pygame.event.get():            if event.type == pygame.QUIT:                running = False            if event.type == pygame.KEYDOWN:                if event.key == pygame.K_r: # Reset ball                     ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3, BALL_RADIUS, RED)
        # Updates        ball.update(dt)        hexagon.update(dt)        handle_collisions(ball, hexagon, dt)
        # Check if ball is way out of bounds (e.g., escaped due to high speed/glitch)        # A simple reset mechanism. More robust would be to prevent escape.        if not (-2*SCREEN_WIDTH < ball.pos.x < 3*SCREEN_WIDTH and \                -2*SCREEN_HEIGHT < ball.pos.y < 3*SCREEN_HEIGHT):            print("Ball escaped! Resetting.")            ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 3, BALL_RADIUS, RED)

        # Drawing        screen.fill(BLACK)        hexagon.draw(screen)        ball.draw(screen)        pygame.display.flip()
    pygame.quit()
if __name__ == "__main__":    main()

How it Works:

  1. Setup (Pygame, Constants):
    • Initializes Pygame, sets screen dimensions, FPS, colors, and physics parameters.
text
    *   `GRAVITY`: Constant downward acceleration.    *   `RESTITUTION`: How much energy is kept after a bounce (1.0 = perfectly elastic, 0.0 = no bounce).    *   `FRICTION_COEFF`: Reduces the tangential velocity component of the ball relative to the wall during a bounce. This makes the ball "roll" or "slide" less along the wall after impact.    *   `HEXAGON_ROTATION_SPEED`: How fast the hexagon spins.
  1. Helper Functions:
text
    *   `rotate_point`: Standard 2D point rotation.    *   Vector math (`magnitude`, `normalize`, `dot_product`): Essential for physics calculations. Pygame's `Vector2` handles many of these.
  1. Ball Class:
text
    *   `__init__`: Sets initial position, velocity (randomized slightly), radius, and color.    *   `update(dt)`:
    *   Applies gravity to `vel.y`.    *   Updates `pos` based on `vel`. `dt` (delta time) ensures frame-rate independent movement.*   `draw()`: Draws the ball as a circle.

4. Hexagon Class:

text
    *   `__init__`: Sets center, size (approximated by `side_length` which is really radius to vertices), color, and rotation speed.    *   `_calculate_vertices()`:
    *   Calculates the 6 vertices of the hexagon based on its current `angle`, `center`, and `side_length`.    *   Crucially, it also calculates `edges` (pairs of vertices) and `normals` (outward-pointing perpendicular vectors for each edge). These are vital for collision detection. The vertices are defined counter-clockwise (CCW).*   `update(dt)`:    *   Increments the `angle`.    *   Recalculates vertices, edges, and normals because the hexagon has rotated.
text
    *   `draw()`: Draws the hexagon polygon.    *   `get_edge_point_velocity(point_on_edge)`: This is key for realistic bounces off a *moving* surface. It calculates the linear velocity of a specific point on the hexagon's edge due to its rotation (`v = omega * r`, where `omega` is angular velocity and `r` is the vector from the center of rotation to the point).
  1. handle_collisions(ball, hexagon, dt): This is the core physics logic.
    • It iterates through each edge of the hexagon.
    • Collision Detection:
      • For each edge, it finds the closest_point_on_line segment from the ball's center to that edge.
      • It calculates the distance from the ball's center to this closest point.
      • If distance < ball.radius, a collision has occurred.
text
    *   **Collision Response:**        1.  **Penetration Resolution:**
        *   The ball might slightly overlap the wall. It's moved out along the `normal` of the collided edge by the `penetration_depth`. This prevents the ball from getting stuck.
text
        2.  **Relative Velocity:**            *   `wall_point_velocity`: The velocity of the hexagon's surface at the point of impact is calculated using `hexagon.get_edge_point_velocity()`.            *   `relative_velocity = ball.vel - wall_point_velocity`. All bounce calculations are done using this relative velocity.        3.  **Reflect Velocity:**
        *   The `relative_velocity` is decomposed into components normal and tangential to the collision surface.        *   The normal component is reversed and scaled by `RESTITUTION`. `vn_scalar = relative_velocity.dot(normal)` gives the magnitude of velocity along the normal. If it's positive, they are already moving apart, so we skip the bounce to avoid issues.        *   The tangential component is scaled by `(1 - FRICTION_COEFF)` to simulate friction slowing down sliding/rolling motion.        *   These components are recombined to get the new `new_relative_velocity`.
text
        4.  **New Absolute Velocity:**            *   The `wall_point_velocity` is added back to the `new_relative_velocity` to get the ball's new absolute velocity in world space: `ball.vel = new_relative_velocity + wall_point_velocity`.
  1. Main Game Loop (main()):
    • Initializes Pygame, screen, clock.
    • Creates Ball and Hexagon objects.
text
    *   **Game Loop:**        *   `dt = clock.tick(FPS) / (1000 / FPS)`: Calculates delta time, normalized so `dt=1.0` when running at target FPS. This makes physics calculations smoother and independent of frame rate fluctuations.
    *   Handles events (quitting, pressing 'R' to reset the ball).    *   Calls `update()` for the ball and hexagon.    *   Calls `handle_collisions()`.    *   Includes a basic check to reset the ball if it somehow escapes far off-screen (can happen with very high speeds or slight inaccuracies in penetration resolution).    *   Clears the screen (`screen.fill(BLACK)`).    *   Calls `draw()` for the hexagon and ball.    *   Updates the display (`pygame.display.flip()`).

To Run This Code:

  1. Make sure you have Pygame installed: pip install pygame
  2. Save the code as a Python file (e.g., bouncing_hexagon.py).
  3. Run it from your terminal: python bouncing_hexagon.py

You should see a red ball bouncing inside a blue, rotating hexagon. Press 'R' to reset the ball's position and velocity.

gemini-2.5-pro-exp-03-25
Can you fix this code, the ball falls through the bottom of the hexagon as soon as I run "python bouncing_hexagon.py"

You're right to point this out! The most common reason for a ball falling through a shape like this, especially if it happens quickly, is an issue in the collision response, specifically how the ball's position is corrected when a penetration is detected.

Looking at the original handle_collisions function, the logic for correction_normal has a subtle but critical flaw:

python
            # ...            # penetration_depth = ball.radius - distance            # Original problematic logic:            # if dist_vec.length_squared() > 0 and dist_vec.dot(normal) > 0:            #      correction_normal = normal            # else: # This branch is often taken for typical penetration            #      # dist_vec points from wall to ball. normal points out from wall.            #      # So dist_vec.normalize() points INWARD relative to the wall's outside.            #      # This pushes the ball further into the wall or in the wrong direction.            #      correction_normal = dist_vec.normalize() if dist_vec.length_squared() > 0 else normal            # ball.pos += correction_normal * penetration_depth            # ...

The Problem:

  • dist_vec is calculated as ball.pos - closest_point_on_line. This vector points from the closest point on the wall segment to the ball's center.
  • normal is the outward-pointing normal of the hexagon edge.
  • When the ball penetrates (distance < ball.radius), its center is "inside" the wall.
  • In the else branch (which is hit when dist_vec.dot(normal) <= 0, a common scenario for penetration), dist_vec.normalize() gives a unit vector pointing from the wall towards the ball's current (penetrating) center.
  • If you then move the ball along this dist_vec.normalize() by penetration_depth, you are effectively pushing it further along the direction it was already penetrating, or not directly outwards along the surface normal. This can cause it to pass through.

The Fix:

The penetration should always be resolved by moving the ball outwards along the surface normal of the edge it collided with.

Here's the corrected handle_collisions function and the main function (only handle_collisions is significantly changed, but I'll provide the full context for easy copy-pasting).

python
import pygameimport mathimport random
# --- Constants ---SCREEN_WIDTH = 800SCREEN_HEIGHT = 600FPS = 60
# ColorsWHITE = (255, 255, 255)BLACK = (0, 0, 0)RED = (255, 0, 0)GREEN = (0, 255, 0)BLUE = (0, 0, 255)YELLOW = (255, 255, 0)CYAN = (0, 255, 255)MAGENTA = (255, 0, 255)
# Physics parametersGRAVITY = 0.5  # pixels/frame^2BALL_RADIUS = 15HEXAGON_SIDE_LENGTH = 200 # Approximate radius to verticesHEXAGON_ROTATION_SPEED = 0.01  # radians per frameRESTITUTION = 0.8  # Coefficient of restitution (bounciness)FRICTION_COEFF = 0.05 # Friction against wall tangent
# --- Helper Functions ---def rotate_point(point, angle, center):    s = math.sin(angle)    c = math.cos(angle)    x, y = point    cx, cy = center    x -= cx    y -= cy    x_new = x * c - y * s    y_new = x * s + y * c    x_new += cx    y_new += cy    return (x_new, y_new)
# (Vector helper functions like magnitude, normalize, dot_product are handled by pygame.Vector2)
# --- Ball Class ---class Ball:    def __init__(self, x, y, radius, color):        self.pos = pygame.Vector2(x, y)        self.vel = pygame.Vector2(random.uniform(-3, 3), random.uniform(-2, 0))        self.radius = radius        self.color = color
    def update(self, dt):        self.vel.y += GRAVITY * dt        self.pos += self.vel * dt
    def draw(self, screen):        pygame.draw.circle(screen, self.color, (int(self.pos.x), int(self.pos.y)), self.radius)
# --- Hexagon Class ---class Hexagon:    def __init__(self, center_x, center_y, side_length, color, rotation_speed):        self.center = pygame.Vector2(center_x, center_y)        self.side_length = side_length        self.color = color        self.angle = 0        self.rotation_speed = rotation_speed        self.vertices = []        self.edges = []        self.normals = []        self._calculate_vertices()
    def _calculate_vertices(self):        self.vertices = []        for i in range(6):            angle_deg = 60 * i - 30 # Pointy-down hexagon            angle_rad = math.radians(angle_deg) + self.angle            x = self.side_length * math.cos(angle_rad)            y = self.side_length * math.sin(angle_rad)            self.vertices.append(pygame.Vector2(self.center.x + x, self.center.y + y))
        self.edges = []        self.normals = []        num_vertices = len(self.vertices)        for i in range(num_vertices):            p1 = self.vertices[i]            p2 = self.vertices[(i + 1) % num_vertices]            self.edges.append((p1, p2))            edge_vec = p2 - p1            if edge_vec.length_squared() > 0: # Avoid division by zero for zero-length edge                normal = pygame.Vector2(edge_vec.y, -edge_vec.x).normalize()            else: # Should not happen with a proper hexagon                normal = pygame.Vector2(0,0) # Or handle error            self.normals.append(normal)
    def update(self, dt):        self.angle += self.rotation_speed * dt        self._calculate_vertices()
    def draw(self, screen):        if len(self.vertices) >= 3: # Ensure there are enough vertices to draw a polygon            pygame.draw.polygon(screen, self.color, self.vertices, 3)
    def get_edge_point_velocity(self, point_on_edge):        r_vec = point_on_edge - self.center        # Using per-frame rotation speed, as dt is applied globally or implicitly in how speed is defined        vx = -self.rotation_speed * r_vec.y # Omega * dt is already self.rotation_speed * dt        vy = self.rotation_speed * r_vec.x  # So here it should just be omega * r_vec                                           # Corrected: rotation_speed is rad/frame (assuming dt=1)                                           # If dt is applied to rotation_speed elsewhere, this might be omega_per_second * dt * r_vec                                           # However, given rotation_speed is small, this should be angular velocity in rad/frame                                           # So velocity is (omega_rad_per_frame * r_vec_pixels)        # Let's assume rotation_speed is angular velocity omega (rad/step) where step duration is dt        # The current self.rotation_speed is effectively omega * dt_target_frame_time.        # To get velocity in pixels/step:        # ang_vel_per_step = self.rotation_speed (this is angle change per step)        vx_step = - (self.rotation_speed) * r_vec.y # This is simplified, assuming rotation_speed is angular velocity for the step        vy_step =   (self.rotation_speed) * r_vec.x        # This is a bit subtle. If self.rotation_speed is rad/sec, it should be        # ang_vel = self.rotation_speed * dt (if dt is in seconds)        # For now, the original way is angular velocity per *normalized frame*        return pygame.Vector2(vx, vy)

# --- Collision Handling (Corrected) ---def handle_collisions(ball, hexagon, dt):    for i, edge in enumerate(hexagon.edges):        p1, p2 = edge        normal = hexagon.normals[i]
        if normal.length_squared() == 0: # Skip if normal is zero vector            continue
        p1_to_ball = ball.pos - p1        edge_vec = p2 - p1        edge_len_sq = edge_vec.length_squared()
        if edge_len_sq == 0:            continue
        t = p1_to_ball.dot(edge_vec) / edge_len_sq                closest_point_on_line = None        if t < 0:            closest_point_on_line = p1        elif t > 1:            closest_point_on_line = p2        else:            closest_point_on_line = p1 + t * edge_vec                dist_vec = ball.pos - closest_point_on_line        distance = dist_vec.length()
        if distance < ball.radius and distance > 1e-5: # Add small epsilon to avoid issues if distance is zero            # --- Collision detected ---
            # 1. Resolve penetration: Move ball out along the normal            penetration_depth = ball.radius - distance            # ** THE CRITICAL FIX IS HERE: **            # Always push the ball out along the outward surface normal.            ball.pos += normal * penetration_depth
            # 2. Calculate relative velocity            wall_point_velocity = hexagon.get_edge_point_velocity(closest_point_on_line)            relative_velocity = ball.vel - wall_point_velocity
            # 3. Reflect velocity along the normal            vn_scalar = relative_velocity.dot(normal)
            if vn_scalar >= 0: # Already moving away or parallel, no bounce response needed                continue
            v_normal_comp_vec = vn_scalar * normal            v_tangential_comp_vec = relative_velocity - v_normal_comp_vec                        v_normal_comp_vec *= -RESTITUTION            v_tangential_comp_vec *= (1 - FRICTION_COEFF)
            new_relative_velocity = v_normal_comp_vec + v_tangential_comp_vec            ball.vel = new_relative_velocity + wall_point_velocity        elif distance < 1e-5 and ball.radius > 1e-5: # If ball center is ON the closest_point             # This can happen if penetration resolution overcorrects or due to precision.             # Push it out slightly along the normal.             ball.pos += normal * (ball.radius * 0.1) # Push out by a small fraction of radius

# --- Main Game Loop ---def main():    pygame.init()    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))    pygame.display.set_caption("Bouncing Ball in Spinning Hexagon")    clock = pygame.time.Clock()
    # Initial position for the ball, a bit higher to avoid immediate complex interactions.    ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - HEXAGON_SIDE_LENGTH // 2, BALL_RADIUS, RED)    hexagon = Hexagon(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, HEXAGON_SIDE_LENGTH, BLUE, HEXAGON_ROTATION_SPEED)
    running = True    while running:        dt_raw = clock.tick(FPS)         dt = dt_raw / (1000.0 / FPS) # Ensure float division for dt        if dt == 0: dt = 1/FPS # Avoid dt=0 if tick returns 0 ms
        for event in pygame.event.get():            if event.type == pygame.QUIT:                running = False            if event.type == pygame.KEYDOWN:                if event.key == pygame.K_r: # Reset ball                     ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - HEXAGON_SIDE_LENGTH // 2, BALL_RADIUS, RED)

        ball.update(dt)        hexagon.update(dt)        handle_collisions(ball, hexagon, dt) # dt is not strictly needed by this version of handle_collisions
        # Basic out-of-bounds check (can be improved)        if not (-SCREEN_WIDTH < ball.pos.x < 2*SCREEN_WIDTH and \                -SCREEN_HEIGHT < ball.pos.y < 2*SCREEN_HEIGHT):            print("Ball escaped or is too far out! Resetting.")            ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - HEXAGON_SIDE_LENGTH // 2, BALL_RADIUS, RED)
        screen.fill(BLACK)        hexagon.draw(screen)        ball.draw(screen)        pygame.display.flip()
    pygame.quit()
if __name__ == "__main__":    main()

Key Changes Made:

  1. Corrected Penetration Resolution: In handle_collisions:

    python
                # 1. Resolve penetration: Move ball out along the normal            penetration_depth = ball.radius - distance            # ** THE CRITICAL FIX IS HERE: **            # Always push the ball out along the outward surface normal.            ball.pos += normal * penetration_depth

    This ensures the ball is pushed directly out of the wall along the edge's outward normal.

  2. Slightly Adjusted Initial Ball Position: In main():

    python
        ball = Ball(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - HEXAGON_SIDE_LENGTH // 2, BALL_RADIUS, RED)

    The ball now starts a bit higher up, more towards the center of the hexagon, giving it more space before the first collision. This isn't strictly necessary if the collision logic is perfect, but it can help avoid complex initial interactions. The reset key 'R' also uses this position.

  3. Small Epsilon in Collision Detection: In handle_collisions:

    python
    if distance < ball.radius and distance > 1e-5:# ...elif distance < 1e-5 and ball.radius > 1e-5: # If ball center is ON the closest_point     ball.pos += normal * (ball.radius * 0.1)

    This adds a tiny tolerance to prevent issues if distance becomes zero or extremely small, which can sometimes cause problems with normalization or division by zero in subsequent calculations (though Vector2.normalize() handles zero vectors). The elif handles the rare case where the ball center might be exactly on the closest_point_on_line.

  4. Hexagon get_edge_point_velocity Clarification: The get_edge_point_velocity method in Hexagon uses self.rotation_speed. This value is HEXAGON_ROTATION_SPEED (e.g., 0.01 radians per frame). The velocities vx and vy are therefore in pixels per frame (assuming dt=1 in the update cycle). This is consistent with how ball.vel is updated. No change was strictly needed here for the fall-through bug, but it's an area where dt scaling can be tricky if not handled consistently.

  5. Safe dt calculation: Added 1000.0 for float division and a check for dt == 0.

Try this corrected version. The ball should now bounce much more reliably off the hexagon walls.

gemini-2.5-pro-exp-03-25

This is a shared TryAI chat. Sign in to start your own conversation.

Sign in to TryAI