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