Upload files to "/"
This commit is contained in:
478
snake.py
Normal file
478
snake.py
Normal file
@@ -0,0 +1,478 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user