From d8ce8730268f4b69720f7024cde85051b5603bc4 Mon Sep 17 00:00:00 2001 From: guanjihuan Date: Mon, 13 Apr 2026 10:45:58 +0800 Subject: [PATCH] Create ball_games_with_pygame_version2.py --- .../ball_games_with_pygame_version2.py | 627 ++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 2019.10.27_ball_games_with_pygame/ball_games_with_pygame_version2.py diff --git a/2019.10.27_ball_games_with_pygame/ball_games_with_pygame_version2.py b/2019.10.27_ball_games_with_pygame/ball_games_with_pygame_version2.py new file mode 100644 index 0000000..38dc110 --- /dev/null +++ b/2019.10.27_ball_games_with_pygame/ball_games_with_pygame_version2.py @@ -0,0 +1,627 @@ +""" +This code is supported by the website: https://www.guanjihuan.com +The newest version of this code is on the web page: https://www.guanjihuan.com/archives/703 +""" + +import pygame +import random +import math +import sys + +# 初始化pygame +pygame.init() + +# 颜色定义 +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +RED = (255, 0, 0) +BLUE = (0, 0, 255) +GREEN = (0, 255, 0) +YELLOW = (255, 255, 0) +PURPLE = (128, 0, 128) +ORANGE = (255, 165, 0) +CYAN = (0, 255, 255) + +# 游戏设置 +SCREEN_WIDTH = 800 +SCREEN_HEIGHT = 600 +WORLD_WIDTH = 1400 +WORLD_HEIGHT = 1050 +FPS = 60 + +# 创建窗口 +screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) +pygame.display.set_caption("球球大作战") +clock = pygame.time.Clock() + +# 字体 - 尝试加载系统中文字体 +def get_font(size): + # Windows系统字体路径 - 尝试更多常见字体 + windows_fonts = [ + "C:/Windows/Fonts/simhei.ttf", # 黑体 + "C:/Windows/Fonts/msyh.ttc", # 微软雅黑 + "C:/Windows/Fonts/msyhbd.ttc", # 微软雅黑粗体 + "C:/Windows/Fonts/msyhl.ttc", # 微软雅黑细体 + "C:/Windows/Fonts/simsun.ttc", # 宋体 + "C:/Windows/Fonts/simkai.ttf", # 楷体 + "C:/Windows/Fonts/arial.ttf", # Arial + ] + # 只文件名尝试 + font_names = [ + "simhei.ttf", + "msyh.ttc", + "simsun.ttc", + "arial.ttf", + ] + # 先试完整路径 + for font_path in windows_fonts: + try: + return pygame.font.Font(font_path, size) + except: + continue + # 再试文件名 + for font_name in font_names: + try: + return pygame.font.Font(font_name, size) + except: + continue + # 如果找不到中文字体,使用默认字体 + print("警告:未找到中文字体") + return pygame.font.Font(None, size) + +font = get_font(36) +small_font = get_font(24) + + +class Cell: + """单个球体(细胞)类""" + def __init__(self, x, y, radius, color, is_player=False): + self.x = x + self.y = y + self.radius = radius + self.color = color + self.is_player = is_player + self.mass = math.pi * radius * radius + + def update_mass(self): + """根据质量更新半径""" + self.radius = math.sqrt(self.mass / math.pi) + + def add_mass(self, mass): + """增加质量""" + self.mass += mass + self.update_mass() + + def contains(self, other): + """检查是否包含另一个球(可以吃掉它) + 覆盖约80%就能吃,不需要完全覆盖 + """ + dist = math.hypot(self.x - other.x, self.y - other.y) + # 80%覆盖判定:dist + 0.8 * other.radius < self.radius + return dist + 0.8 * other.radius < self.radius and self.mass > other.mass + + def draw(self, surface, camera_x, camera_y, zoom=1): + """绘制球体 + camera_x, camera_y: 摄像头跟踪的世界坐标中心点 + zoom: 缩放系数,<1 表示缩小显示 + """ + # 计算屏幕坐标:以摄像头为中心缩放 + screen_x = int(SCREEN_WIDTH // 2 + (self.x - camera_x) * zoom) + screen_y = int(SCREEN_HEIGHT // 2 + (self.y - camera_y) * zoom) + # 缩放半径 + scaled_radius = int(self.radius * zoom) + + # 只有在屏幕可见范围内才绘制 + if (-scaled_radius < screen_x < SCREEN_WIDTH + scaled_radius and + -scaled_radius < screen_y < SCREEN_HEIGHT + scaled_radius): + pygame.draw.circle(surface, self.color, (screen_x, screen_y), scaled_radius) + # 绘制边框 + pygame.draw.circle(surface, BLACK, (screen_x, screen_y), scaled_radius, 2) + + # 如果是玩家,显示分数 + if self.is_player: + text = small_font.render(str(int(self.mass)), True, WHITE) + text_rect = text.get_rect(center=(screen_x, screen_y)) + surface.blit(text, text_rect) + + +class Food: + """食物类""" + def __init__(self, x, y): + self.x = x + self.y = y + self.radius = 6 + self.mass = 25 # 增大食物的质量值 + self.color = random.choice([RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE, CYAN]) + + def draw(self, surface, camera_x, camera_y, zoom=1): + """绘制食物,支持缩放""" + screen_x = int(SCREEN_WIDTH // 2 + (self.x - camera_x) * zoom) + screen_y = int(SCREEN_HEIGHT // 2 + (self.y - camera_y) * zoom) + scaled_radius = int(self.radius * zoom) + if (-scaled_radius < screen_x < SCREEN_WIDTH + scaled_radius and + -scaled_radius < screen_y < SCREEN_HEIGHT + scaled_radius): + pygame.draw.circle(surface, self.color, (screen_x, screen_y), scaled_radius) + + +class Game: + """游戏主类""" + def __init__(self): + self.player_cells = [] + self.ai_cells = [] + self.foods = [] + self.score = 0 + self.game_over = False + self.paused = False + self.final_mass = 0 + + # 初始化玩家 + start_x = WORLD_WIDTH // 2 + start_y = WORLD_HEIGHT // 2 + player = Cell(start_x, start_y, 20, BLUE, is_player=True) + self.player_cells.append(player) + + # 生成初始食物 - 更多更密 + self.generate_food(200) + + # 生成初始AI球球 + self.generate_ai(5) + + def generate_food(self, count): + """生成食物""" + for _ in range(count): + x = random.randint(10, WORLD_WIDTH - 10) + y = random.randint(10, WORLD_HEIGHT - 10) + self.foods.append(Food(x, y)) + + def generate_ai(self, count): + """生成AI球球""" + for _ in range(count): + x = random.randint(50, WORLD_WIDTH - 50) + y = random.randint(50, WORLD_HEIGHT - 50) + radius = random.randint(15, 30) + color = (random.randint(100, 255), random.randint(100, 255), random.randint(100, 255)) + self.ai_cells.append(Cell(x, y, radius, color)) + + def update_player(self, mouse_x, mouse_y): + """更新玩家位置 - 所有玩家的球都跟随鼠标移动""" + if not self.player_cells: + return + + # 摄像头位置基于玩家重心(最大的球) + main_player = self.get_main_player() + if not main_player: + return + + # 计算世界坐标中的目标位置 + camera_x = main_player.x - SCREEN_WIDTH // 2 + camera_y = main_player.y - SCREEN_HEIGHT // 2 + world_mouse_x = mouse_x + camera_x + world_mouse_y = mouse_y + camera_y + + # 是否按住加速键 + mouse_buttons = pygame.mouse.get_pressed() + boost_active = mouse_buttons[0] # 左键加速 + + # 更新所有玩家球 + for player in self.player_cells: + # 向鼠标方向移动 - 整体加速,大球也不会太慢 + speed = 28000 / player.mass + 45 # 提高基础速度和整体速度 + + # 如果按住鼠标左键,激活加速(4倍速),不消耗质量 + if boost_active: + speed *= 4 + + dx = world_mouse_x - player.x + dy = world_mouse_y - player.y + dist = math.hypot(dx, dy) + + if dist > 5: + dx /= dist + dy /= dist + player.x += dx * speed / FPS + player.y += dy * speed / FPS + + # 边界限制:允许球最多一半超出地图,这样能吃到角落的食物 + player.x = max(-player.radius/2, min(WORLD_WIDTH + player.radius/2, player.x)) + player.y = max(-player.radius/2, min(WORLD_HEIGHT + player.radius/2, player.y)) + + # 自动合体:检查所有小球,如果接近主球就合体 + self.merge_cells() + + def merge_cells(self): + """自动合体 - 分裂的球靠近时会合并回主球""" + if len(self.player_cells) <= 1: + return + + main_player = self.get_main_player() + if not main_player: + return + + # 检查其他小球 + to_merge = [] + for i, cell in enumerate(self.player_cells): + if cell is main_player: + continue + + # 计算距离 + dist = math.hypot(cell.x - main_player.x, cell.y - main_player.y) + + # 80%覆盖就能合体(和吞噬规则一致 + if dist + 0.8 * cell.radius < main_player.radius: + to_merge.append(i) + main_player.add_mass(cell.mass) + + # 移除合并的球(倒序删除避免索引问题) + for i in reversed(to_merge): + del self.player_cells[i] + + def update_ai(self): + """更新AI移动""" + for ai in self.ai_cells: + # 简单AI:向附近较小的球移动,避开较大的球 + target = None + closest_dist = float('inf') + + # 寻找最近的较小目标 + for food in self.foods: + dist = math.hypot(ai.x - food.x, ai.y - food.y) + if dist < closest_dist: + closest_dist = dist + target = (food.x, food.y) + + # 检查玩家 + for player in self.player_cells: + dist = math.hypot(ai.x - player.x, ai.y - player.y) + if ai.mass > player.mass * 1.2: + if dist < closest_dist: + closest_dist = dist + target = (player.x, player.y) + elif player.mass > ai.mass * 1.2 and dist < 200: + # 逃离大球 + dx = ai.x - player.x + dy = ai.y - player.y + dist = math.hypot(dx, dy) + if dist > 0: + dx /= dist + dy /= dist + ai.x += dx * (5 / FPS) * 8 + ai.y += dy * (5 / FPS) * 8 + target = None + + # 向目标移动 + if target: + speed = 18000 / ai.mass + 30 # 提高AI速度,加快游戏节奏 + dx = target[0] - ai.x + dy = target[1] - ai.y + dist = math.hypot(dx, dy) + if dist > 5: + dx /= dist + dy /= dist + ai.x += dx * speed / FPS + ai.y += dy * speed / FPS + + # 边界限制:允许球最多一半超出地图 + ai.x = max(-ai.radius/2, min(WORLD_WIDTH + ai.radius/2, ai.x)) + ai.y = max(-ai.radius/2, min(WORLD_HEIGHT + ai.radius/2, ai.y)) + + def check_collisions(self): + """检查碰撞 - 吃东西和互相吞噬""" + # 玩家吃食物 + for player in self.player_cells: + to_remove = [] + for i, food in enumerate(self.foods): + if player.contains(Cell(food.x, food.y, food.radius, None)): + player.add_mass(food.mass) + to_remove.append(i) + self.score += food.mass + + # 移除被吃的食物 + for i in reversed(to_remove): + del self.foods[i] + + # AI吃食物 + for ai in self.ai_cells: + to_remove = [] + for i, food in enumerate(self.foods): + if ai.contains(Cell(food.x, food.y, food.radius, None)): + ai.add_mass(food.mass) + to_remove.append(i) + for i in reversed(to_remove): + del self.foods[i] + + # AI吞噬玩家 + for ai in self.ai_cells[:]: + for player in self.player_cells[:]: + if ai.contains(player): + ai.add_mass(player.mass) + # 在移除前保存最终质量 + if len(self.player_cells) == 1: + self.final_mass = player.mass + self.player_cells.remove(player) + elif player.contains(ai): + player.add_mass(ai.mass) + self.ai_cells.remove(ai) + self.score += ai.mass + break + + # AI之间互相吞噬 + for ai1 in self.ai_cells[:]: + for ai2 in self.ai_cells[:]: + if ai1 != ai2 and ai1.contains(ai2): + ai1.add_mass(ai2.mass) + self.ai_cells.remove(ai2) + break + + # 补充食物 - 更多更密 + if len(self.foods) < 300: + self.generate_food(20) + + # 动态补充AI:玩家质量越高,生成越多对手 + main_player = self.get_main_player() + if main_player: + # 限制最多35个AI,平衡挑战性和流畅性,减少卡顿 + target_ai_count = min(35, 15 + int(main_player.mass / 150)) + else: + target_ai_count = 12 + + # 如果AI数量少于目标,持续补充 + if len(self.ai_cells) < target_ai_count: + self.generate_ai(1) + + # 质量衰减:所有球按比例减小体积,越大减少越多 + self.mass_decay() + + def get_main_player(self): + """获取玩家最大的球""" + if not self.player_cells: + return None + return max(self.player_cells, key=lambda c: c.mass) + + def draw(self): + """绘制游戏,支持自动缩放""" + # 背景 + screen.fill((240, 240, 240)) + + # 获取主玩家 + main_player = self.get_main_player() + if main_player: + # 摄像头始终跟随主玩家中心点 + camera_x = main_player.x + camera_y = main_player.y + + # 自动缩放计算:主球显示直径最大固定为屏幕宽度的一半 + # 达到后,主球显示大小不再增大,实际质量继续增长,其他球跟着缩小 + max_display_radius = SCREEN_WIDTH / 4 # 半径 = 1/4宽度 → 直径 = 1/2宽度,正好是屏幕一半 + if main_player.radius > max_display_radius: + # 需要缩小整个画面,保持主球显示大小不超 + zoom = max_display_radius / main_player.radius + else: + # 还没到最大,正常显示 + zoom = 1.0 + + # 最小缩放到0.15,避免缩得太小 + zoom = max(0.15, zoom) + else: + camera_x = WORLD_WIDTH // 2 + camera_y = WORLD_HEIGHT // 2 + zoom = 1.0 + + # 绘制网格背景(网格也随缩放调整) + grid_size = int(40 * zoom) + if grid_size < 5: + grid_size = 5 + # 计算网格起始位置 + # 因为现在 camera 在中心,所以起始点计算方式改变 + start_x = int((-camera_x * zoom) % grid_size) + start_y = int((-camera_y * zoom) % grid_size) + start_x += SCREEN_WIDTH // 2 + start_y += SCREEN_HEIGHT // 2 + for x in range(start_x - grid_size, SCREEN_WIDTH, grid_size): + pygame.draw.line(screen, (220, 220, 220), (x, 0), (x, SCREEN_HEIGHT), 1) + for y in range(start_y - grid_size, SCREEN_HEIGHT, grid_size): + pygame.draw.line(screen, (220, 220, 220), (0, y), (SCREEN_WIDTH, y), 1) + + # 绘制食物(所有物体都按zoom缩放) + for food in self.foods: + food.draw(screen, camera_x, camera_y, zoom) + + # 绘制AI + for ai in self.ai_cells: + ai.draw(screen, camera_x, camera_y, zoom) + + # 绘制玩家 + for player in self.player_cells: + player.draw(screen, camera_x, camera_y, zoom) + + # 绘制UI(UI不缩放,始终固定在屏幕) + if main_player: + mass_text = font.render(f"质量: {int(main_player.mass)}", True, BLACK) + screen.blit(mass_text, (10, 10)) + + # 分数显示已禁用 + # score_text = font.render(f"分数: {self.score}", True, BLACK) + # screen.blit(score_text, (10, 50)) + + # 游戏结束 + if not self.player_cells and not self.game_over: + self.game_over = True + # 已经在AI吃掉最后一个球时保存了final_mass,如果还有其他球在这里补充 + if self.final_mass == 0: + self.final_mass = sum(p.mass for p in self.player_cells) + + if self.game_over: + game_over_text = font.render(f"游戏结束! 最终质量: {int(self.final_mass)}", True, RED) + text_rect = game_over_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2)) + screen.blit(game_over_text, text_rect) + + restart_text = small_font.render("按R重新开始", True, BLACK) + restart_rect = restart_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + 40)) + screen.blit(restart_text, restart_rect) + + if self.paused: + pause_text = font.render("暂停", True, RED) + text_rect = pause_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2)) + screen.blit(pause_text, text_rect) + + # 操作提示 - 分成两行显示避免超出屏幕,使用普通字符 + hint_text1 = small_font.render(f"鼠标: 移动 左键: 加速(不耗质量) 空格: 分裂({len(self.player_cells)}/16)", True, BLACK) + hint_text2 = small_font.render("R: 重来 P: 暂停 完全靠近: 合体 大球衰减更多", True, BLACK) + screen.blit(hint_text1, (10, SCREEN_HEIGHT - 50)) + screen.blit(hint_text2, (10, SCREEN_HEIGHT - 25)) + + def split_player(self, mouse_x, mouse_y): + """玩家分裂 - 最多16个分身""" + main_player = self.get_main_player() + if not main_player or main_player.mass < 36: # 最小分裂质量 + return + + # 限制最多16个分身(4次分裂:1→2→4→8→16) + if len(self.player_cells) >= 16: + return + + camera_x = main_player.x - SCREEN_WIDTH // 2 + camera_y = main_player.y - SCREEN_HEIGHT // 2 + world_mouse_x = mouse_x + camera_x + world_mouse_y = mouse_y + camera_y + + # 计算方向 + dx = world_mouse_x - main_player.x + dy = world_mouse_y - main_player.y + dist = math.hypot(dx, dy) + if dist == 0: + dist = 0.1 + dx = 1 + dy = 0 + + dx /= dist + dy /= dist + + # 创建新球 + new_mass = main_player.mass / 2 + main_player.mass = new_mass + main_player.update_mass() + + new_x = main_player.x + dx * (main_player.radius + 5) + new_y = main_player.y + dy * (main_player.radius + 5) + + new_cell = Cell(new_x, new_y, 0, BLUE, is_player=True) + new_cell.mass = new_mass + new_cell.update_mass() + self.player_cells.append(new_cell) + + # 给新细胞一个推力 + new_cell.x += dx * 10 + new_cell.y += dy * 10 + + def mass_decay(self): + """质量衰减:所有球按比例减小体积,越大减少越多,设置最小质量保证不会太小没法吃食物""" + # 最小质量下限,不会比这个更小 + MIN_MASS = 20 + # 衰减系数,每帧衰减当前质量的比例 + # 纯比例衰减:质量越大衰减越多,小球衰减很少几乎不衰减,符合要求 + decay_rate = 0.0001 + + # 玩家球衰减 + for i, player in reversed(list(enumerate(self.player_cells))): + decay_amount = player.mass * decay_rate + player.mass -= decay_amount + # 限制最小质量 + if player.mass < MIN_MASS: + player.mass = MIN_MASS + player.update_mass() + + # AI球衰减 + for i, ai in reversed(list(enumerate(self.ai_cells))): + decay_amount = ai.mass * decay_rate + ai.mass -= decay_amount + if ai.mass < MIN_MASS: + ai.mass = MIN_MASS + ai.update_mass() + + def restart(self): + """重新开始游戏""" + self.player_cells = [] + self.ai_cells = [] + self.foods = [] + self.score = 0 + self.game_over = False + self.paused = False + + # 初始化玩家 + start_x = WORLD_WIDTH // 2 + start_y = WORLD_HEIGHT // 2 + player = Cell(start_x, start_y, 20, BLUE, is_player=True) + self.player_cells.append(player) + + # 生成初始食物 - 更多更密 + self.generate_food(200) + + # 生成初始AI球球 + self.generate_ai(5) + + +def main(): + """主游戏循环""" + game = Game() + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if event.type == pygame.MOUSEBUTTONDOWN: + if event.button == 1 and not game.game_over and not game.paused: + # 左键加速已经在update处理 + pass + if event.type == pygame.KEYDOWN: + # 获取字符,转小写(兼容输入法) + char = event.unicode.lower() if event.unicode else '' + + # 处理扫描码掩码:某些键盘/SDL会加上0x40000000掩码 + SCANCODE_MASK = 1 << 30 # 1073741824 + key = event.key + if key & SCANCODE_MASK: + key = key & ~SCANCODE_MASK + + # 空格键分裂 + if key == pygame.K_SPACE and not game.game_over and not game.paused: + mouse_x, mouse_y = pygame.mouse.get_pos() + game.split_player(mouse_x, mouse_y) + + # R键重启:兼容不同键盘、输入法、大小写 + if (key == pygame.K_r + or key == 114 + or char == 'r' + or char == 'R'): + game = Game() + + # P键暂停:兼容不同键盘、输入法、大小写 + if (key == pygame.K_p + or key == 112 + or char == 'p' + or char == 'P'): + game.paused = not game.paused + + if not game.game_over and not game.paused: + # 获取鼠标位置 + mouse_x, mouse_y = pygame.mouse.get_pos() + + # 更新游戏 + game.update_player(mouse_x, mouse_y) + game.update_ai() + game.check_collisions() + + # 绘制 + game.draw() + + # 更新显示 + pygame.display.flip() + clock.tick(FPS) + + pygame.quit() + sys.exit() + + +if __name__ == "__main__": + main()