731 lines
26 KiB
Python
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() |