XMWAI 0.4.4__py3-none-any.whl → 0.4.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of XMWAI might be problematic. Click here for more details.
- XMWAI/snake_core.py +288 -119
- {xmwai-0.4.4.dist-info → xmwai-0.4.6.dist-info}/METADATA +1 -1
- {xmwai-0.4.4.dist-info → xmwai-0.4.6.dist-info}/RECORD +6 -7
- XMWAI/assets/__init__.py +0 -0
- {xmwai-0.4.4.dist-info → xmwai-0.4.6.dist-info}/WHEEL +0 -0
- {xmwai-0.4.4.dist-info → xmwai-0.4.6.dist-info}/licenses/LICENSE.txt +0 -0
- {xmwai-0.4.4.dist-info → xmwai-0.4.6.dist-info}/top_level.txt +0 -0
XMWAI/snake_core.py
CHANGED
|
@@ -1,71 +1,105 @@
|
|
|
1
|
-
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
snake_core.py
|
|
4
|
+
标准版 SnakeGame(摄像头 + 手势控制)
|
|
5
|
+
与 main.py 配合使用:ai = XMWAI.SnakeGame(...); ai.hand(); ai.display(); ai.start(); ai.gameover(...)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
2
10
|
import math
|
|
3
11
|
import random
|
|
4
|
-
|
|
12
|
+
|
|
13
|
+
import cv2
|
|
5
14
|
import numpy as np
|
|
15
|
+
import cvzone
|
|
6
16
|
from cvzone.HandTrackingModule import HandDetector
|
|
7
|
-
from PIL import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
18
|
+
|
|
19
|
+
# ------------- 资源路径(包内 assets 目录) -------------
|
|
20
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
21
|
+
ASSETS_DIR = os.path.join(BASE_DIR, "assets")
|
|
12
22
|
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
def get_asset_path(filename: str) -> str:
|
|
24
|
+
def _safe_read_image(path, default_shape=(50, 50, 4)):
|
|
16
25
|
"""
|
|
17
|
-
|
|
26
|
+
读取 PNG(带 alpha)或返回空透明图像(避免 None 导致 shape 失败)
|
|
18
27
|
"""
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
if path and os.path.exists(path):
|
|
29
|
+
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
|
|
30
|
+
if img is None:
|
|
31
|
+
return np.zeros(default_shape, dtype=np.uint8)
|
|
32
|
+
return img
|
|
33
|
+
else:
|
|
34
|
+
return np.zeros(default_shape, dtype=np.uint8)
|
|
21
35
|
|
|
22
36
|
|
|
23
37
|
class SnakeGame:
|
|
24
38
|
def __init__(self, width=720, height=720, snakeInitLength=150, snakeGrowth=50,
|
|
25
39
|
snakeLineWidth=10, snakeHeadSize=15, foodPaths=None, foodNames=None, foodScores=None,
|
|
26
40
|
obstaclePaths=None, fontPath=None):
|
|
27
|
-
|
|
41
|
+
"""
|
|
42
|
+
初始化游戏参数并加载资源(图片、字体)
|
|
43
|
+
参数尽量与 main.py 调用保持一致
|
|
44
|
+
"""
|
|
45
|
+
# 基本参数
|
|
46
|
+
# 宽, 高(cv2.set 采用 3 -> width, 4 -> height)
|
|
28
47
|
self.resolution = (width, height)
|
|
29
48
|
self.snakeInitLength = snakeInitLength
|
|
30
49
|
self._snakeGrowth = snakeGrowth
|
|
31
50
|
self._snakeHeadSize = snakeHeadSize
|
|
32
|
-
self._foodScores = foodScores if foodScores is not None else [3, 2, 1]
|
|
33
51
|
self.snakeLineWidth = snakeLineWidth
|
|
52
|
+
self._foodScores = foodScores if foodScores is not None else [3, 2, 1]
|
|
34
53
|
|
|
35
|
-
#
|
|
54
|
+
# 资源默认路径(包内 assets)
|
|
55
|
+
if fontPath is None:
|
|
56
|
+
fontPath = os.path.join(ASSETS_DIR, "微软雅黑.ttf")
|
|
36
57
|
if foodPaths is None:
|
|
37
|
-
foodPaths = [
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
foodPaths = [
|
|
59
|
+
os.path.join(ASSETS_DIR, "h.png"),
|
|
60
|
+
os.path.join(ASSETS_DIR, "s.png"),
|
|
61
|
+
os.path.join(ASSETS_DIR, "t.png")
|
|
62
|
+
]
|
|
40
63
|
if foodNames is None:
|
|
41
64
|
foodNames = ["汉堡", "薯条", "甜甜圈"]
|
|
42
65
|
if obstaclePaths is None:
|
|
43
|
-
obstaclePaths = [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
obstaclePaths = [
|
|
67
|
+
os.path.join(ASSETS_DIR, "g.png"),
|
|
68
|
+
os.path.join(ASSETS_DIR, "l.png"),
|
|
69
|
+
os.path.join(ASSETS_DIR, "m.png")
|
|
70
|
+
]
|
|
48
71
|
|
|
72
|
+
# 赋值
|
|
73
|
+
self.fontPath = fontPath
|
|
49
74
|
self.foodPaths = foodPaths
|
|
50
75
|
self.foodNames = foodNames
|
|
51
76
|
self.obstaclePaths = obstaclePaths
|
|
52
|
-
self.fontPath = fontPath
|
|
53
77
|
|
|
78
|
+
# 摄像头 / 手势检测等对象
|
|
54
79
|
self.cap = None
|
|
55
80
|
self.detector = None
|
|
81
|
+
self.img = None
|
|
82
|
+
|
|
83
|
+
# 游戏对象(稍后初始化)
|
|
56
84
|
self.snake = None
|
|
57
85
|
self.foodManager = None
|
|
58
86
|
self.obstacleManager = None
|
|
59
|
-
|
|
87
|
+
|
|
88
|
+
# 覆盖文字(画面左上)
|
|
60
89
|
self.overlayTexts = []
|
|
61
90
|
|
|
91
|
+
# 计时器
|
|
62
92
|
self.timer = 30
|
|
63
93
|
self.start_time = None
|
|
64
94
|
|
|
95
|
+
# 加载并初始化内部游戏对象
|
|
65
96
|
self._init_game_objects()
|
|
66
|
-
self.open_window()
|
|
67
97
|
|
|
68
|
-
|
|
98
|
+
# 注意:不在 init 中自动打开摄像头(让用户在 main 中调用 open_window 或 hand/start)
|
|
99
|
+
# 如果你希望自动打开摄像头,请调用 self.open_window()
|
|
100
|
+
# self.open_window()
|
|
101
|
+
|
|
102
|
+
# ---------------- 属性同步(方便外部设置) ----------------
|
|
69
103
|
@property
|
|
70
104
|
def snakeHeadSize(self):
|
|
71
105
|
return self._snakeHeadSize
|
|
@@ -94,26 +128,82 @@ class SnakeGame:
|
|
|
94
128
|
def snakeGrowth(self, value):
|
|
95
129
|
self._snakeGrowth = value
|
|
96
130
|
|
|
97
|
-
#
|
|
131
|
+
# ----------------- 工具:在 OpenCV 图像上写中文 -----------------
|
|
98
132
|
def _putChineseText(self, img, text, pos, fontSize=40, color=(0, 0, 255)):
|
|
99
|
-
|
|
133
|
+
"""
|
|
134
|
+
在 BGR numpy 图像上绘制中文(使用 PIL)
|
|
135
|
+
pos: (x, y) 左上角
|
|
136
|
+
color: BGR 元组(PIL 需要 RGB,但我们直接使用 BGR 也能显示)
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
img_pil = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
|
140
|
+
except Exception:
|
|
141
|
+
img_pil = Image.fromarray(img)
|
|
100
142
|
draw = ImageDraw.Draw(img_pil)
|
|
101
143
|
try:
|
|
102
144
|
font = ImageFont.truetype(self.fontPath, fontSize)
|
|
103
|
-
except
|
|
145
|
+
except Exception:
|
|
104
146
|
font = ImageFont.load_default()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
147
|
+
# PIL 的颜色是 RGB
|
|
148
|
+
# 给定的 color 是 BGR(一致化处理)
|
|
149
|
+
b, g, r = color
|
|
150
|
+
draw.text(pos, text, font=font, fill=(r, g, b))
|
|
151
|
+
img = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)
|
|
152
|
+
return img
|
|
153
|
+
|
|
154
|
+
# ----------------- 初始化游戏对象 -----------------
|
|
108
155
|
def _init_game_objects(self):
|
|
156
|
+
# 内部类实例化
|
|
109
157
|
self.snake = self.Snake(color=(0, 0, 255), initLength=self.snakeInitLength,
|
|
110
158
|
lineWidth=self.snakeLineWidth, headSize=self._snakeHeadSize)
|
|
111
159
|
self.foodManager = self.FoodManager(
|
|
112
160
|
self.foodPaths, self.foodNames, self._foodScores)
|
|
113
161
|
self.obstacleManager = self.ObstacleManager(self.obstaclePaths)
|
|
162
|
+
# 随机生成障碍
|
|
114
163
|
self.obstacleManager.randomObstacles()
|
|
115
164
|
|
|
165
|
+
# ----------------- 摄像头与窗口控制 -----------------
|
|
166
|
+
def open_window(self):
|
|
167
|
+
"""打开摄像头并显示第一帧(如失败会打印错误)"""
|
|
168
|
+
self.cap = cv2.VideoCapture(0)
|
|
169
|
+
# 设置分辨率(注意:cv2.set(3) 对应宽,(4) 对应高)
|
|
170
|
+
self.cap.set(3, self.resolution[0])
|
|
171
|
+
self.cap.set(4, self.resolution[1])
|
|
172
|
+
self.detector = HandDetector(detectionCon=0.7, maxHands=1)
|
|
173
|
+
success, self.img = self.cap.read()
|
|
174
|
+
if not success:
|
|
175
|
+
print("摄像头打开失败")
|
|
176
|
+
return
|
|
177
|
+
self.img = cv2.flip(self.img, 1)
|
|
178
|
+
cv2.imshow("AI Snake", self.img)
|
|
179
|
+
cv2.waitKey(1)
|
|
180
|
+
|
|
181
|
+
def hand(self):
|
|
182
|
+
"""
|
|
183
|
+
快速测试手部检测(按 q 或检测到手后退出)
|
|
184
|
+
供用户检查摄像头与手势模块是否正常
|
|
185
|
+
"""
|
|
186
|
+
if self.cap is None:
|
|
187
|
+
self.open_window()
|
|
188
|
+
if self.detector is None:
|
|
189
|
+
self.detector = HandDetector(detectionCon=0.8, maxHands=1)
|
|
190
|
+
|
|
191
|
+
while True:
|
|
192
|
+
success, img = self.cap.read()
|
|
193
|
+
if not success:
|
|
194
|
+
break
|
|
195
|
+
img = cv2.flip(img, 1)
|
|
196
|
+
hands, img = self.detector.findHands(img, flipType=False)
|
|
197
|
+
cv2.imshow("AI Snake", img)
|
|
198
|
+
key = cv2.waitKey(1) & 0xFF
|
|
199
|
+
if hands or key == ord('q'):
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
# ----------------- 渲染与显示 -----------------
|
|
116
203
|
def _render_frame(self, show_food=True, show_obstacle=True):
|
|
204
|
+
"""读取摄像头一帧并渲染蛇、食物和障碍(内部使用)"""
|
|
205
|
+
if self.cap is None:
|
|
206
|
+
return
|
|
117
207
|
success, self.img = self.cap.read()
|
|
118
208
|
if not success:
|
|
119
209
|
return
|
|
@@ -121,62 +211,44 @@ class SnakeGame:
|
|
|
121
211
|
hands, self.img = self.detector.findHands(self.img, flipType=False)
|
|
122
212
|
player_head = tuple(hands[0]['lmList'][8][0:2]) if hands else None
|
|
123
213
|
|
|
214
|
+
# 更新蛇的位置
|
|
124
215
|
if not self.snake.gameOver and player_head:
|
|
125
216
|
self.snake.update(self.img, player_head, self.obstacleManager)
|
|
126
217
|
|
|
218
|
+
# 检查是否吃到食物
|
|
127
219
|
if not self.snake.gameOver and player_head and show_food:
|
|
128
220
|
cx, cy = player_head
|
|
129
221
|
rx, ry = self.foodManager.foodPoint
|
|
130
222
|
w, h = self.foodManager.wFood, self.foodManager.hFood
|
|
131
|
-
if rx - w//2 <= cx <= rx + w//2 and ry - h//2 <= cy <= ry + h//2:
|
|
223
|
+
if (rx - w // 2) <= cx <= (rx + w // 2) and (ry - h // 2) <= cy <= (ry + h // 2):
|
|
224
|
+
# 加分并延长蛇的允许长度
|
|
132
225
|
self.snake.score += self.foodManager.foodScores[self.foodManager.foodIndex]
|
|
133
226
|
self.snake.allowedLength += self._snakeGrowth
|
|
227
|
+
# 随机重新放置食物(避免与障碍重叠)
|
|
134
228
|
self.foodManager.randomFoodLocation(self.obstacleManager)
|
|
135
229
|
|
|
230
|
+
# 障碍物移动与绘制
|
|
136
231
|
if show_obstacle:
|
|
137
232
|
self.img = self.obstacleManager.draw(self.img)
|
|
138
233
|
self.obstacleManager.moveObstacles(
|
|
139
234
|
self.resolution[0], self.resolution[1])
|
|
140
235
|
|
|
236
|
+
# 食物绘制
|
|
141
237
|
if show_food:
|
|
142
238
|
self.img = self.foodManager.draw(self.img)
|
|
143
239
|
|
|
144
|
-
# ---------------- 对外接口 ----------------
|
|
145
|
-
def open_window(self):
|
|
146
|
-
self.cap = cv2.VideoCapture(0)
|
|
147
|
-
self.cap.set(3, self.resolution[0])
|
|
148
|
-
self.cap.set(4, self.resolution[1])
|
|
149
|
-
self.detector = HandDetector(detectionCon=0.7, maxHands=1)
|
|
150
|
-
success, self.img = self.cap.read()
|
|
151
|
-
if not success:
|
|
152
|
-
print("摄像头打开失败")
|
|
153
|
-
return
|
|
154
|
-
self.img = cv2.flip(self.img, 1)
|
|
155
|
-
cv2.imshow("AI Snake", self.img)
|
|
156
|
-
cv2.waitKey(1)
|
|
157
|
-
|
|
158
|
-
def hand(self):
|
|
159
|
-
if self.cap is None:
|
|
160
|
-
print("请先调用 open_window()")
|
|
161
|
-
return
|
|
162
|
-
if self.detector is None:
|
|
163
|
-
self.detector = HandDetector(detectionCon=0.8, maxHands=1)
|
|
164
|
-
while True:
|
|
165
|
-
success, self.img = self.cap.read()
|
|
166
|
-
if not success:
|
|
167
|
-
break
|
|
168
|
-
self.img = cv2.flip(self.img, 1)
|
|
169
|
-
hands, self.img = self.detector.findHands(self.img, flipType=False)
|
|
170
|
-
cv2.imshow("AI Snake", self.img)
|
|
171
|
-
key = cv2.waitKey(1) & 0xFF
|
|
172
|
-
if hands or key == ord('q'):
|
|
173
|
-
break
|
|
174
|
-
|
|
175
240
|
def display(self):
|
|
241
|
+
"""
|
|
242
|
+
在窗口上绘制分数和倒计时(只是渲染一帧供展示)
|
|
243
|
+
调用者可以在 start() 前调用一次显示初始状态
|
|
244
|
+
"""
|
|
176
245
|
if self.img is None:
|
|
246
|
+
# 尝试渲染一帧(不显示食物)
|
|
177
247
|
self._render_frame(show_food=False)
|
|
178
248
|
self.foodManager.randomFoodLocation(self.obstacleManager)
|
|
179
249
|
self._render_frame(show_food=True)
|
|
250
|
+
|
|
251
|
+
# 覆盖文字(玩家分数与倒计时)
|
|
180
252
|
self.overlayTexts = [
|
|
181
253
|
(f'玩家分数:{self.snake.score}', (50, 50), 30, (255, 0, 255)),
|
|
182
254
|
(f'倒计时:{self.timer} 秒', (50, 120), 30, (255, 0, 255))
|
|
@@ -187,8 +259,9 @@ class SnakeGame:
|
|
|
187
259
|
cv2.imshow("AI Snake", img_copy)
|
|
188
260
|
cv2.waitKey(1)
|
|
189
261
|
|
|
190
|
-
#
|
|
262
|
+
# ----------------- 重置与结束 -----------------
|
|
191
263
|
def reset_game(self):
|
|
264
|
+
"""重置游戏到初始状态"""
|
|
192
265
|
self.snake.reset()
|
|
193
266
|
self.snake.headSize = self._snakeHeadSize
|
|
194
267
|
self.obstacleManager.randomObstacles()
|
|
@@ -199,53 +272,72 @@ class SnakeGame:
|
|
|
199
272
|
if self.cap is None or not self.cap.isOpened():
|
|
200
273
|
self.open_window()
|
|
201
274
|
|
|
202
|
-
# ---------------- 游戏结束 ----------------
|
|
203
275
|
def gameover(self, path=None, size=(100, 100)):
|
|
276
|
+
"""
|
|
277
|
+
显示游戏结束画面并提供 r(重启)或 q(退出)选项
|
|
278
|
+
path: 自定义结束图片路径(相对于包内 assets),默认使用 assets/1.png
|
|
279
|
+
size: 未使用但保留下来以兼容外部调用
|
|
280
|
+
"""
|
|
204
281
|
if path is None:
|
|
205
|
-
path =
|
|
282
|
+
path = os.path.join(ASSETS_DIR, "1.png")
|
|
283
|
+
else:
|
|
284
|
+
# 若传入相对文件名(如 "1.png"),优先在 assets 中寻找
|
|
285
|
+
if not os.path.isabs(path):
|
|
286
|
+
path = os.path.join(ASSETS_DIR, path)
|
|
206
287
|
|
|
207
288
|
if os.path.exists(path):
|
|
208
289
|
gameover_img = cv2.imread(path)
|
|
290
|
+
# 缩放到窗口大小(以覆盖窗口)
|
|
209
291
|
gameover_img = cv2.resize(gameover_img, self.resolution)
|
|
210
292
|
else:
|
|
293
|
+
# 如果没有找到图片,生成一个黑色背景并写提示
|
|
211
294
|
gameover_img = np.zeros(
|
|
212
295
|
(self.resolution[1], self.resolution[0], 3), np.uint8)
|
|
213
296
|
cv2.putText(gameover_img, "Game Over Image Missing!", (50, 100),
|
|
214
297
|
cv2.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 255), 3)
|
|
215
298
|
|
|
299
|
+
# 在结束画面写最终分数(使用中文绘制)
|
|
216
300
|
gameover_img = self._putChineseText(
|
|
217
|
-
gameover_img, f"最终分数:{self.snake.score}",
|
|
301
|
+
gameover_img, f"最终分数:{self.snake.score}", (50, 200), 40, (68, 84, 106))
|
|
218
302
|
|
|
303
|
+
# 等待用户按键:r 重启, q 退出
|
|
219
304
|
while True:
|
|
220
305
|
cv2.imshow("AI Snake", gameover_img)
|
|
221
306
|
key = cv2.waitKey(0) & 0xFF
|
|
222
|
-
if key == ord('r'):
|
|
307
|
+
if key == ord('r'):
|
|
223
308
|
self.reset_game()
|
|
224
309
|
self.start()
|
|
225
310
|
break
|
|
226
|
-
elif key == ord('q'):
|
|
311
|
+
elif key == ord('q'):
|
|
227
312
|
if self.cap is not None:
|
|
228
313
|
self.cap.release()
|
|
229
314
|
cv2.destroyAllWindows()
|
|
230
315
|
break
|
|
231
316
|
|
|
232
|
-
#
|
|
317
|
+
# ----------------- 游戏主循环 -----------------
|
|
233
318
|
def start(self):
|
|
234
|
-
|
|
319
|
+
"""游戏主循环(会阻塞直到退出或游戏结束)"""
|
|
320
|
+
if self.cap is None or not getattr(self.cap, "isOpened", lambda: False)():
|
|
235
321
|
self.open_window()
|
|
322
|
+
if self.detector is None:
|
|
323
|
+
self.detector = HandDetector(detectionCon=0.7, maxHands=1)
|
|
324
|
+
|
|
236
325
|
self.start_time = time.time()
|
|
237
326
|
self.timer = 30
|
|
238
327
|
|
|
239
328
|
while True:
|
|
329
|
+
# 若游戏结束或倒计时结束,进入结束界面
|
|
240
330
|
if self.snake.gameOver or self.timer == 0:
|
|
241
|
-
|
|
242
|
-
self.gameover()
|
|
331
|
+
self.gameover("1.png", (520, 520))
|
|
243
332
|
break
|
|
244
333
|
|
|
245
334
|
self._render_frame(show_food=True, show_obstacle=True)
|
|
335
|
+
|
|
336
|
+
# 更新倒计时
|
|
246
337
|
elapsed = int(time.time() - self.start_time)
|
|
247
338
|
self.timer = max(0, 30 - elapsed)
|
|
248
339
|
|
|
340
|
+
# 覆盖文字显示
|
|
249
341
|
self.overlayTexts = [
|
|
250
342
|
(f'玩家分数:{self.snake.score}', (50, 50), 30, (255, 0, 255)),
|
|
251
343
|
(f'倒计时:{self.timer} 秒', (50, 120), 30, (255, 0, 255))
|
|
@@ -261,17 +353,21 @@ class SnakeGame:
|
|
|
261
353
|
key = cv2.waitKey(1)
|
|
262
354
|
if key == ord('r'): # 游戏中途重置
|
|
263
355
|
self.reset_game()
|
|
264
|
-
elif key == ord('q'):
|
|
356
|
+
elif key == ord('q'): # 退出
|
|
265
357
|
if self.cap is not None:
|
|
266
358
|
self.cap.release()
|
|
267
359
|
cv2.destroyAllWindows()
|
|
268
360
|
break
|
|
269
361
|
|
|
270
|
-
#
|
|
362
|
+
# ================= 内部类:Snake =================
|
|
271
363
|
class Snake:
|
|
272
364
|
def __init__(self, color, initLength=150, lineWidth=10, headSize=15):
|
|
365
|
+
"""
|
|
366
|
+
简洁的蛇实现:维护点列表,根据 allowedLength 保持长度
|
|
367
|
+
color: BGR 颜色
|
|
368
|
+
"""
|
|
273
369
|
self.points = []
|
|
274
|
-
self.currentLength = 0
|
|
370
|
+
self.currentLength = 0.0
|
|
275
371
|
self.allowedLength = initLength
|
|
276
372
|
self.previousHead = None
|
|
277
373
|
self.score = 0
|
|
@@ -281,27 +377,35 @@ class SnakeGame:
|
|
|
281
377
|
self.headSize = headSize
|
|
282
378
|
|
|
283
379
|
def reset(self):
|
|
380
|
+
"""重置蛇的状态"""
|
|
284
381
|
self.points = []
|
|
285
|
-
self.currentLength = 0
|
|
382
|
+
self.currentLength = 0.0
|
|
286
383
|
self.allowedLength = 150
|
|
287
384
|
self.previousHead = None
|
|
288
385
|
self.score = 0
|
|
289
386
|
self.gameOver = False
|
|
290
387
|
|
|
291
388
|
def update(self, imgMain, currentHead, obstacleManager=None):
|
|
389
|
+
"""
|
|
390
|
+
根据当前手的位置更新蛇(平滑插值 + 限步 + 修剪超长部分)
|
|
391
|
+
currentHead: (x, y)
|
|
392
|
+
"""
|
|
292
393
|
if self.gameOver:
|
|
293
394
|
return
|
|
294
|
-
|
|
295
|
-
if cx is None or cy is None:
|
|
395
|
+
if currentHead is None:
|
|
296
396
|
return
|
|
397
|
+
|
|
398
|
+
cx, cy = currentHead
|
|
297
399
|
if self.previousHead is None:
|
|
298
400
|
self.previousHead = (cx, cy)
|
|
299
401
|
px, py = self.previousHead
|
|
300
402
|
|
|
403
|
+
# 平滑插值,避免抖动
|
|
301
404
|
alpha = 0.7
|
|
302
405
|
cx = int(px * (1 - alpha) + cx * alpha)
|
|
303
406
|
cy = int(py * (1 - alpha) + cy * alpha)
|
|
304
407
|
|
|
408
|
+
# 限制最大步长(避免瞬移导致计算异常)
|
|
305
409
|
maxStep = 50
|
|
306
410
|
dx = cx - px
|
|
307
411
|
dy = cy - py
|
|
@@ -319,96 +423,152 @@ class SnakeGame:
|
|
|
319
423
|
|
|
320
424
|
self.previousHead = (cx, cy)
|
|
321
425
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
426
|
+
# 当长度大于允许长度时,从头部开始删除点
|
|
427
|
+
while self.currentLength > self.allowedLength and len(self.points) > 1:
|
|
428
|
+
removed_dx = self.points[1][0] - self.points[0][0]
|
|
429
|
+
removed_dy = self.points[1][1] - self.points[0][1]
|
|
430
|
+
removed_dist = math.hypot(removed_dx, removed_dy)
|
|
431
|
+
self.currentLength -= removed_dist
|
|
328
432
|
self.points.pop(0)
|
|
329
433
|
|
|
434
|
+
# 绘制蛇身(线段)
|
|
330
435
|
for i in range(1, len(self.points)):
|
|
331
436
|
cv2.line(
|
|
332
|
-
imgMain, self.points[i-1], self.points[i], self.color, self.lineWidth)
|
|
437
|
+
imgMain, self.points[i - 1], self.points[i], self.color, self.lineWidth)
|
|
333
438
|
|
|
439
|
+
# 绘制蛇头(变色圆)
|
|
334
440
|
snakeHeadColor = (random.randint(0, 255), random.randint(
|
|
335
441
|
0, 255), random.randint(0, 255))
|
|
336
442
|
cv2.circle(imgMain, (cx, cy), self.headSize,
|
|
337
443
|
snakeHeadColor, cv2.FILLED)
|
|
338
444
|
|
|
339
|
-
|
|
445
|
+
# 窗口边界检测
|
|
446
|
+
h, w = imgMain.shape[:2]
|
|
340
447
|
margin = 5
|
|
341
448
|
if cx <= margin or cx >= w - margin or cy <= margin or cy >= h - margin:
|
|
342
449
|
self.gameOver = True
|
|
343
450
|
|
|
344
|
-
|
|
451
|
+
# 与障碍物碰撞检测(若存在障碍管理器)
|
|
452
|
+
if obstacleManager is not None:
|
|
345
453
|
for ox, oy, ow, oh, *_ in obstacleManager.obstacles:
|
|
346
454
|
if ox <= cx <= ox + ow and oy <= cy <= oy + oh:
|
|
347
455
|
self.gameOver = True
|
|
348
456
|
|
|
349
|
-
#
|
|
457
|
+
# ================= 内部类:FoodManager =================
|
|
350
458
|
class FoodManager:
|
|
351
459
|
def __init__(self, foodPaths, foodNames, foodScores):
|
|
460
|
+
"""
|
|
461
|
+
foodPaths: list of 图片路径(带 alpha 的 PNG 推荐)
|
|
462
|
+
foodNames: list 名称(可选)
|
|
463
|
+
foodScores: list 对应分数(与 foodPaths 长度一致或可复用)
|
|
464
|
+
"""
|
|
352
465
|
self.foodImages = []
|
|
466
|
+
# 读取每个图片(若不存在,则使用透明占位)
|
|
353
467
|
for path in foodPaths:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
else:
|
|
358
|
-
self.foodImages.append(np.zeros((50, 50, 4), np.uint8))
|
|
468
|
+
img = _safe_read_image(path)
|
|
469
|
+
self.foodImages.append(img)
|
|
470
|
+
|
|
359
471
|
self.foodNames = foodNames
|
|
360
|
-
self.foodScores = foodScores
|
|
472
|
+
self.foodScores = foodScores if foodScores is not None else [
|
|
473
|
+
3] * len(self.foodImages)
|
|
361
474
|
self.foodIndex = 0
|
|
362
|
-
|
|
363
|
-
self.
|
|
475
|
+
# 宽高初始化为占位值(读取后更新)
|
|
476
|
+
self.hFood, self.wFood = 50, 50
|
|
477
|
+
self.foodPoint = (100, 100)
|
|
478
|
+
# 初始放置
|
|
364
479
|
self.randomFoodLocation()
|
|
365
480
|
|
|
366
481
|
def randomFoodLocation(self, obstacleManager=None):
|
|
367
|
-
|
|
482
|
+
"""
|
|
483
|
+
随机放置食物位置,避免与障碍重叠(最多尝试 max_attempts 次)
|
|
484
|
+
坐标范围依据典型摄像头分辨率设定,可根据需要调整
|
|
485
|
+
"""
|
|
486
|
+
max_attempts = 200
|
|
487
|
+
# 摄像头分辨率默认使用 1280x720 或 self 所在外部被设置的分辨率
|
|
488
|
+
# 这里我们使用一个可信任的范围:x in [50, self_max_w - 50], y in [50, self_max_h - 50]
|
|
489
|
+
# 为了不依赖外部,我们使用默认 0..1280 / 0..720 范围,但在 draw 时不会溢出
|
|
368
490
|
for _ in range(max_attempts):
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
491
|
+
# 随机食物类型
|
|
492
|
+
self.foodIndex = random.randint(0, len(self.foodImages) - 1)
|
|
493
|
+
img = self.foodImages[self.foodIndex]
|
|
494
|
+
# 确保 shape 可用(img 可能是 2D/3D )
|
|
495
|
+
try:
|
|
496
|
+
h, w = img.shape[:2]
|
|
497
|
+
except Exception:
|
|
498
|
+
h, w = 50, 50
|
|
499
|
+
self.hFood, self.wFood = h, w
|
|
500
|
+
# 随机位置(靠内侧,避免边缘)
|
|
501
|
+
rx = random.randint(50, max(50, 1280 - 50))
|
|
502
|
+
ry = random.randint(50, max(50, 720 - 50))
|
|
503
|
+
self.foodPoint = (rx, ry)
|
|
504
|
+
# 如果传入障碍管理器,检查是否与任一障碍重叠
|
|
373
505
|
if obstacleManager:
|
|
374
506
|
overlap = False
|
|
375
507
|
for ox, oy, ow, oh, *_ in obstacleManager.obstacles:
|
|
376
|
-
if ox <
|
|
508
|
+
if ox < rx < ox + ow and oy < ry < oy + oh:
|
|
377
509
|
overlap = True
|
|
378
510
|
break
|
|
379
|
-
if
|
|
380
|
-
|
|
511
|
+
if overlap:
|
|
512
|
+
continue
|
|
513
|
+
# 找到一个不重叠的位置
|
|
514
|
+
return
|
|
515
|
+
# 若多次尝试失败,则保留最后的值
|
|
381
516
|
return
|
|
382
517
|
|
|
383
518
|
def draw(self, imgMain):
|
|
519
|
+
"""把当前食物图片覆盖到主图上(使用 cvzone.overlayPNG 以支持 alpha)"""
|
|
384
520
|
rx, ry = self.foodPoint
|
|
385
|
-
|
|
386
|
-
|
|
521
|
+
# 防止越界
|
|
522
|
+
try:
|
|
523
|
+
h, w = self.foodImages[self.foodIndex].shape[:2]
|
|
524
|
+
except Exception:
|
|
525
|
+
h, w = 50, 50
|
|
526
|
+
self.hFood, self.wFood = h, w
|
|
527
|
+
# overlayPNG 需要左上角坐标
|
|
528
|
+
top_left = (int(rx - w // 2), int(ry - h // 2))
|
|
529
|
+
try:
|
|
530
|
+
imgMain = cvzone.overlayPNG(
|
|
531
|
+
imgMain, self.foodImages[self.foodIndex], top_left)
|
|
532
|
+
except Exception:
|
|
533
|
+
# 如果 overlay 失败,尝试简单贴图(忽略 alpha)
|
|
534
|
+
try:
|
|
535
|
+
imgMain[top_left[1]:top_left[1] + h, top_left[0]:top_left[0] + w] = \
|
|
536
|
+
cv2.resize(
|
|
537
|
+
self.foodImages[self.foodIndex][:, :, :3], (w, h))
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
387
540
|
return imgMain
|
|
388
541
|
|
|
389
|
-
#
|
|
542
|
+
# ================= 内部类:ObstacleManager =================
|
|
390
543
|
class ObstacleManager:
|
|
391
544
|
def __init__(self, obstaclePaths):
|
|
545
|
+
"""
|
|
546
|
+
obstaclePaths: list 图片路径
|
|
547
|
+
self.obstacles: list of [x, y, w, h, dx, dy, img]
|
|
548
|
+
"""
|
|
392
549
|
self.obstacleImages = []
|
|
393
550
|
for path in obstaclePaths:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
cv2.imread(path, cv2.IMREAD_UNCHANGED))
|
|
397
|
-
else:
|
|
398
|
-
self.obstacleImages.append(np.zeros((50, 50, 4), np.uint8))
|
|
551
|
+
img = _safe_read_image(path)
|
|
552
|
+
self.obstacleImages.append(img)
|
|
399
553
|
self.obstacles = []
|
|
400
554
|
|
|
401
555
|
def randomObstacles(self):
|
|
556
|
+
"""基于 obstacleImages 生成一组随机位置和速度的障碍物"""
|
|
402
557
|
self.obstacles.clear()
|
|
403
558
|
for img in self.obstacleImages:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
559
|
+
try:
|
|
560
|
+
h, w = img.shape[:2]
|
|
561
|
+
except Exception:
|
|
562
|
+
h, w = 50, 50
|
|
563
|
+
# 随机位置(避免太靠边)
|
|
564
|
+
x = random.randint(150, max(150, 1280 - w - 10))
|
|
565
|
+
y = random.randint(50, max(50, 720 - h - 10))
|
|
407
566
|
dx = random.choice([-5, 5])
|
|
408
567
|
dy = random.choice([-5, 5])
|
|
409
568
|
self.obstacles.append([x, y, w, h, dx, dy, img])
|
|
410
569
|
|
|
411
570
|
def moveObstacles(self, wMax, hMax):
|
|
571
|
+
"""更新每个障碍的位置并在边界反弹"""
|
|
412
572
|
for obs in self.obstacles:
|
|
413
573
|
x, y, ow, oh, dx, dy, img = obs
|
|
414
574
|
x += dx
|
|
@@ -420,7 +580,16 @@ class SnakeGame:
|
|
|
420
580
|
obs[0], obs[1], obs[4], obs[5] = x, y, dx, dy
|
|
421
581
|
|
|
422
582
|
def draw(self, imgMain):
|
|
583
|
+
"""把所有障碍物绘制到主图上"""
|
|
423
584
|
for obs in self.obstacles:
|
|
424
585
|
x, y, w, h, dx, dy, img = obs
|
|
425
|
-
|
|
586
|
+
try:
|
|
587
|
+
imgMain = cvzone.overlayPNG(imgMain, img, (int(x), int(y)))
|
|
588
|
+
except Exception:
|
|
589
|
+
# fallback: 直接简单覆盖(忽略 alpha)
|
|
590
|
+
try:
|
|
591
|
+
imgMain[int(y):int(y) + h, int(x)
|
|
592
|
+
:int(x) + w] = img[:, :, :3]
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
426
595
|
return imgMain
|
|
@@ -3,11 +3,10 @@ XMWAI/bomb_core.py,sha256=h2ZPH3SuoG2L_XOf1dcK8p3lhw5QzhneWl2yMLj1RiU,1819
|
|
|
3
3
|
XMWAI/core.py,sha256=rOXj7FnewSdnzBcFLjpnBtrOTCsvMfiycIcdPDagxho,10012
|
|
4
4
|
XMWAI/idiom_core.py,sha256=yU-VHhqqoutVm6GVikcjL3m9yuB1hUsFBpPYvwY4n5g,1689
|
|
5
5
|
XMWAI/magic_core.py,sha256=Ms4b12PJ8rjsmceg1VUqWCWx2ebvdhLp4sIF6K_Vaok,3491
|
|
6
|
-
XMWAI/snake_core.py,sha256=
|
|
6
|
+
XMWAI/snake_core.py,sha256=f22mPKUxcQYVMiQeNEGAOD5M1ex2jemIQmuwxJdy26c,24038
|
|
7
7
|
XMWAI/trial_class.py,sha256=fPsl7BZvhzch2FOIG4azr999kjtoly53Acm3LqL8f98,9724
|
|
8
8
|
XMWAI/web_core.py,sha256=7awPg1kYW3lYrbgylqJvUF3g050bn6H21PgmQ7Kv1wA,10927
|
|
9
9
|
XMWAI/assets/1.png,sha256=eEuKH_M_q3tc_O2bYnuLOsRP-NlJHIbNg0pgrKXEEjw,139720
|
|
10
|
-
XMWAI/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
10
|
XMWAI/assets/g.png,sha256=hr9hlKJ7y95X-g6-tllrzDNgL1WQkbq5cA5L4jASEAM,11686
|
|
12
11
|
XMWAI/assets/h.png,sha256=qO-kJJOPA8qUth5rqLeOVa_6_n7tU-ABQ14O0EW_YCE,14929
|
|
13
12
|
XMWAI/assets/l.png,sha256=Urm6LxH33HID6ZZbs2oMViUk4GiZ3upLPdsrNU8FlP0,9921
|
|
@@ -117,8 +116,8 @@ XMWAI/static/images/tomato.png,sha256=FEOEAOdUhW_BDFgTpxOkYc0I5Iu29_gtHb3RIPEej0
|
|
|
117
116
|
XMWAI/templates/burger.html,sha256=vDnxpSW8phetyScySsalScZnFKl3LNpy5lJjKxGXgbI,3320
|
|
118
117
|
XMWAI/templates/nutrition_pie.html,sha256=yJVXI28i-UfvF0xOXGSNLMb8oCJNhh2J3zoRDr5_7DM,5567
|
|
119
118
|
XMWAI/templates/创意菜谱.html,sha256=RcDgH58QLyUJ9A59wobu3wvchGBY1snVsXcZQZam5M0,4805
|
|
120
|
-
xmwai-0.4.
|
|
121
|
-
xmwai-0.4.
|
|
122
|
-
xmwai-0.4.
|
|
123
|
-
xmwai-0.4.
|
|
124
|
-
xmwai-0.4.
|
|
119
|
+
xmwai-0.4.6.dist-info/licenses/LICENSE.txt,sha256=bcaIQMrIhdQ3O-PoZlexjmW6h-wLGvHxh5Oksl4ohtc,1066
|
|
120
|
+
xmwai-0.4.6.dist-info/METADATA,sha256=bbkrzu832BEoD09zAgjlhAFzPxCGqKi87RKJVnsHtaY,1227
|
|
121
|
+
xmwai-0.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
122
|
+
xmwai-0.4.6.dist-info/top_level.txt,sha256=yvGcDI-sggK5jqd9wz0saipZvk3oIE3hNGHlqUjxf0c,6
|
|
123
|
+
xmwai-0.4.6.dist-info/RECORD,,
|
XMWAI/assets/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|