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