import pygame import random import sys import os # -------------------------- 基础配置 -------------------------- WIDTH = 600 HEIGHT = 600 BLOCK_SIZE = 20 # 难度对应速度(数字越大蛇移动越快) LEVEL_SPEEDS = { 1: 10, # Level1 最慢 2: 15, # Level2 正常速度 3: 22 # Level3 较快速度 } # 颜色定义 BLACK = (0, 0, 0) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) GRAY = (180, 180, 180) OBSTACLE_COLOR = (139, 69, 19) # 马鞍棕障碍物 # 场地总格子数量,用于胜利判定 TOTAL_BLOCKS = (WIDTH // BLOCK_SIZE) * (HEIGHT // BLOCK_SIZE) # -------------------------- 资源加载(带容错) -------------------------- assets_dir = os.path.join(os.path.dirname(__file__), "assets") # 四方向蛇头图片 snake_head_up = None snake_head_down = None snake_head_left = None snake_head_right = None # 蛇身、食物、障碍物图片 snake_body_img = None food_img = None obstacle_img = None # 音效 eat_sound = None game_over_sound = None win_sound = None def load_assets(): """加载所有素材,加载失败不影响游戏运行""" global snake_head_up, snake_head_down, snake_head_left, snake_head_right global snake_body_img, food_img, obstacle_img, eat_sound, game_over_sound, win_sound # 加载图片资源 try: # 四方向蛇头 up_path = os.path.join(assets_dir, "snake_head_up.png") if os.path.exists(up_path): snake_head_up = pygame.image.load(up_path).convert_alpha() snake_head_up = pygame.transform.scale(snake_head_up, (BLOCK_SIZE, BLOCK_SIZE)) down_path = os.path.join(assets_dir, "snake_head_down.png") if os.path.exists(down_path): snake_head_down = pygame.image.load(down_path).convert_alpha() snake_head_down = pygame.transform.scale(snake_head_down, (BLOCK_SIZE, BLOCK_SIZE)) left_path = os.path.join(assets_dir, "snake_head_left.png") if os.path.exists(left_path): snake_head_left = pygame.image.load(left_path).convert_alpha() snake_head_left = pygame.transform.scale(snake_head_left, (BLOCK_SIZE, BLOCK_SIZE)) right_path = os.path.join(assets_dir, "snake_head_right.png") if os.path.exists(right_path): snake_head_right = pygame.image.load(right_path).convert_alpha() snake_head_right = pygame.transform.scale(snake_head_right, (BLOCK_SIZE, BLOCK_SIZE)) # 蛇身 body_path = os.path.join(assets_dir, "snake_body.png") if os.path.exists(body_path): snake_body_img = pygame.image.load(body_path).convert_alpha() snake_body_img = pygame.transform.scale(snake_body_img, (BLOCK_SIZE, BLOCK_SIZE)) # 食物 food_path = os.path.join(assets_dir, "food.png") if os.path.exists(food_path): food_img = pygame.image.load(food_path).convert_alpha() food_img = pygame.transform.scale(food_img, (BLOCK_SIZE, BLOCK_SIZE)) # 障碍物图片 obs_path = os.path.join(assets_dir, "obstacle.png") if os.path.exists(obs_path): obstacle_img = pygame.image.load(obs_path).convert_alpha() obstacle_img = pygame.transform.scale(obstacle_img, (BLOCK_SIZE, BLOCK_SIZE)) except: pass # 加载音效资源 try: pygame.mixer.init() eat_path = os.path.join(assets_dir, "eat.wav") if os.path.exists(eat_path): eat_sound = pygame.mixer.Sound(eat_path) over_path = os.path.join(assets_dir, "game_over.wav") if os.path.exists(over_path): game_over_sound = pygame.mixer.Sound(over_path) win_path = os.path.join(assets_dir, "win.wav") if os.path.exists(win_path): win_sound = pygame.mixer.Sound(win_path) except: pass # -------------------------- 游戏核心类 -------------------------- class SnakeGame: def __init__(self): pygame.init() # 关闭系统自带键盘长按延迟(关键优化1) pygame.key.set_repeat(0, 0) self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("贪吃蛇 - WASD控制") self.clock = pygame.time.Clock() # 多级字体:标题、大标题、普通文本 self.title_font = pygame.font.Font(None, 64) self.big_font = pygame.font.Font(None, 48) self.menu_font = pygame.font.Font(None, 32) self.font = pygame.font.Font(None, 25) # 加载素材 load_assets() # 游戏状态:menu(菜单选难度) / playing(游戏中) / game_over(结束) / win(胜利) self.state = 'menu' # 默认难度为Level2 self.current_level = 2 self.speed = LEVEL_SPEEDS[self.current_level] # 障碍物列表、吃食物计数(每2个生成一组三连障碍) self.obstacles = [] self.eat_count_for_obs = 0 # 初始化游戏数据(菜单状态下不运行逻辑) self.reset_game() def reset_game(self): """重置游戏状态,保留当前难度,清空障碍物""" self.snake = [ (WIDTH // 2, HEIGHT // 2), (WIDTH // 2 - BLOCK_SIZE, HEIGHT // 2), (WIDTH // 2 - 2 * BLOCK_SIZE, HEIGHT // 2) ] # 初始方向向右 self.dx = BLOCK_SIZE self.dy = 0 self.obstacles = [] self.eat_count_for_obs = 0 self.spawn_food() self.score = 0 def get_all_occupied_pos(self): """获取所有被占用的格子:蛇身 + 障碍物""" occupied = set(self.snake) occupied.update(set(self.obstacles)) return occupied def spawn_food(self): """随机生成食物,避开蛇身和障碍物;无空位则触发胜利""" occupied = self.get_all_occupied_pos() # 所有格子都被占满 → 游戏胜利 if len(occupied) >= TOTAL_BLOCKS: self.state = "win" if win_sound: win_sound.play() return # 正常刷新食物 while True: x = random.randrange(0, WIDTH - BLOCK_SIZE, BLOCK_SIZE) y = random.randrange(0, HEIGHT - BLOCK_SIZE, BLOCK_SIZE) pos = (x, y) if pos not in occupied: self.food = pos break def spawn_triple_obstacle(self): """生成一组3块相连障碍物,随机形状:横条/竖条/L型""" occupied = self.get_all_occupied_pos() occupied.add(self.food) grid_w = WIDTH // BLOCK_SIZE grid_h = HEIGHT // BLOCK_SIZE # 三种三连障碍物形状偏移量 shapes = [ [(0,0), (BLOCK_SIZE,0), (BLOCK_SIZE*2,0)], # 横向直线 [(0,0), (0,BLOCK_SIZE), (0,BLOCK_SIZE*2)], # 纵向直线 [(0,0), (BLOCK_SIZE,0), (0,BLOCK_SIZE)] # L型拐角 ] shape = random.choice(shapes) # 循环找合法生成点 while True: # 随机基础坐标(保证不贴边界) base_x = random.randrange(BLOCK_SIZE, WIDTH - BLOCK_SIZE*2, BLOCK_SIZE) base_y = random.randrange(BLOCK_SIZE, HEIGHT - BLOCK_SIZE*2, BLOCK_SIZE) block_group = [] conflict = False for dx, dy in shape: px = base_x + dx py = base_y + dy pos = (px, py) if pos in occupied: conflict = True break block_group.append(pos) if not conflict: # 无冲突,全部加入障碍物列表 self.obstacles.extend(block_group) break def check_wall_collision(self, head_x, head_y): """穿墙逻辑:边界穿越""" if head_x < 0: head_x = WIDTH - BLOCK_SIZE elif head_x >= WIDTH: head_x = 0 if head_y < 0: head_y = HEIGHT - BLOCK_SIZE elif head_y >= HEIGHT: head_y = 0 return head_x, head_y def check_collision(self, head): """碰撞检测:自身、障碍物""" # 撞到自己 if head in self.snake[1:]: return True # 撞到障碍物 if head in self.obstacles: return True return False def handle_input(self): """修复无延迟输入:分事件+实时按键采样""" event_list = pygame.event.get() keys = pygame.key.get_pressed() # 实时读取所有按住的按键(消除长按延迟) # 全局退出事件 for event in event_list: if event.type == pygame.QUIT: pygame.quit() sys.exit() # 一次性按键触发(菜单/重试用) if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: pygame.quit() sys.exit() # 菜单选难度 if self.state == 'menu': if event.key == pygame.K_1: self.current_level = 1 self.speed = LEVEL_SPEEDS[1] self.reset_game() self.state = 'playing' elif event.key == pygame.K_2: self.current_level = 2 self.speed = LEVEL_SPEEDS[2] self.reset_game() self.state = 'playing' elif event.key == pygame.K_3: self.current_level = 3 self.speed = LEVEL_SPEEDS[3] self.reset_game() self.state = 'playing' # 结束/胜利界面重试 elif self.state in ["game_over", "win"]: if event.key == pygame.K_r: self.reset_game() self.state = 'playing' # 游戏中实时WASD控制(核心修复:实时采样无延迟) if self.state == "playing": # W上,禁止反向向下 if keys[pygame.K_w] and self.dy != BLOCK_SIZE: self.dy = -BLOCK_SIZE self.dx = 0 # S下,禁止反向向上 elif keys[pygame.K_s] and self.dy != -BLOCK_SIZE: self.dy = BLOCK_SIZE self.dx = 0 # A左,禁止反向向右 elif keys[pygame.K_a] and self.dx != BLOCK_SIZE: self.dx = -BLOCK_SIZE self.dy = 0 # D右,禁止反向向左 elif keys[pygame.K_d] and self.dx != -BLOCK_SIZE: self.dx = BLOCK_SIZE self.dy = 0 def update(self): """更新游戏逻辑,仅游戏中运行""" if self.state != 'playing': return head_x, head_y = self.snake[0] new_head = (head_x + self.dx, head_y + self.dy) new_head = self.check_wall_collision(*new_head) # 撞到自身/障碍物 → Game Over if self.check_collision(new_head): self.state = 'game_over' if game_over_sound: game_over_sound.play() return self.snake.insert(0, new_head) # 吃到食物逻辑 if new_head == self.food: self.score += 1 self.eat_count_for_obs += 1 if eat_sound: eat_sound.play() # 每吃2个食物,生成一组3连块障碍物 if self.eat_count_for_obs >= 2: self.spawn_triple_obstacle() self.eat_count_for_obs = 0 self.spawn_food() # 没吃到食物:删除尾部,保持长度 else: self.snake.pop() def draw_snake_head(self, pos): """根据当前方向绘制对应蛇头""" x, y = pos # 向右 if self.dx == BLOCK_SIZE: if snake_head_right: self.screen.blit(snake_head_right, (x, y)) else: pygame.draw.rect(self.screen, GREEN, (x, y, BLOCK_SIZE, BLOCK_SIZE)) # 向左 elif self.dx == -BLOCK_SIZE: if snake_head_left: self.screen.blit(snake_head_left, (x, y)) else: pygame.draw.rect(self.screen, GREEN, (x, y, BLOCK_SIZE, BLOCK_SIZE)) # 向上 elif self.dy == -BLOCK_SIZE: if snake_head_up: self.screen.blit(snake_head_up, (x, y)) else: pygame.draw.rect(self.screen, GREEN, (x, y, BLOCK_SIZE, BLOCK_SIZE)) # 向下 elif self.dy == BLOCK_SIZE: if snake_head_down: self.screen.blit(snake_head_down, (x, y)) else: pygame.draw.rect(self.screen, GREEN, (x, y, BLOCK_SIZE, BLOCK_SIZE)) def draw_menu(self): """绘制难度选择菜单""" # 标题 title_text = self.title_font.render("SNAKE GAME", True, GREEN) self.screen.blit(title_text, (WIDTH//2 - title_text.get_width()//2, 120)) # 副标题 sub_text = self.menu_font.render("Select Difficulty", True, WHITE) self.screen.blit(sub_text, (WIDTH//2 - sub_text.get_width()//2, 200)) # 三个难度选项 level1_text = self.menu_font.render("Press 1 : Level 1 (Slow)", True, GRAY) self.screen.blit(level1_text, (WIDTH//2 - level1_text.get_width()//2, 280)) level2_text = self.menu_font.render("Press 2 : Level 2 (Normal)", True, WHITE) self.screen.blit(level2_text, (WIDTH//2 - level2_text.get_width()//2, 320)) level3_text = self.menu_font.render("Press 3 : Level 3 (Fast)", True, GRAY) self.screen.blit(level3_text, (WIDTH//2 - level3_text.get_width()//2, 360)) # 底部规则提示 tip_text = self.font.render("WASD move | Every 2 food spawn 3 connected obstacles | ESC exit", True, GRAY) self.screen.blit(tip_text, (WIDTH//2 - tip_text.get_width()//2, 480)) def draw(self): """绘制全部画面,分状态渲染""" self.screen.fill(BLACK) # 菜单界面 if self.state == 'menu': self.draw_menu() # 游戏界面(游戏中+结束+胜利都基于游戏画面) else: # 绘制障碍物 for obs_pos in self.obstacles: if obstacle_img: self.screen.blit(obstacle_img, obs_pos) else: pygame.draw.rect(self.screen, OBSTACLE_COLOR, (obs_pos[0], obs_pos[1], BLOCK_SIZE, BLOCK_SIZE)) # 绘制食物 if food_img: self.screen.blit(food_img, self.food) else: pygame.draw.rect(self.screen, RED, (self.food[0], self.food[1], BLOCK_SIZE, BLOCK_SIZE)) # 绘制蛇身和蛇头 for i, block in enumerate(self.snake): if i == 0: self.draw_snake_head(block) else: if snake_body_img: self.screen.blit(snake_body_img, block) else: pygame.draw.rect(self.screen, BLUE, (block[0], block[1], BLOCK_SIZE, BLOCK_SIZE)) # 左上角信息 score_text = self.font.render(f"Score: {self.score}", True, WHITE) self.screen.blit(score_text, (10, 10)) level_text = self.font.render(f"Level: {self.current_level}", True, GRAY) self.screen.blit(level_text, (10, 35)) obs_count_text = self.font.render(f"Obstacle Blocks: {len(self.obstacles)}", True, GRAY) self.screen.blit(obs_count_text, (10, 60)) # 游戏结束遮罩界面 if self.state == 'game_over': overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) self.screen.blit(overlay, (0, 0)) over_title = self.big_font.render("GAME OVER", True, RED) self.screen.blit(over_title, (WIDTH//2 - over_title.get_width()//2, HEIGHT//2 - 70)) retry_text = self.menu_font.render("R - Retry", True, WHITE) self.screen.blit(retry_text, (WIDTH//2 - retry_text.get_width()//2, HEIGHT//2 + 10)) close_text = self.menu_font.render("ESC - Close", True, WHITE) self.screen.blit(close_text, (WIDTH//2 - close_text.get_width()//2, HEIGHT//2 + 50)) # 游戏胜利遮罩界面 elif self.state == 'win': overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) self.screen.blit(overlay, (0, 0)) win_title = self.big_font.render("YOU WIN!", True, GREEN) self.screen.blit(win_title, (WIDTH//2 - win_title.get_width()//2, HEIGHT//2 - 70)) win_tip = self.menu_font.render("All space filled completely!", True, WHITE) self.screen.blit(win_tip, (WIDTH//2 - win_tip.get_width()//2, HEIGHT//2 - 20)) retry_text = self.menu_font.render("R - Retry", True, WHITE) self.screen.blit(retry_text, (WIDTH//2 - retry_text.get_width()//2, HEIGHT//2 + 20)) close_text = self.menu_font.render("ESC - Close", True, WHITE) self.screen.blit(close_text, (WIDTH//2 - close_text.get_width()//2, HEIGHT//2 + 60)) pygame.display.flip() def run(self): """游戏主循环""" while True: self.handle_input() self.update() self.draw() self.clock.tick(self.speed) # -------------------------- 启动游戏 -------------------------- if __name__ == "__main__": game = SnakeGame() game.run()