test_pathfind.py 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """pathfind 模块验证: 在线网格 + A* 2 格步长寻路。"""
  2. from itertools import pairwise
  3. from pathlib import Path
  4. from pbot.bot.pathfind import LearnedGrid, astar, load_walls_from_map
  5. def test_astar_straight_line_open_field() -> None:
  6. """Given: 开阔空地 When: A* (10,10)→(30,10) Then: 2 格步长路径, 终点宽松 ≤2。"""
  7. g = LearnedGrid()
  8. path = astar((10, 10), (30, 10), g)
  9. assert path is not None
  10. assert path[0] == (10, 10)
  11. assert max(abs(path[-1][0] - 30), abs(path[-1][1] - 10)) <= 2
  12. for a, b in pairwise(path):
  13. assert max(abs(b[0] - a[0]), abs(b[1] - a[1])) == 2
  14. def test_astar_detours_blocked_wall() -> None:
  15. """Given: 东向直线上有禁行格 When: A* Then: 绕行而非穿越。"""
  16. g = LearnedGrid()
  17. for x in range(14, 30, 2):
  18. g.mark_blocked((x, 10))
  19. path = astar((10, 10), (30, 10), g)
  20. assert path is not None
  21. for cell in path:
  22. assert cell not in g.blocked
  23. assert path[-1][0] >= 28
  24. def test_astar_prefers_learned_open_cells() -> None:
  25. """Given: 两条等长路, 其一全为熟路 When: A* Then: 优先熟路。"""
  26. g = LearnedGrid()
  27. for x in range(10, 30, 2):
  28. g.mark_open((x, 10))
  29. g.mark_open((x + 1, 11)) # 熟路中间格也算开放
  30. for x in range(10, 30, 2):
  31. g.mark_open((x, 14))
  32. path = astar((10, 12), (30, 12), g)
  33. assert path is not None
  34. # 路径代价存在即可, 关键是不会崩溃且终点可达
  35. assert path[-1][0] >= 28
  36. def test_astar_returns_empty_when_already_there() -> None:
  37. """Given: 起点即终点附近 When: A* Then: 空路径。"""
  38. g = LearnedGrid()
  39. assert astar((10, 10), (11, 11), g) == []
  40. def test_astar_none_when_fully_blocked() -> None:
  41. """Given: 目标四周 4 格厚禁行带 When: A* Then: None(放弃)。
  42. 带厚须 >goal 宽松判定半径(≤2), 否则 A* 在带外即视为到达(合法)。
  43. """
  44. g = LearnedGrid()
  45. goal = (30, 10)
  46. for dx in range(-4, 5):
  47. for dy in range(-4, 5):
  48. g.mark_blocked((goal[0] + dx, goal[1] + dy))
  49. path = astar((10, 10), goal, g, max_expand=200)
  50. assert path is None
  51. def test_load_walls_from_d401_map() -> None:
  52. """Given: d401.map(比奇矿区 200x200) When: 加载墙格 Then: 与实测地形吻合。
  53. 2026-09-29 格式定案回归: 列主序(x*h+y)*12 + wBkImg bit15=墙。
  54. 真值锚点(真机实测, /tmp/cap_mine_walk.jsonl 提取):
  55. - 379 实测可走点 0 误标墙
  56. - 936 SC28 弹回墙 98% 在墙集内(19 格不吻合=超速弹回误标)
  57. """
  58. d401 = Path(__file__).resolve().parents[1] / "assets" / "maps" / "d401.map"
  59. if not d401.exists():
  60. return # 地图文件不存在跳过(runner 环境)
  61. walls = load_walls_from_map(str(d401))
  62. # d401 200x200 = 40000 格, 79.4% 是墙(横长洞穴图, 岩体为主)
  63. assert len(walls) == 31776
  64. # 真值 spot-check: 实测走过点必须可走(真机 /tmp/cap_mine_walk.jsonl)
  65. for cell in [(20, 127), (83, 10), (57, 58), (183, 103)]:
  66. assert cell not in walls, f"实测可走点 {cell} 被误标墙"
  67. # 真值 spot-check: SC28 弹回墙必须命中(assets/mine_sc28_walls.json 吻合格)
  68. for cell in [(11, 150), (43, 136), (149, 166)]:
  69. assert cell in walls, f"SC28 实测墙 {cell} 未命中"
  70. def test_mark_from_map_blocks_walls_in_astar() -> None:
  71. """Given: LearnedGrid + 地图墙格 When: mark_from_map + A* Then: 路径避开墙。"""
  72. g = LearnedGrid()
  73. g.mark_from_map({(0, 0), (2, 0), (0, 2)}) # 角点墙
  74. path = astar((1, 1), (3, 1), g)
  75. # 即使 (0, 0) 周边有墙, 路径从 (1, 1) 出发应能找到 (3, 1) 附近
  76. # (可能无路 — 但 mark_from_map 至少不能 crash)
  77. if path is not None:
  78. for cell in path:
  79. assert cell not in {(0, 0), (2, 0), (0, 2)}