Files
Snake_game/snake.py
2026-06-16 07:05:11 +00:00

478 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()