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