run_juece13_cave30_v2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. """run_juece13_cave30_v2.py — 角色【新角色】→ 天然洞穴一层 → 杀 30 怪(全在洞内)。
  2. v2 修复(v1 实测教训 2026-09-29 14:41):
  3. - v1 病根: 进洞后 allow_reroll=True → 视野无怪 45s 就重走 renwu 链"换落点"
  4. → 角色频繁被任务跳转切换落脚点, 从未认真跑图找怪
  5. - v2 铁律: 【进洞后绝不再点任务跳转】allow_reroll=False + 覆写换落点分支
  6. → 无怪 = 认真跑图(CS5284 问服务器怪位 + 新颖度锚点 + 90s 换远锚)
  7. - 进洞手段: 出生异图时 force_act 强制 renwu_mov,2(不读弹窗 — 任务指针
  8. 已推进到任务三, 弹窗返回矿区 act_move,1 会被 v1 误点) → 落地洞穴即停用
  9. - 计数闸门: 只有 天然洞穴一层 内击杀计入 30; 异图击杀仅记日志
  10. 用法:
  11. uv run python scripts/run_juece13_cave30_v2.py -u longwu988 -p Longwu1988 \
  12. --zone 14570166 --name 新角色 --kill 30
  13. """
  14. from __future__ import annotations
  15. import argparse
  16. import logging
  17. import sys
  18. import time
  19. from typing import override
  20. import requests
  21. from pbot.bot.kill import LAND_QUIET_S, KillBehavior
  22. from pbot.bot.runner import run
  23. from pbot.config import Account
  24. from pbot.errors import PbotError
  25. from pbot.game.session import enter_world
  26. from pbot.login.box_client import enter_game_client, login
  27. from pbot.transport.gateway import login_gate_with_retry
  28. from pbot.types import ZoneId
  29. from pbot.world.mirror import WorldMirror
  30. _LOG = logging.getLogger("juece13_cave_v2")
  31. _PORTS: tuple[int, ...] = (8021, 8061, 8001, 8041)
  32. CAVE_MAP = "天然洞穴一层"
  33. def setup_logging() -> None:
  34. """全量 INFO 日志(框架每步/每刀/每杀均有日志)。"""
  35. logging.basicConfig(
  36. level=logging.INFO,
  37. format="%(asctime)s %(levelname)-7s %(name)s: %(message)s",
  38. stream=sys.stdout,
  39. )
  40. def banner(msg: str, *args: object) -> None:
  41. """阶段分隔条日志。"""
  42. _LOG.info("======== %s ========", msg % args if args else msg)
  43. class CaveStayKillBehavior(KillBehavior):
  44. """洞穴驻留杀怪: 进洞后永不点任务跳转; 无怪纯跑图找怪; 仅洞内计杀。"""
  45. def _in_cave(self, world: WorldMirror) -> bool:
  46. """是否在目标洞穴图。"""
  47. return world.me.map_name == CAVE_MAP
  48. @override
  49. def on_enter(self, conn: GatewayConnection, world: WorldMirror) -> None:
  50. """异图 → force_act 强制 renwu_mov,2 进洞(一次); 洞内 → 直接开打。"""
  51. self._world = world
  52. self._last_step_t = time.time()
  53. self._declare_guaji(conn)
  54. if self._bound is None:
  55. self._bound = ((world.me.x, world.me.y), 25)
  56. self._load_map_if_needed(world)
  57. if self._in_cave(world):
  58. self._force_act = True # 洞内: 后续所有 1011 都强指 renwu_mov,2
  59. self._allow_reroll = False # ★ 铁律: 进洞后绝不允许"换落点重走链"
  60. _LOG.info("[洞穴] 出生即在 %s @(%d,%d) — 静置%.1fs后开打(任务跳转已禁用)",
  61. CAVE_MAP, world.me.x, world.me.y, LAND_QUIET_S)
  62. self._enter("fight")
  63. self._phase_t = time.time() + LAND_QUIET_S
  64. else:
  65. # 异图(等待图/比奇/矿区): force_act=True 强制 renwu_mov,2,
  66. # 不读任务弹窗(弹窗是任务三 act_move,1 → 会传矿区, v1 死循环根因)
  67. self._force_act = True
  68. self._allow_reroll = True
  69. self._renwu_act = "renwu_mov,2"
  70. _LOG.info("[进洞] 当前 %s @(%d,%d) — force_act 强制 renwu_mov,2 传送",
  71. world.me.map_name, world.me.x, world.me.y)
  72. self._enter("renwu_3031")
  73. @override
  74. def tick(self, conn: GatewayConnection, world: WorldMirror) -> object:
  75. """关键相位守卫:
  76. - fight/goto 恒在洞内(入口保证); 传送意外出洞 → 只允许走 renwu 链回洞
  77. - allow_reroll=False 时框架的无怪分支自动走"持续跑图", 但其撞墙分支
  78. _run_toward/_wander_step 里还有 4 处 `_enter(\"renwu_3031\")` 换落点
  79. — 洞内全部覆写为"弃路径换锚点继续跑图"
  80. """
  81. was_cave = self._in_cave(world)
  82. result = super().tick(conn, world)
  83. now_cave = self._in_cave(world)
  84. if was_cave and not now_cave:
  85. # 意外出洞(理论上不可能: 洞内不点跳转) — 高声报警
  86. _LOG.warning("[!] 意外出洞: %s @(%d,%d) — 下 tick 走 renwu 链回洞",
  87. now_cave and world.me.map_name, world.me.x, world.me.y)
  88. # 洞内防呆: 若框架仍在洞内把相位拨回 renwu_3031(所有 reroll 入口),
  89. # 强制掰回 fight — 300ms 决策节拍下这层守卫是最后防线
  90. if now_cave and self._phase in ("renwu_3031", "renwu_1011", "wait_map"):
  91. _LOG.warning("[防呆] 洞内相位=%s — 掰回 fight(任务跳转禁用)",
  92. self._phase)
  93. self._enter("fight")
  94. self._phase_t = time.time() + LAND_QUIET_S
  95. _ = conn
  96. return result
  97. @override
  98. def _run_toward(self, conn: GatewayConnection, world: WorldMirror,
  99. tx: int, ty: int) -> bool:
  100. """覆写: 洞内"累计弹回换落点重走链"分支 → 改为弃路径换锚点。"""
  101. me = world.me
  102. if self._in_cave(world):
  103. # 拦截父类会把相位拨回 renwu_3031 的两个入口(rej_total>=4 与
  104. # A* 连续无路) — 先记下父类调用前的相位, 调用后掰正
  105. before = self._phase
  106. result = super()._run_toward(conn, world, tx, ty)
  107. if self._phase in ("renwu_3031", "renwu_1011") and \
  108. before not in ("renwu_3031", "renwu_1011"):
  109. _LOG.info("[洞内] 父类触发换落点 — 已拦截, 改为弃路径换锚点跑图")
  110. self._enter("fight")
  111. self._phase_t = time.time() # fight 相位无静置惩罚
  112. self._path = None
  113. self._search_anchor = None
  114. self._waypoint = None
  115. return result
  116. return super()._run_toward(conn, world, tx, ty)
  117. @override
  118. def _wander_step(self, conn: GatewayConnection, world: WorldMirror,
  119. now: float) -> None:
  120. """覆写: 洞内漫游的"累计弹回换落点"分支 → 改为就地换向。"""
  121. if self._in_cave(world):
  122. before = self._phase
  123. super()._wander_step(conn, world, now)
  124. if self._phase in ("renwu_3031", "renwu_1011") and \
  125. before not in ("renwu_3031", "renwu_1011"):
  126. _LOG.info("[洞内] 漫游触发换落点 — 已拦截, 就地继续跑图")
  127. self._enter("fight")
  128. self._phase_t = now
  129. self._path = None
  130. self._search_anchor = None
  131. return
  132. super()._wander_step(conn, world, now)
  133. @override
  134. def _current_target(self, world: WorldMirror) -> object:
  135. """非洞穴 → 永不锁定目标。"""
  136. if not self._in_cave(world):
  137. return None
  138. return super()._current_target(world)
  139. @override
  140. def _pick_target(self, world: WorldMirror) -> object:
  141. """非洞穴 → 永不选怪(tick 会走无怪分支 → 异图触发 renwu 链回洞)。"""
  142. if not self._in_cave(world):
  143. return None
  144. return super()._pick_target(world)
  145. @override
  146. def _credit_kill(self, world: WorldMirror, aid: int, name: str,
  147. signal: str) -> None:
  148. """计杀闸门: 只有洞内击杀计入 30/30。"""
  149. if not self._in_cave(world):
  150. _LOG.warning("[异图击杀不计] aid=%d %s 信号=%s @%s",
  151. aid, name, signal, world.me.map_name)
  152. _ = self._my_hits.pop(aid, None)
  153. return
  154. super()._credit_kill(world, aid, name, signal)
  155. def parse_args() -> argparse.Namespace:
  156. """命令行: 账号/区/角色/目标击杀数。"""
  157. ap = argparse.ArgumentParser()
  158. _ = ap.add_argument("-u", "--user", required=True)
  159. _ = ap.add_argument("-p", "--password", required=True)
  160. _ = ap.add_argument("--zone", type=int, required=True)
  161. _ = ap.add_argument("--name", required=True)
  162. _ = ap.add_argument("--kill", type=int, default=30)
  163. _ = ap.add_argument("--login-port", type=int, default=0)
  164. return ap.parse_args()
  165. def main() -> int:
  166. """登录 → 进游戏 → 喝药确认 → 洞穴驻留杀 30。"""
  167. a = parse_args()
  168. setup_logging()
  169. banner("[0] 启动: 区=%d 角色=%s 目标=%d 杀(仅限%s)",
  170. a.zone, a.name, a.kill, CAVE_MAP)
  171. http = requests.Session()
  172. http.trust_env = False
  173. http.verify = False
  174. banner("[1] 盒子登录")
  175. jwt, uid = login(Account(username=a.user, password=a.password))
  176. _LOG.info("[✓] uid=%d", uid)
  177. banner("[2] EnterGameClient zone=%d", a.zone)
  178. for attempt in range(4):
  179. try:
  180. _ = enter_game_client(http, jwt, ZoneId(a.zone))
  181. _LOG.info("[✓] 上报成功")
  182. break
  183. except Exception as e:
  184. if attempt == 3:
  185. raise
  186. _LOG.warning("[!] 第%d次失败(%s), 12s 后重试", attempt + 1, e)
  187. time.sleep(12)
  188. banner("[3] 登录服选角: %s", a.name)
  189. ports = (a.login_port,) if a.login_port else _PORTS
  190. key32, sid, real_name, ticket_key, game_port = login_gate_with_retry(
  191. ZoneId(a.zone), jwt, uid, a.name, ports)
  192. _LOG.info("[票据] key32=%s sid=%d 角色=%s 游戏端口=%d",
  193. key32.decode(errors="replace"), sid, real_name, game_port)
  194. banner("[4] 进游戏")
  195. stop: list[bool] = [False]
  196. conn, world, updater = enter_world(ZoneId(a.zone), real_name, ticket_key,
  197. sid, game_port, uid, stop=stop)
  198. _LOG.info("[✓] 出生 map=%s @(%d,%d)", world.me.map_name, world.me.x,
  199. world.me.y)
  200. try:
  201. banner("[5] 喝药保护确认")
  202. from pbot.game.heal import enable_auto_heal
  203. state = enable_auto_heal(conn, sid, want_on=True, world=world)
  204. if state == "on":
  205. _LOG.info("[✓] 喝药保护已开启")
  206. else:
  207. _LOG.warning("[!] 喝药状态=%s(不阻断)", state)
  208. banner("[6] 洞穴驻留杀怪: 目标 %d @%s", a.kill, CAVE_MAP)
  209. kill = CaveStayKillBehavior(
  210. sid=sid,
  211. kill_target=a.kill,
  212. renwu_act="renwu_mov,2",
  213. allow_reroll=True, # 行为内部按图态动态覆写(洞内恒 False)
  214. )
  215. duration = max(1200.0, a.kill * 150.0)
  216. _LOG.info("行为超时上限: %.0fs", duration)
  217. result = run(conn, world, updater, kill, duration_s=duration)
  218. banner("[7] 结果")
  219. _LOG.info("result=%s 洞内击杀=%d/%d 最终 map=%s @(%d,%d)",
  220. result.value, world.kills, a.kill,
  221. world.me.map_name, world.me.x, world.me.y)
  222. top = sorted(world.frame_counts.items(), key=lambda kv: -kv[1])[:15]
  223. _LOG.info("收帧Top15: %s", top)
  224. ok = world.kills >= a.kill and world.me.map_name == CAVE_MAP
  225. _LOG.info("[%s] %s", "✓" if ok else "✗",
  226. f"完成: {world.kills} 杀全部在 {CAVE_MAP}" if ok else "未达标")
  227. return 0 if ok else 1
  228. finally:
  229. stop[0] = True
  230. conn.close()
  231. _LOG.info("连接已关闭")
  232. if __name__ == "__main__":
  233. try:
  234. sys.exit(main())
  235. except PbotError as e:
  236. _LOG.error("失败: %s", e.detail)
  237. sys.exit(1)