Bouncing_circles code in Python

 

Bouncing_circles - brute force collision detection example in Python

Created by ChatGPT, runs without errors. I just tweaked a few parameters.






Same group → attraction (force pulls circles together)

Opposite group → repulsion (force pushes circles apart)

Moving the center of gravity, bouncing off the screen edges.

High central pull  → chaos and swirling behavior.

Elastic collisions between circles and walls are preserved.

Mass-based attraction within groups.

Flocking: cohesion, alignment, separation.

Colors are coherent within groups.

*****************************************************************

Python code - just copy-paste into a txt file bouncing_circles.py.

pip install pygame

Run with: python bouncing_circles.py

*****************************************************************

# --code begins here--


import pygame

import random

import math


# --- Configuration ---

WIDTH, HEIGHT = 900, 700

NUM_CIRCLES = 200

MIN_RADIUS = 3

MAX_RADIUS = 6

MAX_SPEED =3

GROUPS = 7

FORCE_SCALE = -200

ALIGNMENT_SCALE = 0.005

COHESION_SCALE = 0.003

CENTER_GRAVITY = 25  # you wanted this high — now used as a force magnitude, not multiplied by distance

TRAIL_FADE = 210


# --- Initialize ---

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT))

clock = pygame.time.Clock()

#trail_surface = pygame.Surface((WIDTH, HEIGHT))

trail_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)


# Base colors per group

BASE_COLORS = [

    (255, 80, 80),    # 

    (80, 255, 80),    # 

    (80, 80, 255),    # 

    (255, 255, 80),   # 

    (255, 80, 255),   # 

    (10, 80, 55),   # 

   (110, 10, 200),   # 


]


# --- Circle class ---

class Circle:

    def __init__(self, group):

        self.group = group

        self.radius = random.choices(

            population=[MIN_RADIUS, 8, 10, 12, 15, 20, MAX_RADIUS],

            weights=[50, 15, 10, 10, 7, 5, 3],

            k=1

        )[0]

        self.mass = max(1.0, self.radius ** 2)  # avoid zero mass

        self.x = random.uniform(self.radius, WIDTH - self.radius)

        self.y = random.uniform(self.radius, HEIGHT - self.radius)

        self.vx = random.uniform(-MAX_SPEED, MAX_SPEED)

        self.vy = random.uniform(-MAX_SPEED, MAX_SPEED)

        r, g, b = BASE_COLORS[group]

        self.color = (

            min(255, max(0, r + random.randint(-40, 40))),

            min(255, max(0, g + random.randint(-40, 40))),

            min(255, max(0, b + random.randint(-40, 40))),

        )


    def move(self):

        self.x += self.vx

        self.y += self.vy


        # Smooth speed limiting

        speed = math.hypot(self.vx, self.vy)

        if speed > MAX_SPEED:

            scale = MAX_SPEED / speed

            self.vx *= scale

            self.vy *= scale


        # Bounce off walls

        if self.x < self.radius or self.x > WIDTH - self.radius:

            self.vx *= -4

            self.x = max(self.radius, min(WIDTH - self.radius, self.x))

        if self.y < self.radius or self.y > HEIGHT - self.radius:

            self.vy *= -4

            self.y = max(self.radius, min(HEIGHT - self.radius, self.y))


    def draw(self, surf):

        glow_intensity = max(40, min(255, int(math.sqrt(self.mass) * 2)))

        color = (*self.color, glow_intensity)

        pygame.draw.circle(surf, color, (int(self.x), int(self.y)), self.radius)


# --- Moving center of gravity ---

class MovingCenter:

    def __init__(self, x, y):

        self.x = x

        self.y = y

        self.vx = random.choice([-5, 5])

        self.vy = random.choice([-5, 5])


    def move(self):

        self.x += self.vx

        self.y += self.vy

        if self.x < 0 or self.x > WIDTH:

            self.vx *= -1

            self.x = max(0, min(WIDTH, self.x))

        if self.y < 0 or self.y > HEIGHT:

            self.vy *= -1

            self.y = max(0, min(HEIGHT, self.y))


# --- Forces ---

def handle_forces(circles, center):

    # Compute group centers

    group_centers = [[0, 0, 0] for _ in range(GROUPS)]

    for c in circles:

        gx, gy, count = group_centers[c.group]

        group_centers[c.group] = [gx + c.x, gy + c.y, count + 1]

    group_positions = [(gx/count, gy/count) if count > 0 else (0, 0) for gx, gy, count in group_centers]


    # Pairwise forces + collisions

    for i in range(len(circles)):

        for j in range(i + 1, len(circles)):

            c1 = circles[i]

            c2 = circles[j]

            dx = c2.x - c1.x

            dy = c2.y - c1.y

            dist = math.hypot(dx, dy)

            if dist == 0:

                # jitter slightly to avoid exact overlap numeric issues

                dx = (random.random() - 0.5) * 0.01

                dy = (random.random() - 0.5) * 0.01

                dist = math.hypot(dx, dy)

            nx, ny = dx / dist, dy / dist


            if c1.group == c2.group:

                # gravitational-like attraction (force magnitude falls with dist^2)

                force = FORCE_SCALE * (c1.mass + c2.mass) / (dist**2 + 1e-6)

                fx = nx * force

                fy = ny * force

            else:

                # repulsion proportional to size

                repulsion = 0.02 * (c1.radius + c2.radius)

                fx = -nx * repulsion

                fy = -ny * repulsion


            # apply equal and opposite impulses (convert force -> acceleration)

            c1.vx += fx / c1.mass

            c1.vy += fy / c1.mass

            c2.vx -= fx / c2.mass

            c2.vy -= fy / c2.mass


            # Collision bounce (elastic along normal)

            min_dist = c1.radius + c2.radius

            if dist < min_dist:

                overlap = min_dist - dist

                c1.x -= nx * overlap / 2

                c1.y -= ny * overlap / 2

                c2.x += nx * overlap / 2

                c2.y += ny * overlap / 2

                # relative velocity along normal

                rel = (c1.vx * nx + c1.vy * ny) - (c2.vx * nx + c2.vy * ny)

                if rel < 0:  # only if closing

                    # compute impulse scalar for elastic collision (1D along normal)

                    # using masses: j = -(1+e)*rel / (1/m1 + 1/m2); e=1 elasticity

                    j = -(1+1.0) * rel / (1.0 / c1.mass + 1.0 / c2.mass)

                    c1.vx += (j * nx) / c1.mass

                    c1.vy += (j * ny) / c1.mass

                    c2.vx -= (j * nx) / c2.mass

                    c2.vy -= (j * ny) / c2.mass


    # Flocking + center pull

    for c in circles:

        gx, gy = group_positions[c.group]

        # cohesion (toward group center)

        c.vx += (gx - c.x) * COHESION_SCALE

        c.vy += (gy - c.y) * COHESION_SCALE


        # alignment (nearby same-group neighbors)

        neighbor_vx, neighbor_vy, count = 0, 0, 0

        for other in circles:

            if other.group == c.group and other is not c:

                dx = other.x - c.x

                dy = other.y - c.y

                dist = math.hypot(dx, dy)

                if dist < 60:

                    neighbor_vx += other.vx

                    neighbor_vy += other.vy

                    count += 1

        if count > 0:

            avg_vx = neighbor_vx / count

            avg_vy = neighbor_vy / count

            c.vx += (avg_vx - c.vx) * ALIGNMENT_SCALE

            c.vy += (avg_vy - c.vy) * ALIGNMENT_SCALE


        # --- FIXED central gravity ---

        # Use normalized direction to the moving center, then compute acceleration = force / mass

        dx = center.x - c.x

        dy = center.y - c.y

        dist = math.hypot(dx, dy)

        if dist > 0:

            dirx, diry = dx / dist, dy / dist

            # force magnitude is constant (CENTER_GRAVITY) but we can cap effect for stability

            # and convert force -> acceleration by dividing by mass

            force_mag = CENTER_GRAVITY

            # optional cap: if center is extremely close, avoid huge accelerations

            # accel = (dir * force_mag) / mass

            ax = dirx * force_mag / c.mass

            ay = diry * force_mag / c.mass

            c.vx += ax

            c.vy += ay


# --- Main loop ---

circles = []

for g in range(GROUPS):

    circles.extend([Circle(g) for _ in range(NUM_CIRCLES // GROUPS)])


center = MovingCenter(WIDTH / 2, HEIGHT / 2)


running = True

while running:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:

            running = False


    # Fade trails

    #screen.fill((50, 50, 0))

    trail_surface.fill((1, 8, 1, TRAIL_FADE))


    # Move center then compute forces

    center.move()

    handle_forces(circles, center)


    for c in circles:

        c.move()

        c.draw(trail_surface)


    # Render

    screen.blit(trail_surface, (0, 0))

    #pygame.draw.circle(screen, (255, 255, 255), (int(center.x), int(center.y)), 6)

    pygame.display.flip()

    clock.tick(60)


pygame.quit()


No comments:

Post a Comment