import cv2
import numpy as np
import random
from collections import deque
import mediapipe as mp
import json
import os
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
import platform
# ------------------------------
# 检测系统并选择合适的中文字体路径
# ------------------------------
def get_chinese_font():
system = platform.system()
font_path = None
if system == "Windows":
font_path = "C:/Windows/Fonts/simhei.ttf" # 黑体
elif system == "Darwin": # macOS
font_path = "/System/Library/Fonts/PingFang.ttc"
elif system == "Linux":
for path in [
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"
]:
if os.path.exists(path):
font_path = path
break
try:
return ImageFont.truetype(font_path, 32) if font_path else ImageFont.load_default()
except Exception as e:
print(f"⚠️ 加载中文字体失败: {e}")
return ImageFont.load_default()
CHINESE_FONT = get_chinese_font()
# ------------------------------
# 在 OpenCV 图像上绘制中文(使用 PIL)
# ------------------------------
def put_chinese_text(image, text, position, color=(255, 255, 255), font_size=30):
"""使用 Pillow 绘制中文文本"""
try:
if hasattr(CHINESE_FONT, 'path'):
font = ImageFont.truetype(CHINESE_FONT.path, font_size)
else:
font = ImageFont.load_default()
print("⚠️ 使用默认字体渲染中文")
except:
font = ImageFont.load_default()
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)
draw.text(position, text, fill=tuple(color), font=font)
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
# ------------------------------
# 排行榜管理
# ------------------------------
RANKING_FILE = "snake_ranking.json"
MAX_RECORDS = 100
def load_ranking():
if not os.path.exists(RANKING_FILE):
return []
try:
with open(RANKING_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
seen = set()
unique_data = []
for item in data:
key = (item.get('score', 0), item.get('timestamp', ''))
if key not in seen:
seen.add(key)
unique_data.append(item)
return sorted(unique_data, key=lambda x: x['score'], reverse=True)
except Exception as e:
print("加载排行榜失败:", e)
return []
def save_best_score(score):
ranking = load_ranking()
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
new_record = {"score": score, "timestamp": current_time}
ranking.append(new_record)
seen = set()
unique_ranking = []
for r in ranking:
key = (r['score'], r['timestamp'])
if key not in seen:
seen.add(key)
unique_ranking.append(r)
unique_ranking = sorted(unique_ranking, key=lambda x: x['score'], reverse=True)[:MAX_RECORDS]
try:
with open(RANKING_FILE, 'w', encoding='utf-8') as f:
json.dump(unique_ranking, f, indent=2, ensure_ascii=False)
except Exception as e:
print("保存排行榜失败:", e)
def clear_ranking():
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
for y in range(720):
color_val = 20 + y // 20
frame[y, :] = [color_val, color_val // 2, 60]
h, w = frame.shape[:2]
center_x = w // 2
frame = put_chinese_text(frame, "⚠️ 清空所有记录?", (center_x - 240, h // 2 - 60),
color=(0, 0, 255), font_size=48)
frame = put_chinese_text(frame, "确定要清空吗?(Y=是, N=否)", (center_x - 260, h // 2),
color=(255, 255, 255), font_size=36)
frame = put_chinese_text(frame, "此操作不可撤销!", (center_x - 180, h // 2 + 50),
color=(150, 150, 150), font_size=32)
cv2.imshow("Hand-Controlled Snake Game", frame)
while True:
key = cv2.waitKey(1) & 0xFF
if key in (ord('y'), ord('Y')):
try:
with open(RANKING_FILE, 'w') as f:
f.write("[]")
return True
except Exception as e:
print("清空失败:", e)
return False
elif key in (ord('n'), ord('N'), 27): # ESC
return False
# ------------------------------
# 主菜单界面
# ------------------------------
def show_ranking_menu():
ranking = load_ranking()
frame = np.zeros((720, 1280, 3), dtype=np.uint8)
for y in range(720):
color_val = 20 + y // 20
frame[y, :] = [color_val, color_val // 2, 60]
h, w = frame.shape[:2]
center_x = w // 2
frame = put_chinese_text(frame, "🏆 贪吃蛇游戏排行榜 🏆", (center_x - 280, 40),
color=(255, 255, 100), font_size=50)
frame = put_chinese_text(frame, "名次 得分 时间", (center_x - 240, 90),
color=(200, 255, 255), font_size=30)
start_y = 140
for i, item in enumerate(ranking[:10]):
text = f"{i + 1:2d} {item['score']:4d} {item['timestamp']}"
color = (0, 255, 255) if i == 0 else (200, 255, 200)
frame = put_chinese_text(frame, text, (center_x - 240, start_y + i * 32),
color=color, font_size=28)
if len(ranking) == 0:
frame = put_chinese_text(frame, "暂无记录...", (center_x - 100, start_y),
color=(150, 150, 150), font_size=30)
button_w = 200
btn_y1, btn_y2 = h - 140, h - 80
# 开始按钮
start_x1, start_x2 = center_x - button_w - 50, center_x - 50
cv2.rectangle(frame, (start_x1, btn_y1), (start_x2, btn_y2), (0, 180, 0), -1)
cv2.rectangle(frame, (start_x1, btn_y1), (start_x2, btn_y2), (0, 255, 0), 3)
frame = put_chinese_text(frame, "开始游戏", (center_x - button_w // 2 - 50, h - 100),
color=(255, 255, 255), font_size=40)
# 退出按钮
quit_x1, quit_x2 = center_x + 50, center_x + button_w + 50
cv2.rectangle(frame, (quit_x1, btn_y1), (quit_x2, btn_y2), (180, 0, 0), -1)
cv2.rectangle(frame, (quit_x1, btn_y1), (quit_x2, btn_y2), (255, 0, 0), 3)
frame = put_chinese_text(frame, "退出游戏", (center_x + button_w // 2 - 40, h - 100),
color=(255, 255, 255), font_size=40)
# 清空按钮
clear_x1, clear_x2 = center_x - 100, center_x + 100
cv2.rectangle(frame, (clear_x1, h - 70), (clear_x2, h - 10), (100, 100, 100), -1)
cv2.rectangle(frame, (clear_x1, h - 70), (clear_x2, h - 10), (200, 200, 200), 2)
frame = put_chinese_text(frame, "清空记录", (center_x - 45, h - 30),
color=(255, 255, 255), font_size=32)
frame = put_chinese_text(frame, "按'S'开始|'Q'退出|'C'清空", (center_x - 240, h - 160),
color=(200, 200, 255), font_size=24)
cv2.imshow("Hand-Controlled Snake Game", frame)
while True:
key = cv2.waitKey(1) & 0xFF
if key in (ord('s'), ord('S')):
return "start"
elif key in (ord('q'), ord('Q'), 27):
return "quit"
elif key in (ord('c'), ord('C')):
result = clear_ranking()
return "refresh"
# ------------------------------
# 初始化 MediaPipe Hands
# ------------------------------
mp_hands = mp.solutions.hands
mp_draw = mp.solutions.drawing_utils
hands = mp_hands.Hands(
static_image_mode=False,
max_num_hands=1,
min_detection_confidence=0.7,
min_tracking_confidence=0.5
)
# ------------------------------
# 游戏参数设置
# ------------------------------
width, height = 1280, 720
snake_speed = 8
snake_length = 3
score = 0
game_over = False
walls = []
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
snake = deque([(width // 2, height // 2)])
direction = np.array([1.0, 0.0])
target_pos = None
food_radius = 15
food_color = (0, 255, 0)
food = [random.randint(50, width - 50), random.randint(50, height - 50)]
# ------------------------------
# 工具函数
# ------------------------------
def is_point_in_wall(x, y, walls):
return any(wx <= x <= wx + ww and wy <= y <= wy + wh for (wx, wy, ww, wh) in walls)
def check_wall_collision(head, walls):
hx, hy = head
return any(wx <= hx <= wx + ww and wy <= hy <= wy + wh for (wx, wy, ww, wh) in walls)
def generate_food(snake_body, walls):
max_attempts = 100
min_dist = 30
for _ in range(max_attempts):
x = random.randint(50, width - 50)
y = random.randint(50, height - 50)
pos = np.array([x, y])
if is_point_in_wall(x, y, walls):
continue
if any(np.linalg.norm(pos - np.array(seg)) < min_dist for seg in snake_body):
continue
return [x, y]
return [width - 60, height - 60]
# BFS 路径可达性检查(略去以保持简洁,实际项目建议保留)
# 此处简化为仅距离判断(用于快速测试)
def is_path_available(head_pos, food_pos, walls):
# 简化版:只要不直接被墙堵死就算通路
return not is_point_in_wall(food_pos[0], food_pos[1], walls)
def add_wall_safely(walls_list, snake_body, food_pos):
if len(walls_list) >= 5:
return walls_list
for _ in range(30):
ww = random.randint(40, 150)
wh = random.randint(40, 150)
wx = random.randint(50, width - ww - 50)
wy = random.randint(50, height - wh - 50)
new_wall = (wx, wy, ww, wh)
temp_walls = walls_list + [new_wall]
if not is_point_in_wall(snake_body[0][0], snake_body[0][1], temp_walls) and \
not is_point_in_wall(food_pos[0], food_pos[1], temp_walls):
walls_list.append(new_wall)
break
return walls_list
# ------------------------------
# 主游戏循环
# ------------------------------
while True:
action = show_ranking_menu()
if action == "quit":
break
elif action == "refresh":
continue
elif action == "start":
# 重置游戏
snake.clear()
snake.append((width // 2, height // 2))
direction = np.array([1.0, 0.0])
snake_length = 3
score = 0
game_over = False
walls = []
target_pos = None
food = generate_food(snake, walls)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
frame = cv2.flip(frame, 1)
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 👇 安全调用 MediaPipe 并检查结果
results = hands.process(rgb_frame)
new_target = None
# ✅ 关键修复:先检查是否检测到手
if not game_over and results is not None:
# ✅ 使用 hasattr 和 None 判断避免 IDE 警告
if hasattr(results, 'multi_hand_landmarks') and results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
mp_draw.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)
idx_tip = hand_landmarks.landmark[mp_hands.HandLandmark.INDEX_FINGER_TIP]
x, y = int(idx_tip.x * width), int(idx_tip.y * height)
# 更新目标点(平滑滤波)
if target_pos is None:
target_pos = (x, y)
else:
alpha = 0.3
target_pos = (
int(alpha * x + (1 - alpha) * target_pos[0]),
int(alpha * y + (1 - alpha) * target_pos[1])
)
new_target = target_pos
# 显示目标点(紫色圆圈)
cv2.circle(frame, target_pos, 12, (255, 0, 255), -1)
# 移动逻辑
if not game_over:
head = np.array(snake[0])
if new_target is not None:
to_target = np.array(new_target) - head
dist = np.linalg.norm(to_target)
if dist > 30:
desired_dir = to_target / (dist + 1e-8)
curr_dir_norm = direction / (np.linalg.norm(direction) + 1e-8)
dot = np.clip(np.dot(curr_dir_norm, desired_dir), -1.0, 1.0)
angle = np.arccos(dot)
if angle > 0.25: # 最大转向弧度
w1 = np.sin(angle - 0.25)
w2 = np.sin(0.25)
sin_a = np.sin(angle)
blended = (w1 * curr_dir_norm + w2 * desired_dir) / (sin_a + 1e-8)
direction = blended / (np.linalg.norm(blended) + 1e-8)
else:
direction = desired_dir
new_head = head + direction * snake_speed
new_head = np.round(new_head).astype(int)
snake.appendleft(tuple(new_head))
# 碰撞检测
if (check_wall_collision(new_head, walls) or
new_head[0] <= 0 or new_head[0] >= width or
new_head[1] <= 0 or new_head[1] >= height):
game_over = True
for i in range(4, len(snake)):
seg = np.array(snake[i])
if np.linalg.norm(new_head - seg) < 15:
game_over = True
break
# 吃食物
if np.linalg.norm(new_head - np.array(food)) < (food_radius + 10):
old_score = score
score += 1
food = generate_food(snake, walls)
snake_length += 2
if score // 5 > old_score // 5:
walls = add_wall_safely(walls, snake, food)
while len(snake) > snake_length:
snake.pop()
# 绘图部分
for wall in walls:
wx, wy, ww, wh = wall
cv2.rectangle(frame, (wx, wy), (wx + ww, wy + wh), (255, 255, 0), -1)
cv2.rectangle(frame, (wx, wy), (wx + ww, wy + wh), (0, 0, 0), 3)
cv2.circle(frame, tuple(food), food_radius, food_color, -1)
for i in range(1, len(snake)):
alpha = i / len(snake)
color = (0, int(255 * (1 - alpha)), int(100 * (1 - alpha)))
cv2.line(frame, snake[i - 1], snake[i], color, 8)
cv2.circle(frame, snake[0], 10, (255, 255, 255), -1)
cv2.circle(frame, snake[0], 8, (0, 150, 0), -1)
frame = put_chinese_text(frame, f"得分: {score}", (20, 15), color=(255, 255, 255), font_size=50)
if game_over:
overlay = frame.copy()
cv2.rectangle(overlay, (width // 4, height // 4), (3 * width // 4, 3 * height // 4), (0, 0, 0), -1)
cv2.addWeighted(overlay, 0.7, frame, 0.3, 0, frame)
frame = put_chinese_text(frame, "游戏结束", (width // 2 - 120, height // 2 - 60),
color=(0, 0, 255), font_size=60)
frame = put_chinese_text(frame, "按任意键返回主菜单", (width // 2 - 160, height // 2),
color=(255, 255, 255), font_size=36)
frame = put_chinese_text(frame, f"最终得分: {score}", (width // 2 - 120, height // 2 + 50),
color=(255, 255, 0), font_size=32)
cv2.imshow("Hand-Controlled Snake Game", frame)
cv2.waitKey(0)
break
cv2.imshow("Hand-Controlled Snake Game", frame)
if cv2.waitKey(1) == 27: # ESC
break
# ------------------------------
# 释放资源
# ------------------------------
cap.release()
cv2.destroyAllWindows()
hands.close()
最新发布