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 CHANGED
@@ -1,71 +1,105 @@
1
- import cv2
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
- import cvzone
12
+
13
+ import cv2
5
14
  import numpy as np
15
+ import cvzone
6
16
  from cvzone.HandTrackingModule import HandDetector
7
- from PIL import ImageFont, ImageDraw, Image
8
- import os
9
- import time
10
- import importlib.resources as pkg_resources
11
- import XMWAI.assets # 引入资源目录
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
- # ---------- 工具函数:从 assets 读取资源 ----------
15
- def get_asset_path(filename: str) -> str:
24
+ def _safe_read_image(path, default_shape=(50, 50, 4)):
16
25
  """
17
- 获取包内资源的绝对路径 (兼容 pip 安装后的情况)
26
+ 读取 PNG(带 alpha)或返回空透明图像(避免 None 导致 shape 失败)
18
27
  """
19
- with pkg_resources.path(XMWAI.assets, filename) as p:
20
- return str(p)
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 = [get_asset_path("h.png"),
38
- get_asset_path("s.png"),
39
- get_asset_path("t.png")]
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 = [get_asset_path("g.png"),
44
- get_asset_path("l.png"),
45
- get_asset_path("m.png")]
46
- if fontPath is None:
47
- fontPath = get_asset_path("微软雅黑.ttf")
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
- self.img = None
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
- img_pil = Image.fromarray(img)
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 OSError:
145
+ except Exception:
104
146
  font = ImageFont.load_default()
105
- draw.text(pos, text, font=font, fill=color)
106
- return np.array(img_pil)
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 = get_asset_path("1.png")
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}", size, 40, (68, 84, 106))
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
- if self.cap is None or not self.cap.isOpened():
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
- # 游戏结束,进入 gameover 界面
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
- cx, cy = currentHead
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
- while self.currentLength > self.allowedLength:
323
- if len(self.points) > 1:
324
- removed_dx = self.points[1][0] - self.points[0][0]
325
- removed_dy = self.points[1][1] - self.points[0][1]
326
- removed_dist = math.hypot(removed_dx, removed_dy)
327
- self.currentLength -= removed_dist
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
- h, w, _ = imgMain.shape
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
- if obstacleManager:
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
- if os.path.exists(path):
355
- self.foodImages.append(
356
- cv2.imread(path, cv2.IMREAD_UNCHANGED))
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
- self.hFood, self.wFood = 0, 0
363
- self.foodPoint = 0, 0
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
- max_attempts = 100
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
- self.foodPoint = random.randint(
370
- 50, 1230), random.randint(50, 670)
371
- self.foodIndex = random.randint(0, len(self.foodImages)-1)
372
- self.hFood, self.wFood, _ = self.foodImages[self.foodIndex].shape
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 < self.foodPoint[0] < ox + ow and oy < self.foodPoint[1] < oy + oh:
508
+ if ox < rx < ox + ow and oy < ry < oy + oh:
377
509
  overlap = True
378
510
  break
379
- if not overlap:
380
- return
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
- imgMain = cvzone.overlayPNG(imgMain, self.foodImages[self.foodIndex],
386
- (rx - self.wFood//2, ry - self.hFood//2))
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
- if os.path.exists(path):
395
- self.obstacleImages.append(
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
- h, w, _ = img.shape
405
- x = random.randint(150, 1230)
406
- y = random.randint(50, 670)
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
- imgMain = cvzone.overlayPNG(imgMain, img, (int(x), int(y)))
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: XMWAI
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Small code King AI related library
5
5
  Home-page: https://github.com/Tonykai88/XMWAI.git
6
6
  Author: pydevelopment
@@ -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=oa0pGvcomna_oXknVa5PasbRw0ILEdThfNExJjRbz8w,16418
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.4.dist-info/licenses/LICENSE.txt,sha256=bcaIQMrIhdQ3O-PoZlexjmW6h-wLGvHxh5Oksl4ohtc,1066
121
- xmwai-0.4.4.dist-info/METADATA,sha256=Qsm0Z6Kkd35aNmUsTNNp4AmIrlIAPYVxnJP8VPiPLs0,1227
122
- xmwai-0.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
123
- xmwai-0.4.4.dist-info/top_level.txt,sha256=yvGcDI-sggK5jqd9wz0saipZvk3oIE3hNGHlqUjxf0c,6
124
- xmwai-0.4.4.dist-info/RECORD,,
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