import random import pgzrun # ====================== 全局基础常量 ====================== WIDTH = 600 HEIGHT = 600 GRID = 20 GRID_NUM_X = WIDTH // GRID GRID_NUM_Y = HEIGHT // GRID FORBID_RANGE = 10 # 难度配置:Lv1=40,Lv2=30,Lv3=20 LEVEL_CFG = { 1: (8, 40), 2: (4, 30), 3: (2, 20) } DEFAULT_LEVEL = 2 # 色块颜色 BG_COLOR = (0, 0, 0) OBSTACLE_COLOR = (139, 69, 19) FOOD_FALLBACK_COLOR = (255, 0, 0) SNAKE_BODY_COLOR = (0, 0, 255) SNAKE_HEAD_COLOR = (0, 255, 0) # ====================== 全局状态 ====================== game_state = "menu" current_level = DEFAULT_LEVEL move_frame_gap = LEVEL_CFG[current_level][0] win_length = LEVEL_CFG[current_level][1] snake = [] dir_x, dir_y = 1, 0 next_dir_x, next_dir_y = 1, 0 frame_count = 0 score = 0 food_eaten_count = 0 food_pos = (0, 0) food_actor = None obstacles = [] has_revived = False bgm_obj = None # 背景音乐对象 bgm_playing = False # BGM播放标记 # 全部音效变量(bgm并入sounds) sound_start = None sound_eat = None sound_revive = None sound_gameover = None sound_win = None # ====================== 素材加载 ====================== def load_resources(): global food_actor, bgm_obj, sound_start, sound_eat, sound_revive, sound_gameover, sound_win # 食物图片 try: food_actor = Actor("food") except: food_actor = None # 所有音效统一从sounds加载 try: bgm_obj = sounds.bgm except Exception as e: print("bgm.wav 加载失败:", e) bgm_obj = None try: sound_start = sounds.start except: sound_start = None try: sound_eat = sounds.eat except: sound_eat = None try: sound_revive = sounds.revive except: sound_revive = None try: sound_gameover = sounds.game_over except: sound_gameover = None try: sound_win = sounds.win except: sound_win = None # ====================== 统一停止背景音乐函数(修复重叠核心) ====================== def stop_bgm(): global bgm_playing if bgm_obj is not None: bgm_obj.stop() bgm_playing = False # ====================== 禁区判断 ====================== def is_in_head_forbid_area(pos): x, y = pos hx, hy = snake[0] if dir_x == 1: if x > hx and (x - hx) <= FORBID_RANGE and abs(y - hy) <= 1: return True elif dir_x == -1: if x < hx and (hx - x) <= FORBID_RANGE and abs(y - hy) <= 1: return True elif dir_y == 1: if y > hy and (y - hy) <= FORBID_RANGE and abs(x - hx) <= 1: return True elif dir_y == -1: if y < hy and (hy - y) <= FORBID_RANGE and abs(x - hx) <= 1: return True return False # ====================== 游戏工具函数 ====================== def reset_game(): global snake, dir_x, dir_y, next_dir_x, next_dir_y global score, food_eaten_count, obstacles, frame_count, has_revived center_x = GRID_NUM_X // 2 center_y = GRID_NUM_Y // 2 snake = [(center_x, center_y), (center_x - 1, center_y), (center_x - 2, center_y)] dir_x, dir_y = 1, 0 next_dir_x, next_dir_y = 1, 0 frame_count = 0 score = 0 food_eaten_count = 0 obstacles.clear() has_revived = False spawn_food() def revive_player(): global snake, dir_x, dir_y, next_dir_x, next_dir_y, frame_count, obstacles, has_revived if sound_revive: sound_revive.play() center_x = GRID_NUM_X // 2 center_y = GRID_NUM_Y // 2 length = len(snake) snake = [] for i in range(length): snake.append((center_x - i, center_y)) dir_x, dir_y = 1, 0 next_dir_x, next_dir_y = 1, 0 frame_count = 0 obstacles.clear() has_revived = True spawn_food() def get_empty_positions(): empty = [] snake_set = set(snake) obs_set = set(obstacles) for x in range(GRID_NUM_X): for y in range(GRID_NUM_Y): pos = (x, y) if pos not in snake_set and pos not in obs_set and not is_in_head_forbid_area(pos): empty.append(pos) return empty def spawn_food(): global food_pos empty = get_empty_positions() food_pos = random.choice(empty) def spawn_obstacle_group(): empty = get_empty_positions() if len(empty) < 3: return start = random.choice(empty) sx, sy = start shapes = [ [(sx, sy), (sx+1, sy), (sx+2, sy)], [(sx, sy), (sx, sy+1), (sx, sy+2)], [(sx, sy), (sx+1, sy), (sx, sy+1)] ] shape = random.choice(shapes) fixed_shape = [] for (x, y) in shape: fx = x % GRID_NUM_X fy = y % GRID_NUM_Y fixed_shape.append((fx, fy)) snake_set = set(snake) obs_set = set(obstacles) food_set = {food_pos} valid = True for p in fixed_shape: if p in snake_set or p in obs_set or p in food_set or is_in_head_forbid_area(p): valid = False break if valid: obstacles.extend(fixed_shape) def check_win(): return len(snake) >= win_length # ====================== 游戏更新 ====================== def update(): global game_state, frame_count, dir_x, dir_y, next_dir_x, next_dir_y global score, food_eaten_count, snake, obstacles, bgm_playing # 进入游戏界面且未播放BGM时启动,play(-1) = 无限循环 if game_state == "play" and not bgm_playing and bgm_obj is not None: bgm_obj.play(-1) bgm_playing = True print("BGM启动成功") # 非对局界面跳过逻辑 if game_state == "menu" or game_state in ("death_revive", "gameover", "win"): return frame_count += 1 # 禁止反向掉头 if not (next_dir_x == -dir_x and next_dir_y == -dir_y): dir_x, dir_y = next_dir_x, next_dir_y if frame_count % move_frame_gap != 0: return head_x, head_y = snake[0] new_x = (head_x + dir_x) % GRID_NUM_X new_y = (head_y + dir_y) % GRID_NUM_Y new_head = (new_x, new_y) # 撞障碍/自身死亡 if new_head in snake or new_head in obstacles: if sound_gameover: sound_gameover.play() if not has_revived: game_state = "death_revive" else: game_state = "gameover" return # 吃到食物 if new_head == food_pos: snake.insert(0, new_head) score += 1 food_eaten_count += 1 if food_eaten_count % 2 == 0: spawn_obstacle_group() spawn_food() if sound_eat: sound_eat.play() if check_win(): game_state = "win" if sound_win: sound_win.play() return snake.insert(0, new_head) snake.pop() # ====================== 按键控制 ====================== def on_key_down(key): global game_state, current_level, move_frame_gap, win_length global next_dir_x, next_dir_y # ESC退出:先停止BGM再关闭程序 if key == keys.ESCAPE: stop_bgm() exit() # 难度菜单选关:进入游戏前停止旧BGM if game_state == "menu": if key == keys.K_1: current_level = 1 elif key == keys.K_2: current_level = 2 elif key == keys.K_3: current_level = 3 else: return move_frame_gap, win_length = LEVEL_CFG[current_level] stop_bgm() reset_game() if sound_start: sound_start.play() game_state = "play" return # 死亡复活界面 if game_state == "death_revive": if key == keys.Q: revive_player() game_state = "play" elif key == keys.R: # 按R重开:先停止BGM再重置 stop_bgm() reset_game() if sound_start: sound_start.play() game_state = "play" return # 胜利/彻底失败界面按R重开 if game_state in ("gameover", "win"): if key == keys.R: # 按R重开:先停止BGM再重置 stop_bgm() reset_game() if sound_start: sound_start.play() game_state = "play" return # WASD移动 if game_state == "play": if key == keys.W: next_dir_x, next_dir_y = 0, -1 elif key == keys.S: next_dir_x, next_dir_y = 0, 1 elif key == keys.A: next_dir_x, next_dir_y = -1, 0 elif key == keys.D: next_dir_x, next_dir_y = 1, 0 # ====================== 绘制 ====================== def draw(): screen.fill(BG_COLOR) # 难度菜单文字 40 / 30 / 20 if game_state == "menu": screen.draw.text("SNAKE GAME", center=(WIDTH//2, 120), fontsize=48, color="white") screen.draw.text("Press 1 / 2 / 3 to select level", center=(WIDTH//2, 180), fontsize=24, color="gray") screen.draw.text("Level 1: Slow | Target Length 40", center=(WIDTH//2, 230), fontsize=20, color="green") screen.draw.text("Level 2: Normal | Target Length 30 (Default)", center=(WIDTH//2, 270), fontsize=20, color="blue") screen.draw.text("Level 3: Fast | Target Length 20", center=(WIDTH//2, 310), fontsize=20, color="red") screen.draw.text("WASD Move | ESC Quit", center=(WIDTH//2, 400), fontsize=22, color="lightgray") return # 绘制障碍物 for (x, y) in obstacles: rect = Rect(x * GRID, y * GRID, GRID, GRID) screen.draw.filled_rect(rect, OBSTACLE_COLOR) # 绘制食物 fx, fy = food_pos food_center_x = fx * GRID + GRID / 2 food_center_y = fy * GRID + GRID / 2 if food_actor: food_actor.pos = (food_center_x, food_center_y) food_actor.draw() else: food_rect = Rect(fx * GRID, fy * GRID, GRID, GRID) screen.draw.filled_rect(food_rect, FOOD_FALLBACK_COLOR) # 绘制蛇身体 for idx in range(1, len(snake)): x, y = snake[idx] body_rect = Rect(x * GRID, y * GRID, GRID, GRID) screen.draw.filled_rect(body_rect, SNAKE_BODY_COLOR) # 蛇头 hx, hy = snake[0] head_rect = Rect(hx * GRID, hy * GRID, GRID, GRID) screen.draw.filled_rect(head_rect, SNAKE_HEAD_COLOR) # 左上角文字信息 screen.draw.text(f"Score: {score}", (10, 10), fontsize=20, color="white") screen.draw.text(f"Level: {current_level}", (10, 35), fontsize=20, color="white") screen.draw.text(f"Obstacle Blocks: {len(obstacles)}", (10, 60), fontsize=20, color="lightgray") # 死亡复活遮罩 if game_state == "death_revive": screen.draw.filled_rect(Rect(0, 0, WIDTH, HEIGHT), (0, 0, 0, 180)) screen.draw.text("YOU DIED", center=(WIDTH//2, HEIGHT//2 - 60), fontsize=60, color="orange") screen.draw.text("Press Q to Revive (Keep score & length)", center=(WIDTH//2, HEIGHT//2 - 10), fontsize=22, color="white") screen.draw.text("Press R to Restart Full Game", center=(WIDTH//2, HEIGHT//2 + 30), fontsize=22, color="white") screen.draw.text("ESC to Quit", center=(WIDTH//2, HEIGHT//2 + 70), fontsize=20, color="lightgray") # Game Over遮罩 if game_state == "gameover": screen.draw.filled_rect(Rect(0, 0, WIDTH, HEIGHT), (0, 0, 0, 180)) screen.draw.text("GAME OVER", center=(WIDTH//2, HEIGHT//2 - 40), fontsize=60, color="red") screen.draw.text("R Restart | ESC Quit", center=(WIDTH//2, HEIGHT//2 + 30), fontsize=24, color="white") # 胜利遮罩 if game_state == "win": screen.draw.filled_rect(Rect(0, 0, WIDTH, HEIGHT), (0, 0, 0, 180)) screen.draw.text("YOU WIN!", center=(WIDTH//2, HEIGHT//2 - 50), fontsize=60, color="green") screen.draw.text("All space filled completely!", center=(WIDTH//2, HEIGHT//2 + 10), fontsize=22, color="white") screen.draw.text("R Restart | ESC Quit", center=(WIDTH//2, HEIGHT//2 + 50), fontsize=24, color="white") # 加载素材 load_resources() # 启动游戏 pgzrun.go()