Create ball_games_with_pygame_version2.py

This commit is contained in:
2026-04-13 10:45:58 +08:00
parent f0fa3de637
commit d8ce873026

View File

@@ -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)
# 绘制UIUI不缩放始终固定在屏幕
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()