some new stuff.
idk its all pretty fun! some C++ too!
This commit is contained in:
731
simulations/balls/bowling.py
Normal file
731
simulations/balls/bowling.py
Normal 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
275
simulations/balls/main.py
Normal 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()
|
||||
Reference in New Issue
Block a user