Files
INF6B/simulations/balls/bowling.py
rattatwinko d0eaabdd87 some new stuff.
idk its all pretty fun! some C++ too!
2025-10-15 11:16:51 +02:00

731 lines
26 KiB
Python

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()