泡泡龙 - 高清高速版3色 运行: python bubble_shooter.py 依赖: pip install pygame 操作: 鼠标移动瞄准 | 左键发射 | R 重开 | ESC 退出 import os import sys import random import math import pygame # 配置 W, H 700, 800 FPS 144 BUBBLE_RADIUS 24 BUBBLE_DIAM BUBBLE_RADIUS * 2 COLS 16 ROWS 18 GRID_TOP 100 GRID_LEFT (W - COLS * BUBBLE_DIAM) // 2 SHOOTER_X W // 2 SHOOTER_Y H - 80 SHOOTER_RADIUS 32 BUBBLE_SPEED 24 GRAVITY_FALL 0.6 AIM_STEPS 120 AIM_STEP_SIZE 8 # ⚡ 精简到 3 种颜色红、蓝、绿 COLORS [ (255, 90, 120), # 红 (90, 180, 255), # 蓝 (110, 240, 140), # 绿 ] COLOR_DARK [ (180, 50, 80), (50, 120, 190), (60, 170, 80), ] # 字体 def find_font(candidates): for p in candidates: if os.path.exists(p): return p return None def get_cn_font(size): path find_font([ C:/Windows/Fonts/msyh.ttc, C:/Windows/Fonts/simhei.ttf, C:/Windows/Fonts/simsun.ttc, /System/Library/Fonts/PingFang.ttc, /usr/share/fonts/truetype/wqy/wqy-microhei.ttc, /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc, ]) if path: return pygame.font.Font(path, size) return pygame.font.Font(None, size) # 泡泡缓存 def build_bubble_surfaces(): cache {} for ci in range(len(COLORS)): color COLORS[ci] dark COLOR_DARK[ci] size BUBBLE_RADIUS * 2 12 center size // 2 surf pygame.Surface((size, size), pygame.SRCALPHA) pygame.draw.circle(surf, (*color, 50), (center, center), BUBBLE_RADIUS 5) pygame.draw.circle(surf, color, (center, center), BUBBLE_RADIUS) pygame.draw.circle(surf, dark, (center, center), BUBBLE_RADIUS, 4) pygame.draw.circle( surf, (255, 255, 255, 180), (int(center - BUBBLE_RADIUS * 0.35), int(center - BUBBLE_RADIUS * 0.4)), max(3, BUBBLE_RADIUS // 3) ) pygame.draw.circle( surf, (255, 255, 255, 120), (int(center BUBBLE_RADIUS * 0.3), int(center BUBBLE_RADIUS * 0.25)), max(2, BUBBLE_RADIUS // 5) ) cache[ci] surf cache[small] {} for ci in range(len(COLORS)): s pygame.transform.smoothscale( cache[ci], (int(BUBBLE_RADIUS * 1.6), int(BUBBLE_RADIUS * 1.6)) ) cache[small][ci] s return cache # 粒子 class Particle: __slots__ (x, y, vx, vy, life, color, size, gravity) def __init__(self, x, y, color, speed6, size6): angle random.random() * math.pi * 2 spd random.random() * speed 2 self.x x self.y y self.vx math.cos(angle) * spd self.vy math.sin(angle) * spd - 2 self.life 1.0 self.color color self.size random.random() * size 2 self.gravity 0.4 def update(self): self.x self.vx self.y self.vy self.vy self.gravity self.vx * 0.96 self.life - 0.035 def draw(self, surf): if self.life 0: return alpha int(self.life * 255) r max(1, int(self.size * self.life)) s pygame.Surface((r * 2, r * 2), pygame.SRCALPHA) pygame.draw.circle(s, (*self.color, alpha), (r, r), r) surf.blit(s, (self.x - r, self.y - r)) # 下落泡泡 class FallingBubble: __slots__ (x, y, vx, vy, color_idx) def __init__(self, x, y, color_idx): self.x x self.y y self.vx random.uniform(-3, 3) self.vy random.uniform(-4, -1) self.color_idx color_idx def update(self): self.x self.vx self.y self.vy self.vy GRAVITY_FALL self.vx * 0.99 def draw(self, surf, cache): s cache[self.color_idx] surf.blit(s, (self.x - s.get_width() // 2, self.y - s.get_height() // 2)) # 游戏 class BubbleShooter: def __init__(self): pygame.init() self.screen pygame.display.set_mode((W, H)) pygame.display.set_caption(泡泡龙 · 3色版) self.clock pygame.time.Clock() self.font_sm get_cn_font(16) self.font_md get_cn_font(22) self.font_lg get_cn_font(36) self.font_xl get_cn_font(56) self.bubble_cache build_bubble_surfaces() self._build_static_bg() self.reset() def _build_static_bg(self): surf pygame.Surface((W, H)) for y in range(H): ratio y / H r int(15 10 * ratio) g int(20 15 * ratio) b int(45 35 * ratio) pygame.draw.line(surf, (r, g, b), (0, y), (W, y)) for i, alpha in enumerate([80, 50, 25]): s pygame.Surface((W, 6), pygame.SRCALPHA) s.fill((100, 180, 255, alpha)) surf.blit(s, (0, GRID_TOP - 3 i * 2)) self._static_bg surf def reset(self): self.grid [[None] * COLS for _ in range(ROWS)] self.score 0 self.best_score getattr(self, best_score, 0) self.game_over False self.win False self.frame 0 self.particles [] self.falling_bubbles [] self.shots 0 self.combo 0 self.current_color random.randrange(len(COLORS)) self.next_color random.randrange(len(COLORS)) self.shooting_bubble None self.aim_angle -math.pi / 2 # 初始泡泡 self.initial_rows 6 for r in range(self.initial_rows): for c in range(COLS): if random.random() 0.12: continue self.grid[r][c] random.randrange(len(COLORS)) self.ensure_min_colors(3) def ensure_min_colors(self, min_count): colors_present set() for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is not None: colors_present.add(self.grid[r][c]) if len(colors_present) min_count: for r in range(self.initial_rows): for c in range(COLS): if self.grid[r][c] is not None and random.random() 0.5: self.grid[r][c] random.randrange(len(COLORS)) def grid_to_pixel(self, row, col): x GRID_LEFT col * BUBBLE_DIAM BUBBLE_RADIUS if row % 2 1: x BUBBLE_RADIUS y GRID_TOP row * BUBBLE_DIAM BUBBLE_RADIUS return x, y def pixel_to_grid(self, x, y): best_r, best_c 0, 0 best_dist 1e9 for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is not None: continue gx, gy self.grid_to_pixel(r, c) d (gx - x) ** 2 (gy - y) ** 2 if d best_dist: best_dist d best_r, best_c r, c return best_r, best_c def get_neighbors(self, row, col): if row % 2 0: offsets [(-1, -1), (-1, 0), (0, -1), (0, 1), (1, -1), (1, 0)] else: offsets [(-1, 0), (-1, 1), (0, -1), (0, 1), (1, 0), (1, 1)] result [] for dr, dc in offsets: nr, nc row dr, col dc if 0 nr ROWS and 0 nc COLS: result.append((nr, nc)) return result def shoot(self): if self.shooting_bubble is not None or self.game_over: return angle self.aim_angle vx math.cos(angle) * BUBBLE_SPEED vy math.sin(angle) * BUBBLE_SPEED self.shooting_bubble { x: SHOOTER_X, y: SHOOTER_Y, vx: vx, vy: vy, color: self.current_color, } self.shots 1 def update_shooting_bubble(self): b self.shooting_bubble if b is None: return steps max(1, int(BUBBLE_SPEED / 8)) for _ in range(steps): b[x] b[vx] / steps b[y] b[vy] / steps if b[x] - BUBBLE_RADIUS 0: b[x] BUBBLE_RADIUS b[vx] -b[vx] elif b[x] BUBBLE_RADIUS W: b[x] W - BUBBLE_RADIUS b[vx] -b[vx] if b[y] - BUBBLE_RADIUS GRID_TOP: self.place_bubble(b[x], GRID_TOP BUBBLE_RADIUS, b[color]) self.shooting_bubble None return for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is None: continue gx, gy self.grid_to_pixel(r, c) dx gx - b[x] dy gy - b[y] if dx * dx dy * dy (BUBBLE_DIAM - 6) ** 2: self.place_bubble(b[x], b[y], b[color]) self.shooting_bubble None return if b[y] H: self.shooting_bubble None def place_bubble(self, x, y, color_idx): row, col self.pixel_to_grid(x, y) if self.grid[row][col] is not None: best_dist 1e9 best_r, best_c None, None for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is None: gx, gy self.grid_to_pixel(r, c) d (gx - x) ** 2 (gy - y) ** 2 if d best_dist: best_dist d best_r, best_c r, c if best_r is None: return row, col best_r, best_c self.grid[row][col] color_idx matched self.find_matches(row, col, color_idx) if len(matched) 3: self.combo 1 base_score len(matched) * 10 combo_bonus self.combo * 5 gained base_score combo_bonus self.score gained for (r, c) in matched: gx, gy self.grid_to_pixel(r, c) color COLORS[self.grid[r][c]] for _ in range(5): self.particles.append(Particle(gx, gy, color, speed7, size6)) self.grid[r][c] None self.drop_floating() if self.is_cleared(): self.win True self.game_over True else: self.combo 0 if not self.game_over: for r in range(ROWS - 1, ROWS - 3, -1): for c in range(COLS): if self.grid[r][c] is not None: self.game_over True break if self.game_over: break self.current_color self.next_color self.next_color random.randrange(len(COLORS)) def find_matches(self, row, col, color_idx): visited set() queue [(row, col)] visited.add((row, col)) matched [] while queue: r, c queue.pop(0) matched.append((r, c)) for nr, nc in self.get_neighbors(r, c): if (nr, nc) in visited: continue if self.grid[nr][nc] color_idx: visited.add((nr, nc)) queue.append((nr, nc)) return matched def drop_floating(self): connected set() queue [] for c in range(COLS): if self.grid[0][c] is not None: connected.add((0, c)) queue.append((0, c)) while queue: r, c queue.pop(0) for nr, nc in self.get_neighbors(r, c): if (nr, nc) in connected: continue if self.grid[nr][nc] is not None: connected.add((nr, nc)) queue.append((nr, nc)) for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is not None and (r, c) not in connected: gx, gy self.grid_to_pixel(r, c) self.falling_bubbles.append(FallingBubble(gx, gy, self.grid[r][c])) self.grid[r][c] None def is_cleared(self): for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is not None: return False return True def update(self): self.frame 1 self.update_shooting_bubble() for fb in self.falling_bubbles[:]: fb.update() if fb.y H 50: self.falling_bubbles.remove(fb) for p in self.particles[:]: p.update() if p.life 0: self.particles.remove(p) def draw(self): self.screen.blit(self._static_bg, (0, 0)) pygame.draw.line(self.screen, (80, 130, 220), (2, GRID_TOP), (2, H), 3) pygame.draw.line(self.screen, (80, 130, 220), (W - 2, GRID_TOP), (W - 2, H), 3) # ⚡ 网格泡泡不再晃动 for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is None: continue gx, gy self.grid_to_pixel(r, c) s self.bubble_cache[self.grid[r][c]] self.screen.blit(s, (gx - s.get_width() // 2, gy - s.get_height() // 2)) for fb in self.falling_bubbles: fb.draw(self.screen, self.bubble_cache) for p in self.particles: p.draw(self.screen) if self.shooting_bubble: b self.shooting_bubble s self.bubble_cache[b[color]] self.screen.blit(s, (b[x] - s.get_width() // 2, b[y] - s.get_height() // 2)) if not self.game_over and self.shooting_bubble is None: self.draw_aim_line() self.draw_shooter() self.draw_ui() if self.game_over: self.draw_game_over() def draw_aim_line(self): angle self.aim_angle vx math.cos(angle) vy math.sin(angle) x, y SHOOTER_X, SHOOTER_Y color COLORS[self.current_color] for i in range(AIM_STEPS): x vx * AIM_STEP_SIZE y vy * AIM_STEP_SIZE if x - BUBBLE_RADIUS 0: x BUBBLE_RADIUS vx -vx elif x BUBBLE_RADIUS W: x W - BUBBLE_RADIUS vx -vx if y GRID_TOP BUBBLE_RADIUS: break hit False for r in range(ROWS): for c in range(COLS): if self.grid[r][c] is None: continue gx, gy self.grid_to_pixel(r, c) dx gx - x dy gy - y if dx * dx dy * dy (BUBBLE_DIAM - 6) ** 2: hit True break if hit: break if hit: break if i % 3 0: alpha max(40, 220 - i * 2) r 4 s pygame.Surface((r * 2, r * 2), pygame.SRCALPHA) pygame.draw.circle(s, (*color, alpha), (r, r), r) self.screen.blit(s, (x - r, y - r)) def draw_shooter(self): pygame.draw.circle(self.screen, (40, 55, 100), (SHOOTER_X, SHOOTER_Y 14), 42) pygame.draw.circle(self.screen, (80, 120, 200), (SHOOTER_X, SHOOTER_Y 14), 42, 4) angle self.aim_angle end_x SHOOTER_X math.cos(angle) * SHOOTER_RADIUS * 1.9 end_y SHOOTER_Y math.sin(angle) * SHOOTER_RADIUS * 1.9 pygame.draw.line(self.screen, (50, 70, 130), (SHOOTER_X, SHOOTER_Y), (end_x, end_y), 16) pygame.draw.line(self.screen, (120, 160, 250), (SHOOTER_X, SHOOTER_Y), (end_x, end_y), 10) if self.shooting_bubble is None: s self.bubble_cache[self.current_color] self.screen.blit(s, (SHOOTER_X - s.get_width() // 2, SHOOTER_Y - s.get_height() // 2)) next_x SHOOTER_X 80 next_y SHOOTER_Y 20 pygame.draw.circle(self.screen, (30, 40, 70), (next_x, next_y), BUBBLE_RADIUS 6) pygame.draw.circle(self.screen, (80, 110, 180), (next_x, next_y), BUBBLE_RADIUS 6, 2) s self.bubble_cache[small][self.next_color] self.screen.blit(s, (next_x - s.get_width() // 2, next_y - s.get_height() // 2)) def draw_ui(self): score_text self.font_lg.render(f分数: {self.score}, True, (230, 245, 255)) self.screen.blit(score_text, (25, 20)) best_text self.font_sm.render(f最高: {self.best_score}, True, (150, 180, 220)) self.screen.blit(best_text, (25, 65)) tip self.font_sm.render(下一个, True, (150, 180, 220)) self.screen.blit(tip, (SHOOTER_X 55, SHOOTER_Y - 16)) if self.combo 1: combo_text self.font_lg.render(f连击 x{self.combo}, True, (255, 200, 80)) self.screen.blit(combo_text, (W - combo_text.get_width() - 25, 20)) fps_text self.font_sm.render(fFPS: {int(self.clock.get_fps())}, True, (100, 130, 180)) self.screen.blit(fps_text, (W - fps_text.get_width() - 25, 65)) hint self.font_sm.render( 鼠标瞄准 | 左键发射 | R 重开 | ESC 退出, True, (130, 150, 190) ) self.screen.blit(hint, (W // 2 - hint.get_width() // 2, H - 25)) def draw_game_over(self): overlay pygame.Surface((W, H), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) self.screen.blit(overlay, (0, 0)) if self.win: title_text 通关 title_color (110, 240, 140) else: title_text 游戏结束 title_color (255, 100, 130) title self.font_xl.render(title_text, True, title_color) self.screen.blit(title, (W // 2 - title.get_width() // 2, H // 2 - 120)) score_text self.font_lg.render(f最终得分: {self.score}, True, (255, 220, 100)) self.screen.blit(score_text, (W // 2 - score_text.get_width() // 2, H // 2)) shots_text self.font_md.render(f发射次数: {self.shots}, True, (180, 200, 230)) self.screen.blit(shots_text, (W // 2 - shots_text.get_width() // 2, H // 2 55)) hint self.font_md.render(按 R 重新开始, True, (200, 220, 255)) self.screen.blit(hint, (W // 2 - hint.get_width() // 2, H // 2 130)) def run(self): running True while running: for e in pygame.event.get(): if e.type pygame.QUIT: running False elif e.type pygame.KEYDOWN: if e.key pygame.K_ESCAPE: running False elif e.key pygame.K_r: self.reset() elif e.type pygame.MOUSEMOTION: mx, my e.pos dx mx - SHOOTER_X dy my - SHOOTER_Y angle math.atan2(dy, dx) if angle -0.15: angle -0.15 elif angle -math.pi 0.15: angle -math.pi 0.15 self.aim_angle angle elif e.type pygame.MOUSEBUTTONDOWN: if e.button 1: self.shoot() self.update() self.draw() pygame.display.flip() self.clock.tick(FPS) pygame.quit() sys.exit() if __name__ __main__: print( * 50) print( 泡泡龙 · 3色版) print( * 50) print( 鼠标移动瞄准 | 左键发射) print( R 重开 | ESC 退出) print( * 50) BubbleShooter().run()