some new stuff.

idk its all pretty fun! some C++ too!
This commit is contained in:
2025-10-15 11:16:51 +02:00
parent 1b31319003
commit d0eaabdd87
23 changed files with 3448 additions and 0 deletions

View File

@@ -0,0 +1,731 @@
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import math
import random
import numpy as np
class Ball:
def __init__(self):
self.pos = [0, 0.5, 15]
self.vel = [0, 0, 0]
self.rot = [0, 0, 0]
self.rot_vel = [0, 0, 0]
self.radius = 0.5
self.mass = 7.0
self.launched = False
self.finger_holes = []
self.texture = None
self.load_texture()
def load_texture(self):
# Create a simple procedural texture for the ball
texture_size = 64
texture_data = np.zeros((texture_size, texture_size, 3), dtype=np.uint8)
for i in range(texture_size):
for j in range(texture_size):
# Create a marble-like pattern
x = i / texture_size * 2 * math.pi
y = j / texture_size * 2 * math.pi
# Base color (dark blue/black)
r = 20 + int(10 * math.sin(x * 5) * math.cos(y * 5))
g = 20 + int(10 * math.sin(x * 3) * math.cos(y * 7))
b = 60 + int(20 * math.sin(x * 7) * math.cos(y * 3))
texture_data[i, j] = [r, g, b]
self.texture = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, self.texture)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_size, texture_size, 0,
GL_RGB, GL_UNSIGNED_BYTE, texture_data)
def reset(self):
self.pos = [0, 0.5, 15]
self.vel = [0, 0, 0]
self.rot = [0, 0, 0]
self.rot_vel = [0, 0, 0]
self.launched = False
def launch(self, power, angle):
self.launched = True
self.vel[2] = -power * 20 # Increased power
self.vel[0] = math.sin(math.radians(angle)) * power * 8
self.rot_vel[0] = -power * 300 # More realistic spin
def update(self, dt, pins):
if not self.launched:
return False
# Gravity
self.vel[1] -= 9.8 * dt
# Floor collision with improved physics
if self.pos[1] - self.radius < 0:
self.pos[1] = self.radius
# Energy loss on bounce
self.vel[1] = -self.vel[1] * 0.6 # Bounce with energy loss
# Improved friction based on surface contact
friction = 0.95
self.vel[0] *= friction
self.vel[2] *= friction
# Add slight random variation for more realistic movement
self.vel[0] += random.uniform(-0.1, 0.1)
self.vel[2] += random.uniform(-0.1, 0.1)
# Air resistance
air_resistance = 0.99
self.vel[0] *= air_resistance
self.vel[2] *= air_resistance
# Update position
for i in range(3):
self.pos[i] += self.vel[i] * dt
self.rot[i] += self.rot_vel[i] * dt
# Check collisions with pins
for pin in pins:
if pin.standing:
dx = self.pos[0] - pin.pos[0]
dz = self.pos[2] - pin.pos[2]
dist = math.sqrt(dx*dx + dz*dz)
if dist < self.radius + pin.radius:
pin.knock_down(self.vel)
# More realistic bounce physics
normal = [dx/dist, 0, dz/dist]
dot = self.vel[0]*normal[0] + self.vel[2]*normal[2]
self.vel[0] -= 1.8 * dot * normal[0]
self.vel[2] -= 1.8 * dot * normal[2]
# Add some vertical bounce
self.vel[1] += 0.5
# Check if ball is out of bounds
if self.pos[2] < -5 or abs(self.pos[0]) > 10:
return True
# Check if ball has stopped
speed = math.sqrt(self.vel[0]**2 + self.vel[1]**2 + self.vel[2]**2)
if speed < 0.1 and self.pos[1] <= self.radius + 0.01:
return True
return False
def draw(self):
glPushMatrix()
glTranslatef(*self.pos)
glRotatef(self.rot[0], 1, 0, 0)
glRotatef(self.rot[1], 0, 1, 0)
# Enable texturing for the ball
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture)
# Ball body with texture
glColor3f(0.8, 0.8, 1.0) # Lighter base color
quad = gluNewQuadric()
gluQuadricTexture(quad, GL_TRUE)
gluSphere(quad, self.radius, 32, 32)
# Finger holes
glDisable(GL_TEXTURE_2D)
glColor3f(0.05, 0.05, 0.05)
for angle in [0, 120, 240]:
glPushMatrix()
glRotatef(angle, 0, 1, 0)
glTranslatef(0.3, 0, 0)
gluCylinder(quad, 0.06, 0.06, 0.2, 16, 1)
glPopMatrix()
glDisable(GL_TEXTURE_2D)
glPopMatrix()
class Pin:
def __init__(self, x, z):
self.pos = [x, 0.75, z]
self.vel = [0, 0, 0]
self.rot = [0, 0, 0]
self.rot_vel = [0, 0, 0]
self.radius = 0.15
self.height = 0.75
self.standing = True
self.fall_timer = 0
self.initial_pos = [x, 0.75, z] # Store initial position for reset
def knock_down(self, impact_vel):
if self.standing:
self.standing = False
# More realistic pin physics based on impact
self.vel = [
impact_vel[0] * random.uniform(0.3, 0.7),
random.uniform(1.5, 3.0),
impact_vel[2] * random.uniform(0.3, 0.7)
]
self.rot_vel = [
random.uniform(-400, 400),
random.uniform(-400, 400),
random.uniform(-400, 400)
]
def reset(self):
self.pos = self.initial_pos.copy()
self.vel = [0, 0, 0]
self.rot = [0, 0, 0]
self.rot_vel = [0, 0, 0]
self.standing = True
self.fall_timer = 0
def update(self, dt):
if not self.standing:
# Gravity
self.vel[1] -= 9.8 * dt
# Floor collision with improved physics
if self.pos[1] < 0.1:
self.pos[1] = 0.1
self.vel[1] = -self.vel[1] * 0.3 # Bounce with energy loss
self.vel[0] *= 0.7
self.vel[2] *= 0.7
self.rot_vel = [v * 0.7 for v in self.rot_vel]
# Update position
for i in range(3):
self.pos[i] += self.vel[i] * dt
self.rot[i] += self.rot_vel[i] * dt
self.fall_timer += dt
def draw(self):
glPushMatrix()
glTranslatef(*self.pos)
if not self.standing:
glRotatef(self.rot[0], 1, 0, 0)
glRotatef(self.rot[1], 0, 1, 0)
glRotatef(self.rot[2], 0, 0, 1)
# Pin body with improved materials
glColor3f(0.95, 0.95, 0.95) # Brighter white
quad = gluNewQuadric()
gluCylinder(quad, 0.15, 0.08, self.height, 16, 1)
# Red stripes with gloss effect
glColor3f(0.9, 0.1, 0.1)
glPushMatrix()
glTranslatef(0, 0, self.height * 0.6)
gluCylinder(quad, 0.11, 0.09, self.height * 0.1, 16, 1)
glPopMatrix()
# Second stripe
glPushMatrix()
glTranslatef(0, 0, self.height * 0.3)
gluCylinder(quad, 0.12, 0.11, self.height * 0.1, 16, 1)
glPopMatrix()
# Top with rounded cap
glPushMatrix()
glTranslatef(0, 0, self.height)
gluSphere(quad, 0.08, 16, 16)
glPopMatrix()
glPopMatrix()
class BowlingGame:
def __init__(self):
pygame.init()
self.width, self.height = 1200, 800
self.screen = pygame.display.set_mode((self.width, self.height), DOUBLEBUF | OPENGL)
pygame.display.set_caption("Enhanced 3D Bowling Game")
self.setup_gl()
self.ball = Ball()
self.pins = self.setup_pins()
self.power = 0
self.angle = 0
self.charging = False
self.max_power = 1.0
self.score = 0
self.frame = 1
self.roll = 1
self.game_over = False
self.camera_pos = [0, 5, 20]
self.camera_target = [0, 1, 0]
self.camera_angle = 0
self.camera_distance = 20
self.lane_texture = self.create_lane_texture()
self.background_color = [0.1, 0.2, 0.4] # Dark blue background
# Game state
self.waiting_for_roll = True
self.ball_done = False
self.knocked_this_roll = 0
# Lighting
self.light_pos = [5, 10, 10]
self.ambient_light = [0.4, 0.4, 0.4, 1.0]
self.diffuse_light = [0.9, 0.9, 0.9, 1.0]
self.specular_light = [1.0, 1.0, 1.0, 1.0]
def create_lane_texture(self):
texture_size = 256
texture_data = np.zeros((texture_size, texture_size, 3), dtype=np.uint8)
for i in range(texture_size):
for j in range(texture_size):
# Create wood grain pattern
wood_intensity = 150 + int(50 * math.sin(i/20) * math.cos(j/30))
texture_data[i, j] = [wood_intensity, wood_intensity-20, wood_intensity-40]
texture = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, texture)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_size, texture_size, 0,
GL_RGB, GL_UNSIGNED_BYTE, texture_data)
return texture
def setup_gl(self):
glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_LIGHT1) # Additional light
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
# Improved lighting
glLightfv(GL_LIGHT0, GL_POSITION, (5, 10, 10, 1))
glLightfv(GL_LIGHT0, GL_AMBIENT, (0.4, 0.4, 0.4, 1))
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.9, 0.9, 0.9, 1))
glLightfv(GL_LIGHT0, GL_SPECULAR, (1.0, 1.0, 1.0, 1))
# Second light for better illumination
glLightfv(GL_LIGHT1, GL_POSITION, (-5, 8, 5, 1))
glLightfv(GL_LIGHT1, GL_AMBIENT, (0.2, 0.2, 0.2, 1))
glLightfv(GL_LIGHT1, GL_DIFFUSE, (0.5, 0.5, 0.5, 1))
glLightfv(GL_LIGHT1, GL_SPECULAR, (0.5, 0.5, 0.5, 1))
# Material properties for better reflections
glMaterialfv(GL_FRONT, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])
glMaterialf(GL_FRONT, GL_SHININESS, 50.0)
glMatrixMode(GL_PROJECTION)
gluPerspective(45, self.width/self.height, 0.1, 100.0)
glMatrixMode(GL_MODELVIEW)
def setup_pins(self):
pins = []
start_z = -10
spacing = 0.5
# Standard 10-pin triangle formation
for row in range(4):
for col in range(row + 1):
x = (col - row/2) * spacing
z = start_z - row * spacing * 0.866
pins.append(Pin(x, z))
return pins
def reset_pins(self):
for pin in self.pins:
pin.reset()
def reset_ball(self):
self.ball.reset()
self.power = 0
self.angle = 0
self.charging = False
self.ball_done = False
self.waiting_for_roll = True
def count_knocked_pins(self):
return sum(1 for pin in self.pins if not pin.standing)
def draw_lane(self):
# Enable texturing for the lane
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.lane_texture)
# Lane with texture
glColor3f(0.9, 0.8, 0.6) # Wood color
glBegin(GL_QUADS)
glTexCoord2f(0, 0)
glVertex3f(-2, 0, 20)
glTexCoord2f(1, 0)
glVertex3f(2, 0, 20)
glTexCoord2f(1, 1)
glVertex3f(2, 0, -15)
glTexCoord2f(0, 1)
glVertex3f(-2, 0, -15)
glEnd()
glDisable(GL_TEXTURE_2D)
# Gutters with improved appearance
glColor3f(0.3, 0.3, 0.3)
glBegin(GL_QUADS)
# Left gutter
glVertex3f(-2.5, 0, 20)
glVertex3f(-2, 0, 20)
glVertex3f(-2, 0, -15)
glVertex3f(-2.5, 0, -15)
# Right gutter
glVertex3f(2, 0, 20)
glVertex3f(2.5, 0, 20)
glVertex3f(2.5, 0, -15)
glVertex3f(2, 0, -15)
glEnd()
# Gutter edges
glColor3f(0.5, 0.5, 0.5)
glLineWidth(2.0)
glBegin(GL_LINES)
# Left edge
glVertex3f(-2.5, 0.1, 20)
glVertex3f(-2.5, 0.1, -15)
# Right edge
glVertex3f(2.5, 0.1, 20)
glVertex3f(2.5, 0.1, -15)
glEnd()
# Lane boundaries
glColor3f(0.9, 0.9, 0.1) # Bright yellow
glLineWidth(3.0)
glBegin(GL_LINES)
# Left boundary
glVertex3f(-2, 0.01, 20)
glVertex3f(-2, 0.01, -15)
# Right boundary
glVertex3f(2, 0.01, 20)
glVertex3f(2, 0.01, -15)
glEnd()
# Arrows with improved visibility
glColor3f(0.7, 0.7, 0.2)
for z in range(5, 15, 3):
glBegin(GL_TRIANGLES)
glVertex3f(0, 0.01, z)
glVertex3f(-0.2, 0.01, z + 0.5)
glVertex3f(0.2, 0.01, z + 0.5)
glEnd()
# Foul line
glColor3f(1.0, 0.0, 0.0) # Red
glLineWidth(2.0)
glBegin(GL_LINES)
glVertex3f(-2, 0.02, 0)
glVertex3f(2, 0.02, 0)
glEnd()
glEnable(GL_LIGHTING)
def draw_hud(self):
glDisable(GL_LIGHTING)
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
glOrtho(0, self.width, self.height, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
glDisable(GL_DEPTH_TEST)
# Semi-transparent HUD background
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glColor4f(0.1, 0.1, 0.2, 0.7)
glBegin(GL_QUADS)
glVertex2f(0, 0)
glVertex2f(self.width, 0)
glVertex2f(self.width, 80)
glVertex2f(0, 80)
glEnd()
glDisable(GL_BLEND)
# Score display
font = pygame.font.SysFont('Arial', 24)
score_text = font.render(f"Score: {self.score} | Frame: {self.frame} | Roll: {self.roll}", True, (255, 255, 255))
text_surface = pygame.image.tostring(score_text, 'RGBA', True)
glRasterPos2f(20, 20)
glDrawPixels(score_text.get_width(), score_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
# Power bar with improved visuals
if not self.ball.launched and self.waiting_for_roll:
bar_width = 200
bar_height = 25
x, y = 20, self.height - 60
# Background
glColor3f(0.3, 0.3, 0.3)
glBegin(GL_QUADS)
glVertex2f(x, y)
glVertex2f(x + bar_width, y)
glVertex2f(x + bar_width, y + bar_height)
glVertex2f(x, y + bar_height)
glEnd()
# Power fill with gradient
fill = self.power * bar_width
if self.power < 0.3:
glColor3f(0.0, 1.0, 0.0) # Green
elif self.power < 0.7:
glColor3f(1.0, 1.0, 0.0) # Yellow
else:
glColor3f(1.0, 0.0, 0.0) # Red
glBegin(GL_QUADS)
glVertex2f(x, y)
glVertex2f(x + fill, y)
glVertex2f(x + fill, y + bar_height)
glVertex2f(x, y + bar_height)
glEnd()
# Border
glColor3f(1.0, 1.0, 1.0)
glLineWidth(2.0)
glBegin(GL_LINE_LOOP)
glVertex2f(x, y)
glVertex2f(x + bar_width, y)
glVertex2f(x + bar_width, y + bar_height)
glVertex2f(x, y + bar_height)
glEnd()
# Power label
power_text = font.render("Power", True, (255, 255, 255))
text_surface = pygame.image.tostring(power_text, 'RGBA', True)
glRasterPos2f(x, y - 25)
glDrawPixels(power_text.get_width(), power_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
# Angle indicator
angle_y = y - 50
glColor3f(0.5, 0.5, 1.0)
glLineWidth(3.0)
glBegin(GL_LINES)
glVertex2f(x + bar_width/2, angle_y)
glVertex2f(x + bar_width/2 + self.angle * 2, angle_y)
glEnd()
# Angle label
angle_text = font.render("Angle", True, (255, 255, 255))
text_surface = pygame.image.tostring(angle_text, 'RGBA', True)
glRasterPos2f(x, angle_y - 25)
glDrawPixels(angle_text.get_width(), angle_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
# Instructions
instructions = font.render("Use LEFT/RIGHT to aim, SPACE to charge power", True, (200, 200, 100))
text_surface = pygame.image.tostring(instructions, 'RGBA', True)
glRasterPos2f(self.width - instructions.get_width() - 20, 20)
glDrawPixels(instructions.get_width(), instructions.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
# Display knocked pins for current roll
if self.ball_done:
knocked_text = font.render(f"Knocked down: {self.knocked_this_roll} pins", True, (255, 255, 255))
text_surface = pygame.image.tostring(knocked_text, 'RGBA', True)
glRasterPos2f(self.width/2 - knocked_text.get_width()/2, 50)
glDrawPixels(knocked_text.get_width(), knocked_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
continue_text = font.render("Press SPACE to continue", True, (200, 200, 100))
text_surface = pygame.image.tostring(continue_text, 'RGBA', True)
glRasterPos2f(self.width/2 - continue_text.get_width()/2, 80)
glDrawPixels(continue_text.get_width(), continue_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
glEnable(GL_DEPTH_TEST)
glPopMatrix()
glMatrixMode(GL_PROJECTION)
glPopMatrix()
glMatrixMode(GL_MODELVIEW)
glEnable(GL_LIGHTING)
def handle_input(self):
keys = pygame.key.get_pressed()
# Camera controls
if keys[K_a]:
self.camera_angle -= 1
if keys[K_d]:
self.camera_angle += 1
if keys[K_w] and self.camera_distance > 10:
self.camera_distance -= 0.5
if keys[K_s] and self.camera_distance < 30:
self.camera_distance += 0.5
# Update camera position based on angle
rad_angle = math.radians(self.camera_angle)
self.camera_pos[0] = math.sin(rad_angle) * self.camera_distance
self.camera_pos[2] = math.cos(rad_angle) * self.camera_distance + 5
if self.waiting_for_roll and not self.ball.launched:
# Adjust angle
if keys[K_LEFT]:
self.angle -= 1
self.angle = max(-30, self.angle)
if keys[K_RIGHT]:
self.angle += 1
self.angle = min(30, self.angle)
# Charge power
if keys[K_SPACE]:
if not self.charging:
self.charging = True
self.power = 0
else:
self.power += 0.02
if self.power > self.max_power:
self.power = self.max_power
else:
if self.charging and self.power > 0.1: # Minimum power threshold
self.ball.launch(self.power, self.angle)
self.charging = False
self.waiting_for_roll = False
# Continue to next roll
if self.ball_done and keys[K_SPACE]:
self.next_roll()
def next_roll(self):
self.knocked_this_roll = self.count_knocked_pins()
self.score += self.knocked_this_roll
# Game progression logic
if self.roll == 1:
if self.knocked_this_roll == 10: # Strike
self.roll = 3 # Skip second roll
else:
self.roll = 2
elif self.roll == 2:
self.roll = 3
if self.roll == 3:
self.frame += 1
self.roll = 1
self.reset_pins()
if self.frame > 10: # End of game
self.game_over = True
self.reset_ball()
def run(self):
clock = pygame.time.Clock()
running = True
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == QUIT:
running = False
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
running = False
elif event.key == K_r:
self.reset_ball()
self.reset_pins()
self.score = 0
self.frame = 1
self.roll = 1
self.game_over = False
self.handle_input()
# Update game objects
if not self.ball_done:
self.ball_done = self.ball.update(dt, self.pins)
for pin in self.pins:
pin.update(dt)
# Render
glClearColor(*self.background_color, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
# Update camera target to follow ball
if self.ball.launched and not self.ball_done:
self.camera_target[0] = self.ball.pos[0] * 0.1
self.camera_target[2] = self.ball.pos[2] * 0.1
else:
self.camera_target = [0, 1, 0]
gluLookAt(self.camera_pos[0], self.camera_pos[1], self.camera_pos[2],
self.camera_target[0], self.camera_target[1], self.camera_target[2],
0, 1, 0)
self.draw_lane()
self.ball.draw()
for pin in self.pins:
pin.draw()
self.draw_hud()
# Game over screen
if self.game_over:
glDisable(GL_LIGHTING)
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
glOrtho(0, self.width, self.height, 0, -1, 1)
glMatrixMode(GL_MODELVIEW)
glPushMatrix()
glLoadIdentity()
glDisable(GL_DEPTH_TEST)
# Semi-transparent overlay
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glColor4f(0.0, 0.0, 0.0, 0.7)
glBegin(GL_QUADS)
glVertex2f(0, 0)
glVertex2f(self.width, 0)
glVertex2f(self.width, self.height)
glVertex2f(0, self.height)
glEnd()
glDisable(GL_BLEND)
# Game over text
font = pygame.font.SysFont('Arial', 48)
game_over_text = font.render("GAME OVER", True, (255, 255, 255))
text_surface = pygame.image.tostring(game_over_text, 'RGBA', True)
glRasterPos2f(self.width/2 - game_over_text.get_width()/2, self.height/2 - 50)
glDrawPixels(game_over_text.get_width(), game_over_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
score_text = font.render(f"Final Score: {self.score}", True, (255, 255, 255))
text_surface = pygame.image.tostring(score_text, 'RGBA', True)
glRasterPos2f(self.width/2 - score_text.get_width()/2, self.height/2 + 20)
glDrawPixels(score_text.get_width(), score_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
restart_text = pygame.font.SysFont('Arial', 24).render("Press R to restart", True, (200, 200, 100))
text_surface = pygame.image.tostring(restart_text, 'RGBA', True)
glRasterPos2f(self.width/2 - restart_text.get_width()/2, self.height/2 + 80)
glDrawPixels(restart_text.get_width(), restart_text.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, text_surface)
glEnable(GL_DEPTH_TEST)
glPopMatrix()
glMatrixMode(GL_PROJECTION)
glPopMatrix()
glMatrixMode(GL_MODELVIEW)
glEnable(GL_LIGHTING)
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
game = BowlingGame()
game.run()

275
simulations/balls/main.py Normal file
View File

@@ -0,0 +1,275 @@
import pygame
import math
import random
# Initialize Pygame
pygame.init()
# Screen setup
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Enhanced Physics Bouncing Balls")
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (220, 50, 50)
GREEN = (50, 220, 50)
CYAN = (50, 220, 220)
YELLOW = (220, 220, 50)
# Clock
clock = pygame.time.Clock()
FPS = 60
# Global physics
gravity = 0.5
elasticity = 0.85
air_resistance = 0.99 # 1.0 = no resistance, lower = more resistance
friction = 0.98 # Ground friction when rolling
# Ball class
class Ball:
def __init__(self, x, y):
self.radius = 20
self.x = x
self.y = y
self.mass = self.radius / 10.0 # Mass proportional to radius
# Give random initial velocity in both x and y
self.vx = random.uniform(-5, 5)
self.vy = random.uniform(-5, 0)
self.tracer_length = 50
self.tracers = []
self.dragging = False
self.drag_start_pos = None
self.drag_last_pos = (x, y)
self.drag_history = []
def update(self):
if not self.dragging:
# Apply gravity
self.vy += gravity
# Apply air resistance (quadratic drag)
speed = math.hypot(self.vx, self.vy)
if speed > 0:
drag_force = (1 - air_resistance) * speed
drag_x = -drag_force * (self.vx / speed)
drag_y = -drag_force * (self.vy / speed)
self.vx += drag_x
self.vy += drag_y
# Update position
self.x += self.vx
self.y += self.vy
# Bounce off floor with friction
if self.y + self.radius > HEIGHT:
self.y = HEIGHT - self.radius
self.vy = -self.vy * elasticity
self.vx *= friction # Apply friction when bouncing on ground
# Stop if moving very slowly
if abs(self.vy) < 0.1:
self.vy = 0
if abs(self.vx) < 0.1:
self.vx = 0
# Bounce off ceiling
if self.y - self.radius < 0:
self.y = self.radius
self.vy = -self.vy * elasticity
# Bounce off right wall
if self.x + self.radius > WIDTH:
self.x = WIDTH - self.radius
self.vx = -self.vx * elasticity
# Bounce off left wall
if self.x - self.radius < 0:
self.x = self.radius
self.vx = -self.vx * elasticity
# Update tracers
self.tracers.append((int(self.x), int(self.y)))
if len(self.tracers) > self.tracer_length:
self.tracers.pop(0)
def start_drag(self, mx, my):
self.dragging = True
self.drag_start_pos = (mx, my)
self.drag_last_pos = (mx, my)
self.drag_history = [(mx, my)]
self.vx = 0
self.vy = 0
def update_drag(self, mx, my):
if self.dragging:
self.x, self.y = mx, my
self.drag_history.append((mx, my))
if len(self.drag_history) > 5: # Keep last 5 positions for velocity calculation
self.drag_history.pop(0)
self.drag_last_pos = (mx, my)
def end_drag(self):
if self.dragging and len(self.drag_history) >= 2:
# Calculate velocity from drag motion (throw mechanics)
dx = self.drag_history[-1][0] - self.drag_history[0][0]
dy = self.drag_history[-1][1] - self.drag_history[0][1]
frames = len(self.drag_history)
# Set velocity based on drag motion
self.vx = dx / frames * 2
self.vy = dy / frames * 2
self.dragging = False
self.drag_start_pos = None
self.drag_history = []
def draw(self, surface):
# Draw tracers with fade effect
for i, pos in enumerate(self.tracers):
alpha = int(255 * (i / len(self.tracers)))
size = int(3 + 2 * (i / len(self.tracers)))
color = (50, 220, 220)
pygame.draw.circle(surface, color, pos, size)
# Draw ball
pygame.draw.circle(surface, RED, (int(self.x), int(self.y)), self.radius)
# Draw velocity vector (scaled for visibility)
if not self.dragging:
vel_scale = 3
pygame.draw.line(surface, GREEN,
(int(self.x), int(self.y)),
(int(self.x + self.vx * vel_scale),
int(self.y + self.vy * vel_scale)), 3)
# Draw drag trajectory preview
if self.dragging and self.drag_start_pos:
pygame.draw.line(surface, YELLOW,
self.drag_start_pos,
(int(self.x), int(self.y)), 2)
# Draw arrow at the end
pygame.draw.circle(surface, YELLOW, (int(self.x), int(self.y)), 5)
# Ball-to-ball collision detection and response
def handle_ball_collisions(balls):
for i, ball1 in enumerate(balls):
for ball2 in balls[i+1:]:
dx = ball2.x - ball1.x
dy = ball2.y - ball1.y
distance = math.hypot(dx, dy)
# Check if balls are colliding
if distance < ball1.radius + ball2.radius and distance > 0:
# Normalize collision vector
nx = dx / distance
ny = dy / distance
# Separate balls
overlap = (ball1.radius + ball2.radius) - distance
ball1.x -= nx * overlap * 0.5
ball1.y -= ny * overlap * 0.5
ball2.x += nx * overlap * 0.5
ball2.y += ny * overlap * 0.5
# Calculate relative velocity
dvx = ball2.vx - ball1.vx
dvy = ball2.vy - ball1.vy
# Calculate relative velocity in collision normal direction
dot_product = dvx * nx + dvy * ny
# Only resolve if balls are moving toward each other
if dot_product < 0:
# Calculate impulse (conservation of momentum with equal mass)
impulse = 2 * dot_product / (ball1.mass + ball2.mass)
# Apply impulse to both balls
ball1.vx += impulse * ball2.mass * nx * elasticity
ball1.vy += impulse * ball2.mass * ny * elasticity
ball2.vx -= impulse * ball1.mass * nx * elasticity
ball2.vy -= impulse * ball1.mass * ny * elasticity
# List of balls
balls = [Ball(WIDTH//2, HEIGHT//2)]
running = True
while running:
mx, my = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Drag start
elif event.type == pygame.MOUSEBUTTONDOWN:
for ball in balls:
if math.hypot(mx - ball.x, my - ball.y) < ball.radius + 10:
ball.start_drag(mx, my)
break
# Drag end
elif event.type == pygame.MOUSEBUTTONUP:
for ball in balls:
if ball.dragging:
ball.end_drag()
# Spawn new ball
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
balls.append(Ball(random.randint(50, WIDTH-50),
random.randint(50, HEIGHT-50)))
elif event.key == pygame.K_c:
balls.clear()
balls.append(Ball(WIDTH//2, HEIGHT//2))
# Keyboard control for physics
keys = pygame.key.get_pressed()
if keys[pygame.K_UP]:
gravity = min(2.0, gravity + 0.01)
if keys[pygame.K_DOWN]:
gravity = max(0, gravity - 0.01)
if keys[pygame.K_RIGHT]:
elasticity = min(1.0, elasticity + 0.01)
if keys[pygame.K_LEFT]:
elasticity = max(0, elasticity - 0.01)
if keys[pygame.K_w]:
air_resistance = min(1.0, air_resistance + 0.001)
if keys[pygame.K_s]:
air_resistance = max(0.9, air_resistance - 0.001)
# Update dragging balls
for ball in balls:
if ball.dragging:
ball.update_drag(mx, my)
# Update balls
for ball in balls:
ball.update()
# Handle collisions between balls
handle_ball_collisions(balls)
# Drawing
screen.fill(BLACK)
for ball in balls:
ball.draw(screen)
# Physics info
font = pygame.font.SysFont(None, 22)
info = [
f"Gravity: {gravity:.2f} (↑↓) Elasticity: {elasticity:.2f} (←→)",
f"Air Resistance: {air_resistance:.3f} (WS) Balls: {len(balls)}",
f"SPACE: Add ball C: Clear Drag to throw!"
]
for i, text in enumerate(info):
surface = font.render(text, True, WHITE)
screen.blit(surface, (10, 10 + i * 25))
pygame.display.flip()
clock.tick(FPS)
pygame.quit()