0% found this document useful (0 votes)
61 views35 pages

Bubble Shooter Improved

This document outlines the implementation of a bubble shooting game using Pygame, detailing constants, color definitions, game states, sound effects, and a particle system for visual effects. It includes a Bubble class with enhanced 3D visuals and pop animations, as well as methods for creating and managing particle effects. The document also covers gameplay mechanics such as bubble movement, shooting speed, and collision handling.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
61 views35 pages

Bubble Shooter Improved

This document outlines the implementation of a bubble shooting game using Pygame, detailing constants, color definitions, game states, sound effects, and a particle system for visual effects. It includes a Bubble class with enhanced 3D visuals and pop animations, as well as methods for creating and managing particle effects. The document also covers gameplay mechanics such as bubble movement, shooting speed, and collision handling.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 35

import pygame

import sys
import math
import random
from collections import deque
import os

# --- Constants ---


SCREEN_WIDTH = 800
SCREEN_HEIGHT = 900
GRID_ROWS = 15
GRID_COLS = 11
BUBBLE_RADIUS = 28
BUBBLE_DIAMETER = BUBBLE_RADIUS * 2

# Grid dimensions
GRID_START_X = (SCREEN_WIDTH - (GRID_COLS * BUBBLE_DIAMETER - BUBBLE_RADIUS)) / 2
GRID_START_Y = 80
ROW_HEIGHT = int(BUBBLE_RADIUS * math.sqrt(3))

# --- Color Definitions with Enhanced Gradients ---


# Main colors (RGB)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)
DARK_BLUE = (20, 20, 50) # Darker background color

# Bubble colors with improved gradient info (outer color, inner color, shine color)
BUBBLE_COLORS = {
"RED": [(220, 20, 60), (255, 120, 120), (255, 200, 200)], # Red
"BLUE": [(20, 50, 230), (100, 150, 255), (180, 210, 255)], # Blue
"GREEN": [(20, 180, 20), (120, 255, 120), (200, 255, 200)], # Green
"YELLOW": [(230, 180, 0), (255, 255, 80), (255, 255, 200)], # Yellow
"CYAN": [(0, 180, 180), (90, 255, 255), (180, 255, 255)], # Cyan
"PURPLE": [(180, 30, 230), (230, 150, 255), (240, 200, 255)], # Purple
}

AVAILABLE_COLORS = list(BUBBLE_COLORS.keys())

# Game States
STATE_MENU = 0
STATE_PLAYING = 1
STATE_GAME_OVER = 2
STATE_LEVEL_COMPLETE = 3
STATE_PAUSED = 4

# Physics/Gameplay
SHOOTER_Y = SCREEN_HEIGHT - 100
SHOOT_SPEED = 18
SHOTS_UNTIL_DROP = 8
CEILING_BUFFER = BUBBLE_RADIUS

GAME_OVER_LINE_Y = SCREEN_HEIGHT - BUBBLE_DIAMETER * 3.5

# --- Sound Effects (Optional) ---


def load_sounds():
sounds = {}

# Create a dummy Sound class to avoid errors


class DummySound:
def __init__(self):
pass
def play(self):
pass
def set_volume(self, vol):
pass

sound_files = {
"shoot": "sounds/shoot.wav",
"pop": "sounds/pop.wav",
"match": "sounds/match.wav",
"bounce": "sounds/bounce.wav",
"drop": "sounds/drop.wav",
"game_over": "sounds/game_over.wav",
"level_complete": "sounds/level_complete.wav",
}

# Create dummy sounds if files don't exist


for name, file in sound_files.items():
try:
if os.path.exists(file):
sounds[name] = pygame.mixer.Sound(file)
sounds[name].set_volume(0.7)
else:
sounds[name] = DummySound()
except:
sounds[name] = DummySound()

return sounds

# --- Particle Effect System ---


class ParticleSystem:
def __init__(self):
self.particles = []

def add_burst(self, pos, color, count=15, speed_range=(1, 4), size_range=(2,


8), life_range=(15, 35)):
for _ in range(count):
angle = random.uniform(0, math.pi * 2)
speed = random.uniform(speed_range[0], speed_range[1])
size = random.uniform(size_range[0], size_range[1])
life = random.randint(life_range[0], life_range[1])

# Add some randomness to the color


color_variation = [
min(255, max(0, c + random.randint(-20, 20)))
for c in color
]

self.particles.append({
'pos': pygame.Vector2(pos),
'velocity': pygame.Vector2(math.cos(angle) * speed, math.sin(angle)
* speed),
'color': color_variation,
'size': size,
'life': life,
'max_life': life,
'fade': True,
'pulsate': random.random() > 0.5,
'pulse_rate': random.uniform(0.05, 0.2)
})

def add_sparkle(self, pos, color, count=3):


"""Add sparkle effect particles"""
for _ in range(count):
# Random position around the center
offset = pygame.Vector2(random.uniform(-BUBBLE_RADIUS*0.8,
BUBBLE_RADIUS*0.8),
random.uniform(-BUBBLE_RADIUS*0.8,
BUBBLE_RADIUS*0.8))

# Sparkle particle with short life


self.particles.append({
'pos': pygame.Vector2(pos) + offset,
'velocity': pygame.Vector2(0, -0.2), # Slight upward drift
'color': (255, 255, 255), # White sparkle
'size': random.uniform(1, 3),
'life': random.randint(5, 15),
'max_life': 15,
'fade': True,
'pulsate': True,
'pulse_rate': 0.4
})

def update(self):
i = 0
while i < len(self.particles):
p = self.particles[i]
p['pos'] += p['velocity']
p['life'] -= 1

# Apply gravity to some particles


if random.random() > 0.7:
p['velocity'].y += 0.05

# Slow down particles gradually


p['velocity'] *= 0.98

if p['life'] <= 0:
self.particles.pop(i)
else:
i += 1

def draw(self, surface):


for p in self.particles:
# Calculate alpha based on remaining life
alpha = 255 * (p['life'] / p['max_life']) if p['fade'] else 255

# Pulsating effect
size_factor = 1.0
if p['pulsate']:
size_factor = 0.7 + 0.5 * math.sin(p['life'] * p['pulse_rate'])

size = p['size'] * size_factor

# Create a surface for the particle with alpha


color = p['color'] + (int(alpha),)
particle_surf = pygame.Surface((size*2, size*2), pygame.SRCALPHA)

# For sparkle particles, draw a star shape


if p['color'] == (255, 255, 255) and p['pulsate']:
# Draw a simple star shape
center = (size, size)
points = []
num_points = 4
for i in range(num_points * 2):
angle = math.pi * 2 * i / (num_points * 2)
dist = size if i % 2 == 0 else size * 0.4
points.append((
center[0] + math.cos(angle) * dist,
center[1] + math.sin(angle) * dist
))
if len(points) >= 3:
pygame.draw.polygon(particle_surf, color, points)
else:
# Regular circular particle
pygame.draw.circle(particle_surf, color, (size, size), size)

# Blit the particle


surface.blit(particle_surf, (p['pos'].x - size, p['pos'].y - size))

# --- Bubble Class with Enhanced 3D Visuals ---


class Bubble(pygame.sprite.Sprite):
def __init__(self, color_name, grid_pos=None, world_pos=None):
super().__init__()
self.radius = BUBBLE_RADIUS
self.color_name = color_name
self.color = BUBBLE_COLORS[color_name][0] # Outer color
self.inner_color = BUBBLE_COLORS[color_name][1] # Inner color
self.shine_color = BUBBLE_COLORS[color_name][2] # Shine/highlight color
self.grid_pos = grid_pos # (col, row) tuple or None

# Create the bubble surface with enhanced 3D gradient and shine


self.image = self.create_bubble_surface()
self.rect = self.image.get_rect()

if world_pos:
self.rect.center = world_pos
elif grid_pos and 'grid_manager' in globals():
self.rect.center = grid_manager.grid_to_world(grid_pos)

# For moving bubbles


self.velocity = pygame.Vector2(0, 0)
self.rotation = 0 # For visual spinning effect
self.rotation_speed = random.uniform(-2, 2)

# For cluster checks


self.processed = False
self.is_anchored = False

# Animation properties
self.pop_animation = False
self.pop_frame = 0
self.pop_max_frames = 8
self.scale = 1.0 # For scaling animations
# Effects
self.shake = 0
self.shake_offset = pygame.Vector2(0, 0)
self.sparkle_timer = random.randint(0, 100) # Random start time for
sparkle
self.pulse = 0

def create_bubble_surface(self):
# Create a surface with alpha channel
surf = pygame.Surface((BUBBLE_DIAMETER, BUBBLE_DIAMETER), pygame.SRCALPHA)

# Draw 3D gradient (from outer to inner to shine color)


# Outer edge (darkest)
pygame.draw.circle(surf, self.color, (self.radius, self.radius),
self.radius)

# Middle gradient
for i in range(self.radius-1, self.radius//2, -1):
ratio = (i - self.radius//2) / (self.radius - self.radius//2)
r = int(self.color[0] * ratio + self.inner_color[0] * (1 - ratio))
g = int(self.color[1] * ratio + self.inner_color[1] * (1 - ratio))
b = int(self.color[2] * ratio + self.inner_color[2] * (1 - ratio))
pygame.draw.circle(surf, (r, g, b), (self.radius, self.radius), i)

# Inner gradient (brighter)


for i in range(self.radius//2, 0, -1):
ratio = i / (self.radius//2)
r = int(self.inner_color[0] * ratio + self.shine_color[0] * (1 -
ratio))
g = int(self.inner_color[1] * ratio + self.shine_color[1] * (1 -
ratio))
b = int(self.inner_color[2] * ratio + self.shine_color[2] * (1 -
ratio))
pygame.draw.circle(surf, (r, g, b), (self.radius, self.radius), i)

# Add main highlight/shine


highlight_radius = self.radius * 0.6
highlight_offset = -self.radius * 0.3

# Create highlight mask


highlight = pygame.Surface((BUBBLE_DIAMETER, BUBBLE_DIAMETER),
pygame.SRCALPHA)
pygame.draw.circle(highlight, (255, 255, 255, 150),
(self.radius + highlight_offset, self.radius +
highlight_offset),
highlight_radius)
surf.blit(highlight, (0, 0), special_flags=pygame.BLEND_RGB_ADD)

# Add small secondary highlight (for more 3D effect)


small_highlight = pygame.Surface((BUBBLE_DIAMETER, BUBBLE_DIAMETER),
pygame.SRCALPHA)
small_highlight_offset = self.radius * 0.4
pygame.draw.circle(small_highlight, (255, 255, 255, 100),
(self.radius - small_highlight_offset, self.radius -
small_highlight_offset),
self.radius * 0.2)
surf.blit(small_highlight, (0, 0), special_flags=pygame.BLEND_RGB_ADD)

# Add stronger outline


pygame.draw.circle(surf, (*self.color, 200), (self.radius, self.radius),
self.radius, 2)

return surf

def create_pop_animation_frame(self, frame):


# Enhanced pop animation frames with more dramatic effect
scale = 1.0 + frame * 0.15 # Larger expansion
alpha = 255 - (frame * 32)

# Create a scaled surface


size = int(BUBBLE_DIAMETER * scale)
surf = pygame.Surface((size, size), pygame.SRCALPHA)

# Draw scaled bubble with fading alpha and color shift


scaled_radius = int(self.radius * scale)

# Shift colors towards white as it expands


fade_to_white = frame / self.pop_max_frames

# Outer edge (darkest)


outer_color = [int(c * (1-fade_to_white) + 255 * fade_to_white) for c in
self.color]
outer_color.append(int(alpha * (1-frame/self.pop_max_frames)))
pygame.draw.circle(surf, tuple(outer_color), (size//2, size//2),
scaled_radius)

# Middle gradient
for i in range(scaled_radius-1, scaled_radius//2, -1):
ratio = (i - scaled_radius//2) / (scaled_radius - scaled_radius//2)
r = int(outer_color[0] * ratio + self.inner_color[0] * (1 - ratio))
g = int(outer_color[1] * ratio + self.inner_color[1] * (1 - ratio))
b = int(outer_color[2] * ratio + self.inner_color[2] * (1 - ratio))
a = min(alpha, 255)
pygame.draw.circle(surf, (r, g, b, a), (size//2, size//2), i)

# Inner gradient (brighter)


inner_radius = scaled_radius//2
for i in range(inner_radius, 0, -1):
ratio = i / inner_radius
r = int(self.inner_color[0] * ratio + self.shine_color[0] * (1 -
ratio))
g = int(self.inner_color[1] * ratio + self.shine_color[1] * (1 -
ratio))
b = int(self.inner_color[2] * ratio + self.shine_color[2] * (1 -
ratio))
a = min(alpha, 255)
pygame.draw.circle(surf, (r, g, b, a), (size//2, size//2), i)

return surf

def start_pop_animation(self):
self.pop_animation = True
self.pop_frame = 0

def update(self):
# Update pulsing effect for idle bubbles
self.pulse = (self.pulse + 0.05) % (math.pi * 2)
# Update sparkle timer
if not self.velocity.length_squared() > 0 and not self.pop_animation:
self.sparkle_timer -= 1
if self.sparkle_timer <= 0:
# Create sparkle effect
if 'grid_manager' in globals() and random.random() > 0.7:
grid_manager.particles.add_sparkle(self.rect.center,
self.color)
self.sparkle_timer = random.randint(80, 200) # Reset timer

# Handle pop animation


if self.pop_animation:
self.pop_frame += 1
if self.pop_frame >= self.pop_max_frames:
self.kill()
return

# Apply shake effect


if self.shake > 0:
self.shake_offset.x = random.randint(-3, 3)
self.shake_offset.y = random.randint(-3, 3)
self.shake -= 1
else:
self.shake_offset = pygame.Vector2(0, 0)

# Only move if it's a shot bubble


if self.velocity.length_squared() > 0:
self.rect.move_ip(self.velocity)

# Spin effect for moving bubbles


self.rotation += self.rotation_speed

# Wall Bounce with improved feedback


if self.rect.left <= 0 or self.rect.right >= SCREEN_WIDTH:
self.velocity.x *= -1
# Clamp position to prevent sticking
self.rect.left = max(0, self.rect.left)
self.rect.right = min(SCREEN_WIDTH, self.rect.right)

# Add bounce effect


self.shake = 5
self.rotation_speed *= -1

# Add bounce particles


if 'grid_manager' in globals():
grid_manager.particles.add_burst(
self.rect.center,
[255, 255, 255],
count=5,
life_range=(5, 15)
)

# Play bounce sound if available


if 'sounds' in globals() and 'bounce' in sounds:
sounds['bounce'].play()

# Ceiling Collision
if self.rect.top <= CEILING_BUFFER:
self.rect.top = CEILING_BUFFER
self.velocity = pygame.Vector2(0, 0)
self.shake = 5
# Let the game manager handle the snapping logic

def draw(self, surface):


# For pop animation
if self.pop_animation:
frame = self.create_pop_animation_frame(self.pop_frame)
frame_rect = frame.get_rect(center=self.rect.center)
surface.blit(frame, frame_rect)
return

# Apply rotation for moving bubbles if needed


if self.velocity.length_squared() > 0:
rotated_image = pygame.transform.rotate(self.image, self.rotation)
rotated_rect = rotated_image.get_rect(center=self.rect.center +
self.shake_offset)
surface.blit(rotated_image, rotated_rect)
else:
# For stationary bubbles, apply subtle pulsing effect
pulse_factor = 1.0 + math.sin(self.pulse) * 0.03

if pulse_factor != 1.0:
# Only scale if we need to
pulsed_size = int(BUBBLE_DIAMETER * pulse_factor)
pulsed_image = pygame.transform.smoothscale(self.image,
(pulsed_size, pulsed_size))
pulsed_rect = pulsed_image.get_rect(center=self.rect.center +
self.shake_offset)
surface.blit(pulsed_image, pulsed_rect)
else:
# Just apply shake if any
surface.blit(self.image, (self.rect.x + self.shake_offset.x,
self.rect.y + self.shake_offset.y))

# --- Grid Manager Class with Fixed Matching Logic ---


class GridManager:
def __init__(self, rows, cols, start_x, start_y):
self.rows = rows
self.cols = cols
self.start_x = start_x
self.start_y = start_y
self.grid = [[None for _ in range(cols)] for _ in range(rows)]
self.bubbles = pygame.sprite.Group()
self.particles = ParticleSystem()

# Visual effects
self.shake_amount = 0
self.shake_offset = pygame.Vector2(0, 0)

# Debug
self.debug_info = {"last_match_check": None}

def grid_to_world(self, grid_pos):


col, row = grid_pos
x = self.start_x + col * BUBBLE_DIAMETER
# Offset odd rows for hex layout
if row % 2 != 0:
x += BUBBLE_RADIUS
y = self.start_y + row * ROW_HEIGHT
return (int(x), int(y))

def world_to_grid(self, world_pos):


wx, wy = world_pos

# Account for grid shake effect


wx -= self.shake_offset.x
wy -= self.shake_offset.y

# Calculate row
row_f = (wy - self.start_y) / ROW_HEIGHT
row = int(round(row_f))
row = max(0, min(self.rows - 1, row))

# Calculate column (accounting for odd row offset)


x_offset = BUBBLE_RADIUS if row % 2 != 0 else 0
col_f = (wx - self.start_x - x_offset) / BUBBLE_DIAMETER
col = int(round(col_f))
col = max(0, min(self.cols - 1, col))

return (col, row)

def is_valid_grid_pos(self, grid_pos):


col, row = grid_pos
return 0 <= row < self.rows and 0 <= col < self.cols

def get_neighbors(self, grid_pos):


col, row = grid_pos
neighbors = []
# Offsets depend on row parity for hex grid
if row % 2 == 0: # Even row
offsets = [(-1, -1), (0, -1), (-1, 0), (1, 0), (-1, 1), (0, 1)]
else: # Odd row
offsets = [(0, -1), (1, -1), (-1, 0), (1, 0), (0, 1), (1, 1)]

for dc, dr in offsets:


nc, nr = col + dc, row + dr
if self.is_valid_grid_pos((nc, nr)):
neighbors.append((nc, nr))
return neighbors

def spawn_initial_bubbles(self, num_rows):


for r in range(num_rows):
for c in range(self.cols):
if self.grid[r][c] is None:
color_name = random.choice(AVAILABLE_COLORS)
grid_pos = (c, r)
bubble = Bubble(color_name, grid_pos=grid_pos)
self.grid[r][c] = bubble
self.bubbles.add(bubble)
if r == 0: # Anchor top row bubbles
bubble.is_anchored = True

# Animate the initial bubbles appearing with a cascade effect


self.apply_grid_shake(10)

def apply_grid_shake(self, amount):


self.shake_amount = amount
def update_effects(self):
# Update grid shake effect
if self.shake_amount > 0:
self.shake_offset.x = random.randint(-self.shake_amount,
self.shake_amount)
self.shake_offset.y = random.randint(-self.shake_amount,
self.shake_amount)
self.shake_amount -= 1
else:
self.shake_offset = pygame.Vector2(0, 0)

# Update particle effects


self.particles.update()

def find_nearest_empty_slot(self, target_world_pos):


# Improved BFS search for best empty slot
start_grid_pos = self.world_to_grid(target_world_pos)

if self.is_valid_grid_pos(start_grid_pos) and self.grid[start_grid_pos[1]]


[start_grid_pos[0]] is None:
return start_grid_pos

q = deque([start_grid_pos])
visited = {start_grid_pos}
min_dist_sq = float('inf')
best_slot = None

while q:
curr_c, curr_r = q.popleft()

# Check if current is empty and closer


if self.is_valid_grid_pos((curr_c, curr_r)) and self.grid[curr_r]
[curr_c] is None:
dist_sq = (pygame.Vector2(target_world_pos) -
pygame.Vector2(self.grid_to_world((curr_c, curr_r)))).length_squared()
if dist_sq < min_dist_sq:
min_dist_sq = dist_sq
best_slot = (curr_c, curr_r)
# Early exit if we're very close
if min_dist_sq < (BUBBLE_RADIUS * 0.8)**2:
break

# Explore neighbors
for nc, nr in self.get_neighbors((curr_c, curr_r)):
if self.is_valid_grid_pos((nc, nr)) and (nc, nr) not in visited:
visited.add((nc, nr))
q.append((nc, nr))

if len(visited) > 50:


break # Limit search depth

return best_slot

def snap_bubble(self, shot_bubble):


# Check if the bubble hit another bubble or ceiling
collision_bubble = None
for bubble in self.bubbles:
if pygame.sprite.collide_circle(shot_bubble, bubble):
collision_bubble = bubble
break

# Find the best empty slot


target_grid_pos = None
if collision_bubble:
# Use the collision point to find adjacent slot
direction = pygame.Vector2(shot_bubble.rect.center) -
pygame.Vector2(collision_bubble.rect.center)

# Avoid division by zero


if direction.length() > 0:
direction.normalize_ip()
else:
# If exactly overlapping, pick a random direction
angle = random.uniform(0, math.pi * 2)
direction = pygame.Vector2(math.cos(angle), math.sin(angle))

# Calculate approximate target position


approx_pos = (pygame.Vector2(collision_bubble.rect.center) +
direction * BUBBLE_DIAMETER * 0.95)

target_grid_pos = self.find_nearest_empty_slot(approx_pos)
else:
# Ceiling hit
target_grid_pos = self.find_nearest_empty_slot(shot_bubble.rect.center)

if target_grid_pos and self.is_valid_grid_pos(target_grid_pos):


col, row = target_grid_pos

# Create a new static bubble in the grid with the same color
new_grid_bubble = Bubble(shot_bubble.color_name,
grid_pos=target_grid_pos)
self.grid[row][col] = new_grid_bubble
self.bubbles.add(new_grid_bubble)

# Remove the flying shot bubble


shot_bubble.kill()

# Add snap effect


new_grid_bubble.shake = 8
self.apply_grid_shake(3)

# Add particles at snap point


color_rgb = BUBBLE_COLORS[new_grid_bubble.color_name][0][:3]
self.particles.add_burst(new_grid_bubble.rect.center, color_rgb,
count=5, life_range=(5, 15))

# Check for matches - FIXED LOGIC


matches = self.find_connected_bubbles(target_grid_pos,
match_color=True)

self.debug_info["last_match_check"] = {
"pos": target_grid_pos,
"color": new_grid_bubble.color_name,
"matches_count": len(matches),
"matches": [(b.grid_pos, b.color_name) for b in matches if
hasattr(b, 'grid_pos')]
}
if len(matches) >= 3:
# Score based on number of matches (exponential to reward larger
matches)
score_value = len(matches) * len(matches) * 5
global score
score += score_value

# Apply pop animation to matched bubbles with staggered timing


for i, bubble in enumerate(matches):
# Delay slightly for cascade effect
bubble.start_pop_animation()

# Add particle burst


world_pos = bubble.rect.center
color = BUBBLE_COLORS[bubble.color_name][0][:3] # Remove alpha
if present
self.particles.add_burst(world_pos, color, count=15)

# Remove from grid data


if bubble.grid_pos:
self.grid[bubble.grid_pos[1]][bubble.grid_pos[0]] = None

# Apply screen shake for big matches


self.apply_grid_shake(min(len(matches), 10))

# Play match sound


if 'sounds' in globals() and 'match' in sounds:
sounds['match'].play()

# Check for floating bubbles


self.check_floating_bubbles()
else:
# Play snap sound
if 'sounds' in globals() and 'pop' in sounds:
sounds['pop'].play()

return True

else:
# Failed to snap, destroy the shot
shot_bubble.kill()
return False

def find_connected_bubbles(self, start_grid_pos, match_color=False):


"""Find connected bubbles, with color matching if requested"""
if not self.is_valid_grid_pos(start_grid_pos):
return []

col, row = start_grid_pos


start_bubble = self.grid[row][col]

if not start_bubble:
return []

target_color_name = start_bubble.color_name
connected = []

# Use BFS to find all connected bubbles


q = deque([start_bubble])
visited = {start_bubble}

while q:
current = q.popleft()
connected.append(current)

# Check all neighbors


if not hasattr(current, 'grid_pos') or current.grid_pos is None:
continue

for nc, nr in self.get_neighbors(current.grid_pos):


if not self.is_valid_grid_pos((nc, nr)):
continue

neighbor = self.grid[nr][nc]

if (neighbor and
neighbor not in visited and
(not match_color or neighbor.color_name == target_color_name)):
visited.add(neighbor)
q.append(neighbor)

# If color matching, only return bubbles of the same color


if match_color:
same_color = [b for b in connected if b.color_name ==
target_color_name]
return same_color

return connected

def reset_processed_flags(self):
for bubble in self.bubbles:
bubble.processed = False
bubble.is_anchored = False

def check_floating_bubbles(self):
# Reset flags
self.reset_processed_flags()

# Find all bubbles connected to the ceiling


anchored_bubbles = set()
q = deque()

# Add all top-row bubbles to queue


for c in range(self.cols):
bubble = self.grid[0][c]
if bubble:
q.append(bubble)
anchored_bubbles.add(bubble)
bubble.is_anchored = True

processed_for_anchor = set(anchored_bubbles)

while q:
current = q.popleft()
if not hasattr(current, 'grid_pos') or current.grid_pos is None:
continue
for nc, nr in self.get_neighbors(current.grid_pos):
if not self.is_valid_grid_pos((nc, nr)):
continue

neighbor = self.grid[nr][nc]
if neighbor and neighbor not in processed_for_anchor:
neighbor.is_anchored = True
processed_for_anchor.add(neighbor)
q.append(neighbor)

# Identify and remove floating bubbles


floating = []
for bubble in self.bubbles:
if not bubble.is_anchored and not bubble.pop_animation:
floating.append(bubble)

if floating:
# Add score for floating bubbles
global score
score += len(floating) * 20

# Apply fall animation to floating bubbles


for bubble in floating:
# Add falling animation and effects
if hasattr(bubble, 'grid_pos') and bubble.grid_pos:
self.grid[bubble.grid_pos[1]][bubble.grid_pos[0]] = None

# Add particle effect


self.particles.add_burst(bubble.rect.center,
BUBBLE_COLORS[bubble.color_name][0][:3],
count=15)

# Start fall animation


bubble.velocity = pygame.Vector2(random.uniform(-1, 1),
random.uniform(3, 5))
bubble.rotation_speed = random.uniform(-5, 5)

# Schedule removal after animation


pygame.time.set_timer(pygame.USEREVENT, 1000, True)

# Apply screen shake


self.apply_grid_shake(min(len(floating), 15))

# Play drop sound


if 'sounds' in globals() and 'drop' in sounds:
sounds['drop'].play()

def add_new_row(self):
# Shift all bubbles down
for r in range(self.rows-1, 0, -1):
for c in range(self.cols):
self.grid[r][c] = self.grid[r-1][c]
if self.grid[r][c]:
self.grid[r][c].grid_pos = (c, r)
# Update bubble position visually
self.grid[r][c].rect.center = self.grid_to_world((c, r))

# Add new row at the top


for c in range(self.cols):
color_name = random.choice(AVAILABLE_COLORS)
new_bubble = Bubble(color_name, grid_pos=(c, 0))
self.grid[0][c] = new_bubble
self.bubbles.add(new_bubble)
new_bubble.is_anchored = True
new_bubble.shake = 10 # Add shake effect to new bubbles

# Apply screen shake


self.apply_grid_shake(8)

# Check game over condition


return self.check_game_over()

def check_game_over(self):
for bubble in self.bubbles:
if bubble.rect.bottom >= GAME_OVER_LINE_Y and not bubble.pop_animation:
return True
return False

def draw(self, surface):


# Apply grid shake effect to drawing
surface_copy = surface.copy()

# Draw grid bubble backgrounds or grid lines for debug


if DEBUG_MODE:
for r in range(self.rows):
for c in range(self.cols):
pos = self.grid_to_world((c, r))
pos = (pos[0] + self.shake_offset.x, pos[1] +
self.shake_offset.y)
pygame.draw.circle(surface_copy, GRAY, pos, BUBBLE_RADIUS, 1)

# Draw all bubbles with shake offset applied


for bubble in self.bubbles:
if not bubble.pop_animation:
original_center = bubble.rect.center
# Temporarily adjust position for drawing with grid shake
bubble.rect.center = (original_center[0] + self.shake_offset.x,
original_center[1] + self.shake_offset.y)
bubble.draw(surface_copy)
# Restore original position
bubble.rect.center = original_center
else:
# For popping bubbles, don't apply the grid shake
bubble.draw(surface_copy)

# Draw particles
self.particles.draw(surface_copy)

# Draw the game over line if in debug mode


if DEBUG_MODE:
pygame.draw.line(surface_copy, (255, 50, 50, 128),
(0, GAME_OVER_LINE_Y),
(SCREEN_WIDTH, GAME_OVER_LINE_Y), 2)

# Draw match debug info if available


if "last_match_check" in self.debug_info and
self.debug_info["last_match_check"]:
info = self.debug_info["last_match_check"]
text = f"Match check: {info['color']} - {info['matches_count']}
matches"
debug_surf = font_small.render(text, True, (255, 255, 255))
surface_copy.blit(debug_surf, (10, 10))

# Apply the drawn content to the main surface


surface.blit(surface_copy, (0, 0))

# --- Shooter Class with Improved Visuals ---


class Shooter:
def __init__(self, pos_x, pos_y):
self.pos = pygame.Vector2(pos_x, pos_y)
self.base_pos = pygame.Vector2(pos_x, pos_y)
self.current_bubble_color = None
self.next_bubble_color = None
self.aim_angle = -90 # Straight up initially (degrees)
self.load_bubbles()

# Cannon visual - improved with better metallic look


self.base_image = pygame.Surface((BUBBLE_DIAMETER * 1.5, BUBBLE_DIAMETER *
1.5), pygame.SRCALPHA)
# Create metallic gradient for base
for i in range(int(BUBBLE_RADIUS * 1.2), 0, -1):
ratio = i / (BUBBLE_RADIUS * 1.2)
# Metallic gradient: dark to light to dark
if ratio > 0.5:
shade = 80 + int(100 * (1 - ratio))
else:
shade = 80 + int(100 * (ratio * 2))
pygame.draw.circle(self.base_image, (shade, shade, shade+20),
(self.base_image.get_width()//2,
self.base_image.get_height()//2),
i)

# Add highlight to base


highlight = pygame.Surface((BUBBLE_DIAMETER * 1.5, BUBBLE_DIAMETER * 1.5),
pygame.SRCALPHA)
highlight_radius = BUBBLE_RADIUS * 0.8
highlight_offset = -BUBBLE_RADIUS * 0.3
pygame.draw.circle(highlight, (255, 255, 255, 120),
(self.base_image.get_width()//2 + highlight_offset,
self.base_image.get_height()//2 + highlight_offset),
highlight_radius)
self.base_image.blit(highlight, (0, 0), special_flags=pygame.BLEND_RGB_ADD)

# Add rim
pygame.draw.circle(self.base_image, (60, 60, 80),
(self.base_image.get_width()//2,
self.base_image.get_height()//2),
BUBBLE_RADIUS * 1.2, 2)

self.cannon_length = BUBBLE_DIAMETER * 1.8


self.cannon_width = BUBBLE_RADIUS * 1.2

# Aiming Line with prediction


self.aim_line_length = SCREEN_HEIGHT
self.prediction_points = []

# Animation properties
self.recoil = 0
self.shake = 0
self.shake_offset = pygame.Vector2(0, 0)

def load_bubbles(self):
# Ensure bubbles are different colors if possible
if not self.current_bubble_color:
self.current_bubble_color = random.choice(AVAILABLE_COLORS)

next_color = random.choice(AVAILABLE_COLORS)
while next_color == self.current_bubble_color and len(AVAILABLE_COLORS) >
1:
next_color = random.choice(AVAILABLE_COLORS)

self.next_bubble_color = next_color

def swap_bubbles(self):
self.current_bubble_color, self.next_bubble_color = self.next_bubble_color,
self.current_bubble_color
# Add swap animation effect
self.shake = 5

def aim(self, target_pos):


dx = target_pos[0] - self.pos.x
dy = target_pos[1] - self.pos.y

# Prevent aiming below or horizontal


if dy > -10:
dy = -10

self.aim_angle = math.degrees(math.atan2(-dy, dx))

# Update the prediction path


self.update_prediction_path()

def update_prediction_path(self):
# Calculate the prediction path with bounces
self.prediction_points = []

# Start at shooter position


start_x, start_y = self.pos
rad_angle = math.radians(self.aim_angle)
direction = pygame.Vector2(math.cos(rad_angle), -math.sin(rad_angle))

MAX_BOUNCES = 3
MAX_POINTS = 20
point_count = 0

for _ in range(MAX_BOUNCES + 1):


# Extend line in current direction
for i in range(1, MAX_POINTS):
point = pygame.Vector2(start_x, start_y) + direction * (i * 30)

# Check if we hit a wall


if point.x <= 0 or point.x >= SCREEN_WIDTH:
# Calculate intersection with wall
if point.x <= 0: # Left wall
intersection_y = start_y + direction.y * ((-start_x) /
direction.x)
start_x = 0
else: # Right wall
intersection_y = start_y + direction.y * ((SCREEN_WIDTH -
start_x) / direction.x)
start_x = SCREEN_WIDTH

start_y = intersection_y

# Change direction (bounce)


direction.x *= -1

# Add bounce point


self.prediction_points.append((start_x, start_y))
break

# Check if we hit the ceiling


if point.y <= CEILING_BUFFER:
# We've reached the ceiling, stop prediction
self.prediction_points.append((point.x, CEILING_BUFFER))
return

# Check if we hit any existing bubbles


if 'grid_manager' in globals() and grid_manager is not None:
grid_pos = grid_manager.world_to_grid(point)
if grid_manager.is_valid_grid_pos(grid_pos):
row, col = grid_pos[1], grid_pos[0]
if grid_manager.grid[row][col] is not None:
# We've hit a bubble, stop prediction
self.prediction_points.append((point.x, point.y))
return

# No collision, add point to path


self.prediction_points.append((point.x, point.y))
point_count += 1

if point_count >= MAX_POINTS:


return

def shoot(self):
if not self.current_bubble_color:
return None

# Create the bubble sprite to be shot


shot_bubble = Bubble(self.current_bubble_color, world_pos=self.pos)

# Calculate velocity vector


rad_angle = math.radians(self.aim_angle)
shot_bubble.velocity = pygame.Vector2(math.cos(rad_angle), -
math.sin(rad_angle)) * SHOOT_SPEED

# Apply recoil and shake effects


self.recoil = 10
self.shake = 8

# Prepare next shot


self.current_bubble_color = self.next_bubble_color
self.load_bubbles() # Get new next bubble

# Play shoot sound


if 'sounds' in globals() and 'shoot' in sounds:
sounds['shoot'].play()

return shot_bubble

def update(self):
# Update recoil
if self.recoil > 0:
self.recoil -= 1

# Update shake
if self.shake > 0:
self.shake_offset.x = random.randint(-3, 3)
self.shake_offset.y = random.randint(-3, 3)
self.shake -= 1
else:
self.shake_offset = pygame.Vector2(0, 0)

def draw_aim_line(self, surface):


# Draw improved prediction path
if self.prediction_points:
# Draw translucent line with fading width
if len(self.prediction_points) > 1:
# Draw dots on the path with decreasing size and opacity
for i, point in enumerate(self.prediction_points):
alpha = 200 - i * (180 / len(self.prediction_points))
size = 4 - i * (3 / len(self.prediction_points))
if alpha < 30 or size < 1:
continue

# Create a surface with alpha for the dot


dot_surf = pygame.Surface((size*2, size*2), pygame.SRCALPHA)
pygame.draw.circle(dot_surf, (255, 255, 255, int(alpha)),
(size, size), size)
surface.blit(dot_surf, (point[0]-size, point[1]-size))

# Draw connecting lines with decreasing opacity


for i in range(len(self.prediction_points)-1):
alpha = 150 - i * (150 / len(self.prediction_points))
if alpha < 20:
continue

start = self.prediction_points[i]
end = self.prediction_points[i+1]

# Create a surface with alpha for the line segment


angle = math.atan2(end[1]-start[1], end[0]-start[0])
length = math.sqrt((end[0]-start[0])**2 + (end[1]-start[1])**2)

line_surf = pygame.Surface((length+2, 4), pygame.SRCALPHA)


pygame.draw.line(line_surf, (255, 255, 255, int(alpha)), (0,
2), (length, 2), 2)

# Rotate and position the line


rotated = pygame.transform.rotate(line_surf, math.degrees(-
angle))
rect = rotated.get_rect(center=(
(start[0] + end[0])/2,
(start[1] + end[1])/2
))
surface.blit(rotated, rect)

def draw(self, surface):


# Apply recoil effect
recoil_offset = pygame.Vector2(0, 0)
if self.recoil > 0:
rad_angle = math.radians(self.aim_angle + 180) # Opposite direction
recoil_amount = self.recoil * 0.5
recoil_offset = pygame.Vector2(math.cos(rad_angle), -
math.sin(rad_angle)) * recoil_amount

# Draw base
base_rect = self.base_image.get_rect(center=(self.base_pos.x +
self.shake_offset.x,
self.base_pos.y +
self.shake_offset.y))
surface.blit(self.base_image, base_rect)

# Draw cannon
cannon_pos = self.pos + recoil_offset + self.shake_offset
cannon_end = cannon_pos +
pygame.Vector2(math.cos(math.radians(self.aim_angle)),
-
math.sin(math.radians(self.aim_angle))) * self.cannon_length

# Draw cannon with improved metallic gradient (darker inside, lighter


outside)
for i in range(int(self.cannon_width), 0, -2):
position = i / self.cannon_width
# Metallic gradient
if position > 0.7:
shade = 50 + int(30 * (1 - position))
elif position > 0.3:
shade = 80 + int(60 * (position - 0.3) / 0.4)
else:
shade = 80 - int(30 * (0.3 - position) / 0.3)

color = (shade, shade, shade + 20)


pygame.draw.line(surface, color, cannon_pos, cannon_end, i)

# Draw current bubble at shooter position with enhanced bubble effect


if self.current_bubble_color:
bubble_pos = cannon_pos + recoil_offset * 0.5

# Create bubble with enhanced 3D effect


outer_color = BUBBLE_COLORS[self.current_bubble_color][0]
inner_color = BUBBLE_COLORS[self.current_bubble_color][1]
shine_color = BUBBLE_COLORS[self.current_bubble_color][2]

# Outer edge (darkest)


pygame.draw.circle(surface, outer_color, bubble_pos, BUBBLE_RADIUS)

# Middle gradient (from outer to inner)


for i in range(BUBBLE_RADIUS-1, BUBBLE_RADIUS//2, -1):
ratio = (i - BUBBLE_RADIUS//2) / (BUBBLE_RADIUS - BUBBLE_RADIUS//2)
r = int(outer_color[0] * ratio + inner_color[0] * (1 - ratio))
g = int(outer_color[1] * ratio + inner_color[1] * (1 - ratio))
b = int(outer_color[2] * ratio + inner_color[2] * (1 - ratio))
pygame.draw.circle(surface, (r, g, b), bubble_pos, i)

# Inner gradient (brighter)


for i in range(BUBBLE_RADIUS//2, 0, -1):
ratio = i / (BUBBLE_RADIUS//2)
r = int(inner_color[0] * ratio + shine_color[0] * (1 - ratio))
g = int(inner_color[1] * ratio + shine_color[1] * (1 - ratio))
b = int(inner_color[2] * ratio + shine_color[2] * (1 - ratio))
pygame.draw.circle(surface, (r, g, b), bubble_pos, i)

# Add highlight
highlight_pos = (bubble_pos[0] - BUBBLE_RADIUS * 0.3, bubble_pos[1] -
BUBBLE_RADIUS * 0.3)
pygame.draw.circle(surface, (255, 255, 255, 180), highlight_pos,
BUBBLE_RADIUS * 0.5)

# Add small secondary highlight


small_highlight = (bubble_pos[0] + BUBBLE_RADIUS * 0.3, bubble_pos[1] +
BUBBLE_RADIUS * 0.3)
pygame.draw.circle(surface, (255, 255, 255, 120), small_highlight,
BUBBLE_RADIUS * 0.15)

# Add outline
pygame.draw.circle(surface, (*outer_color, 200), bubble_pos,
BUBBLE_RADIUS, 2)

# Draw next bubble indicator with improved visuals


if self.next_bubble_color:
next_pos = (self.base_pos.x + BUBBLE_DIAMETER*1.5 +
self.shake_offset.x,
self.base_pos.y + self.shake_offset.y)

# Draw indicator background - metallic ring


for i in range(int(BUBBLE_RADIUS * 0.9), int(BUBBLE_RADIUS * 0.7), -1):
ratio = (i - BUBBLE_RADIUS * 0.7) / (BUBBLE_RADIUS * 0.2)
shade = 60 + int(20 * ratio)
pygame.draw.circle(surface, (shade, shade, shade + 10), next_pos,
i)

# Draw next bubble with enhanced 3D effect


outer_color = BUBBLE_COLORS[self.next_bubble_color][0]
inner_color = BUBBLE_COLORS[self.next_bubble_color][1]
shine_color = BUBBLE_COLORS[self.next_bubble_color][2]
bubble_scale = 0.6 # Smaller than the current bubble

# Outer edge
pygame.draw.circle(surface, outer_color, next_pos, BUBBLE_RADIUS *
bubble_scale)

# Middle gradient
for i in range(int(BUBBLE_RADIUS * bubble_scale)-1, int(BUBBLE_RADIUS *
bubble_scale//2), -1):
ratio = (i - BUBBLE_RADIUS * bubble_scale//2) / (BUBBLE_RADIUS *
bubble_scale - BUBBLE_RADIUS * bubble_scale//2)
r = int(outer_color[0] * ratio + inner_color[0] * (1 - ratio))
g = int(outer_color[1] * ratio + inner_color[1] * (1 - ratio))
b = int(outer_color[2] * ratio + inner_color[2] * (1 - ratio))
pygame.draw.circle(surface, (r, g, b), next_pos, i)
# Inner gradient
for i in range(int(BUBBLE_RADIUS * bubble_scale//2), 0, -1):
ratio = i / (BUBBLE_RADIUS * bubble_scale//2)
r = int(inner_color[0] * ratio + shine_color[0] * (1 - ratio))
g = int(inner_color[1] * ratio + shine_color[1] * (1 - ratio))
b = int(inner_color[2] * ratio + shine_color[2] * (1 - ratio))
pygame.draw.circle(surface, (r, g, b), next_pos, i)

# Add highlight
highlight_pos = (next_pos[0] - BUBBLE_RADIUS * 0.2, next_pos[1] -
BUBBLE_RADIUS * 0.2)
pygame.draw.circle(surface, (255, 255, 255, 180), highlight_pos,
BUBBLE_RADIUS * 0.3)

# Add outline
pygame.draw.circle(surface, (*outer_color, 200), next_pos,
BUBBLE_RADIUS * bubble_scale, 1)

# "Next" label
if font:
text = font_small.render("Next", True, (240, 240, 240))
text_rect = text.get_rect(center=(next_pos[0], next_pos[1] +
BUBBLE_RADIUS * 1.2))
shadow_rect = text_rect.copy()
shadow_rect.move_ip(1, 1)

# Add text shadow for better visibility


shadow_text = font_small.render("Next", True, (20, 20, 40))
surface.blit(shadow_text, shadow_rect)
surface.blit(text, text_rect)

# --- UI Elements ---


class Button:
def __init__(self, x, y, width, height, text, action=None, color=(80, 80, 120),
hover_color=(120, 120, 180)):
self.rect = pygame.Rect(x, y, width, height)
self.text = text
self.action = action
self.color = color
self.hover_color = hover_color
self.current_color = color
self.font = pygame.font.Font(None, 32)
self.is_hovered = False

# Animation properties
self.pulse = 0

def update(self, mouse_pos, mouse_clicked):


# Check if mouse is over button
self.is_hovered = self.rect.collidepoint(mouse_pos)

# Update pulsing effect


self.pulse = (self.pulse + 1) % 30

if self.is_hovered:
self.current_color = self.hover_color
if mouse_clicked and self.action:
return self.action()
else:
self.current_color = self.color

return None

def draw(self, surface):


# Draw button with pulsing effect when hovered
pulse_factor = 0
if self.is_hovered:
pulse_factor = math.sin(self.pulse / 15 * math.pi) * 10

# Draw button background with gradient


rect = self.rect.inflate(pulse_factor, pulse_factor)

# Create gradient background


gradient_surf = pygame.Surface((rect.width, rect.height), pygame.SRCALPHA)
for y in range(rect.height):
# Top to bottom gradient
ratio = y / rect.height
color = [
min(255, self.current_color[0] + int(20 * (1-ratio))),
min(255, self.current_color[1] + int(20 * (1-ratio))),
min(255, self.current_color[2] + int(40 * (1-ratio)))
]
pygame.draw.line(gradient_surf, color, (0, y), (rect.width, y))

# Apply rounded corners by drawing on a new surface with alpha


rounded_surf = pygame.Surface((rect.width, rect.height), pygame.SRCALPHA)
pygame.draw.rect(rounded_surf, (255, 255, 255, 255), (0, 0, rect.width,
rect.height),
border_radius=12)

# Blit gradient onto the rounded surface using the white as a mask
gradient_surf = gradient_surf.convert_alpha()
rounded_surf.blit(gradient_surf, (0, 0),
special_flags=pygame.BLEND_RGBA_MULT)

# Draw the button


surface.blit(rounded_surf, rect)

# Add button border


pygame.draw.rect(surface, (240, 240, 255), rect, 2, border_radius=12)

# Add inner glow when hovered


if self.is_hovered:
glow_rect = rect.inflate(-4, -4)
pygame.draw.rect(surface, (200, 200, 255, 30), glow_rect, 3,
border_radius=10)

# Draw text with shadow


text_surf = self.font.render(self.text, True, (240, 240, 250))
text_rect = text_surf.get_rect(center=rect.center)

# Draw shadow
shadow_surf = self.font.render(self.text, True, (20, 20, 40))
shadow_rect = text_rect.copy()
shadow_rect.move_ip(2, 2)
surface.blit(shadow_surf, shadow_rect)

surface.blit(text_surf, text_rect)
class FloatingText:
def __init__(self, text, pos, color=(255, 255, 255), duration=60, speed=1,
font_size=28):
self.font = pygame.font.Font(None, font_size)
self.text = text
self.pos = pygame.Vector2(pos)
self.color = color
self.duration = duration
self.age = 0
self.speed = speed
self.offset = pygame.Vector2(0, 0)

def update(self):
self.age += 1
self.offset.y -= self.speed
return self.age >= self.duration

def draw(self, surface):


alpha = 255
if self.age > self.duration * 0.7:
alpha = int(255 * (1 - (self.age - self.duration * 0.7) /
(self.duration * 0.3)))

# Draw text with glow effect


glow_surf = self.font.render(self.text, True, (255, 255, 255))
glow_surf.set_alpha(alpha // 4)

text_surf = self.font.render(self.text, True, self.color)


text_surf.set_alpha(alpha)

pos = (self.pos.x + self.offset.x, self.pos.y + self.offset.y)

# Draw glow
for offset in [(2, 0), (-2, 0), (0, 2), (0, -2)]:
offset_pos = (pos[0] + offset[0], pos[1] + offset[1])
surface.blit(glow_surf, glow_surf.get_rect(center=offset_pos))

# Draw text
surface.blit(text_surf, text_surf.get_rect(center=pos))

class ScoreDisplay:
def __init__(self, x, y):
self.pos = (x, y)
self.target_score = 0
self.displayed_score = 0
self.font = pygame.font.Font(None, 36)
self.floating_texts = []

def update(self, current_score):


# Smoothly update the displayed score
self.target_score = current_score
if self.displayed_score < self.target_score:
self.displayed_score += max(1, (self.target_score -
self.displayed_score) // 5)

# Update floating text animations


i = 0
while i < len(self.floating_texts):
if self.floating_texts[i].update():
self.floating_texts.pop(i)
else:
i += 1

def add_points(self, points, pos):


# Add floating text animation
text = f"+{points}"
self.floating_texts.append(FloatingText(text, pos, (255, 240, 100), 60,
1.5, 32))

def draw(self, surface):


# Draw score background with glass effect
width = 180
height = 50

# Create glass panel effect


panel = pygame.Surface((width, height), pygame.SRCALPHA)
pygame.draw.rect(panel, (30, 30, 60, 180),
(0, 0, width, height),
border_radius=10)

# Add highlight
pygame.draw.rect(panel, (100, 100, 150, 100),
(2, 2, width-4, height//2-2),
border_radius=10)

# Add border
pygame.draw.rect(panel, (140, 140, 200),
(0, 0, width, height),
2, border_radius=10)

# Position the panel


panel_rect = panel.get_rect(center=self.pos)
surface.blit(panel, panel_rect)

# Draw score text with glow effect


text = f"Score: {self.displayed_score}"

# Glow
glow_surf = self.font.render(text, True, (140, 140, 255))
glow_rect = glow_surf.get_rect(center=self.pos)

for offset in [(1, 1), (-1, -1), (1, -1), (-1, 1)]:
offset_pos = (glow_rect.x + offset[0], glow_rect.y + offset[1])
glow_surf.set_alpha(30)
surface.blit(glow_surf, offset_pos)

# Main text
text_surf = self.font.render(text, True, (240, 240, 255))
text_rect = text_surf.get_rect(center=self.pos)
surface.blit(text_surf, text_rect)

# Draw floating texts


for floating_text in self.floating_texts:
floating_text.draw(surface)

# --- Background Effects ---


class Background:
def __init__(self, width, height):
self.width = width
self.height = height
self.surface = pygame.Surface((width, height))

# Create better gradient background


for y in range(height):
# Gradient from dark blue at top to slightly lighter at bottom
ratio = y / height
r = int(20 + 10 * ratio)
g = int(20 + 10 * ratio)
b = int(50 + 30 * ratio)
pygame.draw.line(self.surface, (r, g, b), (0, y), (width, y))

# Add stars/particles with improved visuals


self.stars = []
# Add more stars for better effect
for _ in range(70):
self.stars.append({
'pos': pygame.Vector2(random.randint(0, width), random.randint(0,
height)),
'size': random.uniform(0.5, 2.5),
'pulse': random.uniform(0, math.pi*2),
'speed': random.uniform(0.02, 0.08),
'color': random.choice([
(200, 200, 255), # Blue-white
(255, 240, 200), # Yellow-white
(200, 255, 200), # Green-white
(255, 200, 200), # Red-white
(255, 255, 255), # Pure white
])
})

def update(self):
# Update star pulsing
for star in self.stars:
star['pulse'] = (star['pulse'] + star['speed']) % (math.pi*2)

def draw(self, surface):


# Draw the gradient background
surface.blit(self.surface, (0, 0))

# Draw stars with improved pulsing effect


for star in self.stars:
size = star['size'] * (0.7 + 0.6 * math.sin(star['pulse']))
brightness = min(255, int(200 + 55 * math.sin(star['pulse'])))

# Base color with adjusted brightness


base_color = star['color']
color = [
min(255, int(c * brightness / 255))
for c in base_color
]

# For bigger stars, draw a glow effect


if size > 1.8:
# Draw glow (larger circle with lower alpha)
glow_surf = pygame.Surface((int(size*6), int(size*6)),
pygame.SRCALPHA)
glow_color = (*color, 30)
pygame.draw.circle(glow_surf, glow_color, (int(size*3),
int(size*3)), int(size*2))
surface.blit(glow_surf, (int(star['pos'].x - size*3),
int(star['pos'].y - size*3)))

# Draw the star itself


pygame.draw.circle(surface, color,
(int(star['pos'].x), int(star['pos'].y)),
size)

# --- Game Functions ---


def start_game():
global game_state, score, shots_fired, can_shoot
game_state = STATE_PLAYING
score = 0
shots_fired = 0
can_shoot = True

# Re-initialize game objects


global grid_manager, shooter, flying_bubbles
grid_manager = GridManager(GRID_ROWS, GRID_COLS, GRID_START_X, GRID_START_Y)
shooter = Shooter(SCREEN_WIDTH // 2, SHOOTER_Y)
flying_bubbles = pygame.sprite.Group()

# Setup initial bubbles


grid_manager.spawn_initial_bubbles(8)

return True

def pause_game():
global game_state
if game_state == STATE_PLAYING:
game_state = STATE_PAUSED
elif game_state == STATE_PAUSED:
game_state = STATE_PLAYING
return True

# --- Game Setup ---


pygame.init()
pygame.mixer.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Bubble Shooter HD")
clock = pygame.time.Clock()

# Load fonts
font = pygame.font.Font(None, 36)
font_large = pygame.font.Font(None, 72)
font_small = pygame.font.Font(None, 24)

# Initialize sounds (safely with fallbacks)


sounds = load_sounds()

# Debug mode for visualizing grid and collision areas


DEBUG_MODE = False

# --- UI Elements ---


score_display = ScoreDisplay(SCREEN_WIDTH - 100, 40)
start_button = Button(SCREEN_WIDTH//2 - 100, SCREEN_HEIGHT//2, 200, 60, "Start
Game", start_game)
pause_button = Button(SCREEN_WIDTH - 70, 40, 60, 40, "||", pause_game)

# --- Game Objects ---


background = Background(SCREEN_WIDTH, SCREEN_HEIGHT)
grid_manager = None
shooter = None
flying_bubbles = None

# --- Game State Variables ---


game_state = STATE_MENU
score = 0
shots_fired = 0
can_shoot = True
last_score = 0 # For detecting score changes

# --- Main Game Loop ---


running = True
while running:
# --- Event Handling ---
mouse_clicked = False
mouse_pos = pygame.mouse.get_pos()

for event in pygame.event.get():


if event.type == pygame.QUIT:
running = False

if event.type == pygame.MOUSEBUTTONDOWN:
mouse_clicked = True

if game_state == STATE_PLAYING:
if event.button == 1 and can_shoot: # Left click
new_shot = shooter.shoot()
if new_shot:
flying_bubbles.add(new_shot)
shots_fired += 1
can_shoot = False
elif event.button == 3: # Right click
shooter.swap_bubbles()

if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and game_state in [STATE_GAME_OVER,
STATE_LEVEL_COMPLETE]:
start_game()

if event.key == pygame.K_p:
pause_game()

if event.key == pygame.K_d:
DEBUG_MODE = not DEBUG_MODE

# --- Updates ---


background.update()

if game_state == STATE_MENU:
# Handle menu button
start_button.update(mouse_pos, mouse_clicked)

elif game_state == STATE_PLAYING:


# Update UI
pause_button.update(mouse_pos, mouse_clicked)

# Aim shooter towards mouse


shooter.aim(mouse_pos)
shooter.update()

# Update flying bubbles


flying_bubbles.update()

# Update grid effects


grid_manager.update_effects()

# Check for score changes to add floating text


if score > last_score:
score_display.add_points(score - last_score, (SCREEN_WIDTH - 100, 70))
last_score = score

# Update score display


score_display.update(score)

# Collision Detection
for shot in list(flying_bubbles): # Use a copy for safe iteration
hit_list = pygame.sprite.spritecollide(shot, grid_manager.bubbles,
False, pygame.sprite.collide_circle)
if hit_list:
snap_successful = grid_manager.snap_bubble(shot)
if snap_successful is not None:
can_shoot = True

# Ceiling Hit Check


elif shot.rect.top <= CEILING_BUFFER:
snap_successful = grid_manager.snap_bubble(shot)
if snap_successful is not None:
can_shoot = True

# Check shots count for ceiling drop


if shots_fired >= SHOTS_UNTIL_DROP:
game_over = grid_manager.add_new_row()
shots_fired = 0

if game_over:
game_state = STATE_GAME_OVER
if 'sounds' in globals() and 'game_over' in sounds:
sounds['game_over'].play()

# Check Game Over


if grid_manager.check_game_over():
game_state = STATE_GAME_OVER
if 'sounds' in globals() and 'game_over' in sounds:
sounds['game_over'].play()

# Check Level Complete (no bubbles left)


if len(grid_manager.bubbles) == 0:
game_state = STATE_LEVEL_COMPLETE
if 'sounds' in globals() and 'level_complete' in sounds:
sounds['level_complete'].play()

elif game_state == STATE_PAUSED:


# Handle pause state
pause_button.update(mouse_pos, mouse_clicked)

# --- Drawing ---


# Draw background
background.draw(screen)

if game_state == STATE_MENU:
# Draw menu screen
title_text = font_large.render("Bubble Shooter HD", True, (240, 240, 255))
title_rect = title_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//3))

# Draw glow effect


for offset in [(3, 3), (-3, -3), (3, -3), (-3, 3)]:
shadow_rect = title_rect.copy()
shadow_rect.move_ip(*offset)
shadow_surf = font_large.render("Bubble Shooter HD", True, (80, 80,
150))
shadow_surf.set_alpha(20)
screen.blit(shadow_surf, shadow_rect)

screen.blit(title_text, title_rect)

# Draw subtitle
subtitle = font.render("Click to Play", True, (200, 200, 255))
subtitle_rect = subtitle.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//3
+ 60))
screen.blit(subtitle, subtitle_rect)

start_button.draw(screen)

# Draw decoration bubbles with animation


time_ms = pygame.time.get_ticks()
for i in range(15):
angle = (time_ms / 2000) + (i * math.pi * 2 / 15)
radius = 300 + math.sin(time_ms / 1000 + i) * 30

x = SCREEN_WIDTH//2 + math.cos(angle) * radius


y = SCREEN_HEIGHT//2 + math.sin(angle) * radius

if 0 <= x <= SCREEN_WIDTH and 0 <= y <= SCREEN_HEIGHT:


color_name = AVAILABLE_COLORS[i % len(AVAILABLE_COLORS)]
outer_color = BUBBLE_COLORS[color_name][0]
inner_color = BUBBLE_COLORS[color_name][1]
shine_color = BUBBLE_COLORS[color_name][2]

size = BUBBLE_RADIUS * 0.7 * (0.8 + 0.2 * math.sin(time_ms / 500 +


i))

# Draw bubble with enhanced 3D effect


pygame.draw.circle(screen, outer_color, (x, y), size)
pygame.draw.circle(screen, inner_color, (x, y), size * 0.7)
pygame.draw.circle(screen, shine_color, (x, y), size * 0.4)

# Add highlight
pygame.draw.circle(screen, (255, 255, 255, 150),
(x - size * 0.3, y - size * 0.3),
size * 0.3)
elif game_state in [STATE_PLAYING, STATE_PAUSED]:
# Draw Grid Bubbles
grid_manager.draw(screen)

# Draw Flying Bubbles


for bubble in flying_bubbles:
bubble.draw(screen)

# Draw Shooter
shooter.draw_aim_line(screen)
shooter.draw(screen)

# Draw UI
score_display.draw(screen)
pause_button.draw(screen)

# Draw shots until drop indicator with improved style


next_row_background = pygame.Surface((150, 30), pygame.SRCALPHA)
pygame.draw.rect(next_row_background, (30, 30, 60, 180),
(0, 0, 150, 30),
border_radius=15)
pygame.draw.rect(next_row_background, (100, 100, 150),
(0, 0, 150, 30),
1, border_radius=15)
screen.blit(next_row_background, (10, 10))

shots_text = font_small.render(f"Next Row: {SHOTS_UNTIL_DROP -


shots_fired}", True, (220, 220, 255))
screen.blit(shots_text, (20, 15))

# Draw pause overlay if paused


if game_state == STATE_PAUSED:
# Semi-transparent overlay
overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT),
pygame.SRCALPHA)
overlay.fill((0, 0, 30, 170))
screen.blit(overlay, (0, 0))

# Create panel
panel_width, panel_height = 400, 200
panel = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
pygame.draw.rect(panel, (40, 40, 80, 230),
(0, 0, panel_width, panel_height),
border_radius=20)
pygame.draw.rect(panel, (120, 120, 180),
(0, 0, panel_width, panel_height),
3, border_radius=20)

# Draw panel background highlight


pygame.draw.rect(panel, (80, 80, 120, 100),
(3, 3, panel_width-6, panel_height//2-3),
border_radius=18)

panel_rect = panel.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2))


screen.blit(panel, panel_rect)

# Pause text with glow


pause_text = font_large.render("PAUSED", True, (240, 240, 255))
pause_rect = pause_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2 - 30))

# Draw glow
glow_text = font_large.render("PAUSED", True, (100, 100, 200))
for offset in [(3, 3), (-3, -3), (3, -3), (-3, 3)]:
glow_rect = pause_rect.copy()
glow_rect.move_ip(*offset)
glow_text.set_alpha(30)
screen.blit(glow_text, glow_rect)

screen.blit(pause_text, pause_rect)

# Resume instructions
resume_text = font.render("Press P to Resume", True, (220, 220, 255))
resume_rect = resume_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2 + 30))
screen.blit(resume_text, resume_rect)

elif game_state == STATE_GAME_OVER:


# Draw game over screen with animated effect
time_val = pygame.time.get_ticks() / 1000
pulse = math.sin(time_val * 3) * 15

# Draw the game background still


grid_manager.draw(screen)

# Semi-transparent overlay with vignette effect


overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
for y in range(SCREEN_HEIGHT):
# Calculate distance from center
dy = y - SCREEN_HEIGHT/2
for x in range(SCREEN_WIDTH):
dx = x - SCREEN_WIDTH/2
dist = math.sqrt(dx*dx + dy*dy) / (SCREEN_WIDTH/2)
alpha = min(230, int(150 + 100 * dist))
overlay.set_at((x, y), (0, 0, 10, alpha))
screen.blit(overlay, (0, 0))

# Create panel
panel_width, panel_height = 500, 300
panel = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
pygame.draw.rect(panel, (40, 20, 40, 230),
(0, 0, panel_width, panel_height),
border_radius=30)
pygame.draw.rect(panel, (180, 60, 60),
(0, 0, panel_width, panel_height),
4, border_radius=30)

# Draw panel inner highlight


pygame.draw.rect(panel, (100, 30, 30, 100),
(4, 4, panel_width-8, panel_height//2-4),
border_radius=27)

panel_rect = panel.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2))


panel_rect.inflate_ip(pulse, pulse)
screen.blit(panel, panel_rect)

# Game over text with glow effect


game_over_text = font_large.render("GAME OVER", True, (255, 100, 100))
text_rect = game_over_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2 - 70))

# Draw glow
glow_text = font_large.render("GAME OVER", True, (150, 50, 50))
for offset in [(3, 3), (-3, -3), (3, -3), (-3, 3)]:
glow_rect = text_rect.copy()
glow_rect.move_ip(*offset)
glow_text.set_alpha(40)
screen.blit(glow_text, glow_rect)

screen.blit(game_over_text, text_rect)

# Score display with glowing numbers


score_text = font.render(f"Final Score: {score}", True, (255, 220, 220))
score_rect = score_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2))

# Draw score glow


glow_score = font.render(f"Final Score: {score}", True, (150, 80, 80))
glow_score.set_alpha(40)
for offset in [(2, 2), (-2, -2)]:
glow_score_rect = score_rect.copy()
glow_score_rect.move_ip(*offset)
screen.blit(glow_score, glow_score_rect)

screen.blit(score_text, score_rect)

# Restart instruction
restart_text = font.render("Press R to Restart", True, (220, 180, 180))
restart_rect = restart_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2 + 70))
screen.blit(restart_text, restart_rect)

elif game_state == STATE_LEVEL_COMPLETE:


# Draw level complete screen with fancy effects
time_val = pygame.time.get_ticks() / 1000
pulse = math.sin(time_val * 2) * 15

# Create particles for celebration


if pygame.time.get_ticks() % 10 == 0:
if 'grid_manager' in globals() and grid_manager:
x = random.randint(50, SCREEN_WIDTH-50)
y = random.randint(50, SCREEN_HEIGHT-50)
color = BUBBLE_COLORS[random.choice(AVAILABLE_COLORS)][0][:3]
grid_manager.particles.add_burst(
(x, y),
color,
count=5,
life_range=(20, 40)
)

# Semi-transparent overlay with light rays


overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
overlay.fill((0, 30, 0, 100))

# Draw light rays


center_x, center_y = SCREEN_WIDTH//2, SCREEN_HEIGHT//2
for angle in range(0, 360, 15):
rad_angle = math.radians(angle + time_val * 10)
end_x = center_x + math.cos(rad_angle) * SCREEN_WIDTH
end_y = center_y + math.sin(rad_angle) * SCREEN_HEIGHT

# Create ray surface


ray_surf = pygame.Surface((SCREEN_WIDTH*2, SCREEN_HEIGHT*2),
pygame.SRCALPHA)

# Draw ray as a triangle


points = [
(center_x, center_y),
(center_x + math.cos(rad_angle-0.1) * SCREEN_WIDTH*1.5,
center_y + math.sin(rad_angle-0.1) * SCREEN_HEIGHT*1.5),
(center_x + math.cos(rad_angle+0.1) * SCREEN_WIDTH*1.5,
center_y + math.sin(rad_angle+0.1) * SCREEN_HEIGHT*1.5)
]

pygame.draw.polygon(ray_surf, (100, 255, 100, 5), points)


overlay.blit(ray_surf, (-SCREEN_WIDTH//2, -SCREEN_HEIGHT//2))

screen.blit(overlay, (0, 0))

# Create panel
panel_width, panel_height = 500, 300
panel = pygame.Surface((panel_width, panel_height), pygame.SRCALPHA)
pygame.draw.rect(panel, (20, 40, 20, 220),
(0, 0, panel_width, panel_height),
border_radius=30)
pygame.draw.rect(panel, (60, 180, 60),
(0, 0, panel_width, panel_height),
4, border_radius=30)

# Draw panel inner highlight


pygame.draw.rect(panel, (30, 100, 30, 100),
(4, 4, panel_width-8, panel_height//2-4),
border_radius=27)

panel_rect = panel.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2))


panel_rect.inflate_ip(pulse, pulse)
screen.blit(panel, panel_rect)

# Level complete text with rainbow effect


hue = (time_val * 50) % 360
color_cycle = pygame.Color(0, 0, 0)
color_cycle.hsva = (hue, 80, 100, 100)

complete_text = font_large.render("LEVEL COMPLETE!", True, color_cycle)


text_rect = complete_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2 - 70))

# Draw glow
glow_text = font_large.render("LEVEL COMPLETE!", True, (100, 255, 100))
for offset in [(3, 3), (-3, -3), (3, -3), (-3, 3)]:
glow_rect = text_rect.copy()
glow_rect.move_ip(*offset)
glow_text.set_alpha(30)
screen.blit(glow_text, glow_rect)
screen.blit(complete_text, text_rect)

# Score display with stars


score_text = font.render(f"Score: {score}", True, (220, 255, 220))
score_rect = score_text.get_rect(center=(SCREEN_WIDTH//2,
SCREEN_HEIGHT//2))
screen.blit(score_text, score_rect)

# Draw stars based on score


stars = min(5, max(1, score // 200))
star_width = 30
star_spacing = 40
star_total_width = stars * star_spacing
star_start_x = SCREEN_WIDTH//2 - star_total_width//2 + star_spacing//2

for i in range(stars):
x = star_start_x + i * star_spacing
y = SCREEN_HEIGHT//2 + 40

# Draw a star
star_points = []
for j in range(10):
angle = math.pi/2 + j * math.pi/5
radius = star_width//2 if j % 2 == 0 else star_width//4
star_points.append((
x + radius * math.cos(angle),
y + radius * math.sin(angle)
))

# Gold color with pulsing effect


glow = 1 + 0.2 * math.sin(time_val * 5 + i)
star_color = (min(255, int(220 * glow)),
min(255, int(180 * glow)),
min(255, int(50 * glow)))

pygame.draw.polygon(screen, star_color, star_points)

# Add outline
pygame.draw.polygon(screen, (100, 80, 0), star_points, 1)

# Next level instruction


next_text = font.render("Press R for Next Level", True, (180, 255, 180))
next_rect = next_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 +
80))
screen.blit(next_text, next_rect)

# Update Display
pygame.display.flip()

# Frame Rate Control


clock.tick(60)

# --- Quit Pygame ---


pygame.quit()
sys.exit()

You might also like