run_juece13_mine30.py 11 KB

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