| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- """AutoFightBehavior 客户端对齐重构验证(真客户端 Lua 反推的行为规格)。
- 规格来源(2026-09-30 客户端逆向定案):
- - AutoFindMonsterCommand.lua: 选怪=全视野 squLen 最近, 跳同格怪,
- IsHaveMaster(宠物)永不打
- - RequestLaunchCommand.lua: 普攻 minDis=1 — dist=0 客户端本地拒绝发包;
- 不在出刀位只发移动不发攻击
- - AutoProxy.lua SetAFKTargetDeath: 击杀后原地等 0.2s 再重扫
- - AssistProxy.lua checkAimlessMove: 无怪漫游=±50 随机中距点(|dx|+|dy|>20),
- 不是 BFS 最远点
- """
- import json
- import sys
- import time
- from pathlib import Path
- sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
- import pytest
- from run_juece13_wugong_autofight import (
- ATTACK_INTERVAL,
- ATTACK_RADIUS,
- KILL_WAIT_S,
- PAPET_MIN_ID,
- AutoFightBehavior,
- )
- from pbot.world.actor import ActorKey, Monster
- from pbot.world.mirror import WorldMirror
- from tests.test_wander import SpyConn
- MINE = "比奇矿区"
- def mk_mon(key: int, x: int, y: int, *, name: str = "矿区僵尸",
- dead: bool = False) -> Monster:
- return Monster(key=ActorKey(key), x=x, y=y, race=1049, name=name,
- is_dead=dead)
- def mine_world(x: int = 100, y: int = 100) -> WorldMirror:
- w = WorldMirror()
- w.me.map_name = MINE
- w.me.map_id = "d401"
- w.me.actor_key = 7
- w.me.uid = "14570166001790661813000091989"
- w.me.x, w.me.y = x, y
- w.me.hp, w.me.max_hp = 500, 575
- return w
- def fresh_af(spy: SpyConn, world: WorldMirror, **kw: object) -> AutoFightBehavior:
- """空闲态 AutoFightBehavior: 步进门全开, 事件清空."""
- walls = kw.pop("walls", {(0, 0), (0, 1)}) # type: ignore[arg-type]
- af = AutoFightBehavior(sid=99, map_name=MINE, walls=walls, **kw) # pyright: ignore[reportArgumentType]
- af.on_enter(spy, world)
- af._last_step_t = 0.0
- af._last_attack_t = 0.0
- world.events.clear()
- spy.sent.clear()
- spy.pres.clear()
- return af
- def tick_until(af: AutoFightBehavior, spy: SpyConn, world: WorldMirror,
- n: int = 1) -> None:
- for _ in range(n):
- af.tick(spy, world) # pyright: ignore[reportArgumentType]
- world.events.clear()
- # ---------- 选怪: 真客户端过滤链 ----------
- def test_pick_ignores_pet_ids() -> None:
- """7 位数短ID=宠物/玩家量级(客户端 IsHaveMaster 等价) — 永不选."""
- w = mine_world()
- w.monsters[PAPET_MIN_ID + 12345] = mk_mon(PAPET_MIN_ID + 12345, 101, 100)
- w.monsters[56751] = mk_mon(56751, 105, 100)
- af = fresh_af(SpyConn(), w)
- m = af._pick_monster(w)
- assert m is not None and int(m.key) == 56751
- def test_pick_nearest_in_full_view() -> None:
- """全视野(服务器推送范围)取最近, 不受旧 VISION=8 环扫限制."""
- w = mine_world()
- w.monsters[111] = mk_mon(111, 110, 100) # dist 10 — 旧版扫不到
- w.monsters[222] = mk_mon(222, 118, 100) # dist 18
- af = fresh_af(SpyConn(), w)
- m = af._pick_monster(w)
- assert m is not None and int(m.key) == 111
- def test_pick_skips_same_cell_monster() -> None:
- """同格(dist=0)怪客户端直接跳过(FindNearestMonster 明确排除)."""
- w = mine_world()
- w.monsters[111] = mk_mon(111, 100, 100) # 与我同格
- w.monsters[222] = mk_mon(222, 103, 100)
- af = fresh_af(SpyConn(), w)
- m = af._pick_monster(w)
- assert m is not None and int(m.key) == 222
- def test_pick_skips_dead_and_blacklisted() -> None:
- w = mine_world()
- w.monsters[111] = mk_mon(111, 101, 100, dead=True)
- w.monsters[222] = mk_mon(222, 102, 100)
- af = fresh_af(SpyConn(), w)
- af._big_monsters.add(222)
- assert af._pick_monster(w) is None
- # ---------- 逼近: 先走位后出刀, 永不进怪格 ----------
- def test_approach_last_step_is_walk_not_run() -> None:
- """dist=2 时发 3011(1格走) — 真客户端最后一步永远 1 格, 防冲进怪格."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[56751] = mk_mon(56751, 102, 100) # dist=2
- af = fresh_af(spy, w)
- tick_until(af, spy, w, n=3)
- walks = [f for f in spy.sent if f[0] == 3011]
- runs = [f for f in spy.sent if f[0] == 3013]
- assert walks, "dist=2 必须发 3011 走步"
- assert not runs, "dist=2 不允许 3013 跑步(会冲到 dist=0)"
- def test_approach_never_steps_into_monster_cell() -> None:
- """逼近终点=怪相邻格: 任何情况下发出的移动落点都不等于怪坐标."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[56751] = mk_mon(56751, 104, 100) # dist=4
- af = fresh_af(spy, w)
- t = 1000.0
- for _ in range(8):
- af._now_override = t
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- pre = spy.pres[-1] if spy.pres else 0
- w.me.x, w.me.y = pre & 0xFFFF, (pre >> 16) & 0xFFFF
- af._pending = False
- af._last_step_t = 0.0
- w.events.clear()
- t += 1.0
- if max(abs(w.monsters[56751].x - w.me.x),
- abs(w.monsters[56751].y - w.me.y)) <= 1:
- break
- assert (w.me.x, w.me.y) != (104, 100), "绝不允许踩进怪格(dist=0 不结算)"
- assert max(abs(104 - w.me.x), abs(100 - w.me.y)) <= 1
- def test_attack_only_in_range_and_interval() -> None:
- """dist=1(出刀位)才发 3014; dist=0 不发(客户端本地拒绝)."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[56751] = mk_mon(56751, 101, 100) # dist=1
- af = fresh_af(spy, w)
- tick_until(af, spy, w, n=2)
- attacks = [f for f in spy.sent if f[0] == 3014]
- assert attacks, "dist=1 应立即出刀"
- # dist=0: 同格怪根本不该被选为目标
- w2 = mine_world(101, 100)
- w2.monsters[56751] = mk_mon(56751, 101, 100)
- spy2 = SpyConn()
- af2 = fresh_af(spy2, w2)
- tick_until(af2, spy2, w2, n=2)
- assert af2._target_key != 56751
- assert not [f for f in spy2.sent if f[0] == 3014], \
- "同格怪不许打(打不中且浪费节拍)"
- def test_attack_interval_enforced() -> None:
- """出刀间隔内不重复发 3014(客户端 CD≈0.9s, 我们沿用 ATTACK_INTERVAL)."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[56751] = mk_mon(56751, 101, 100)
- af = fresh_af(spy, w)
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- n1 = len([f for f in spy.sent if f[0] == 3014])
- af._now_override = time.time() + 0.05 # 间隔内
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- n2 = len([f for f in spy.sent if f[0] == 3014])
- assert n1 == 1 and n2 == 1, "间隔内不应再出刀"
- assert KILL_WAIT_S == 0.2 # AutoProxy 定案: 击杀后等 0.2s
- # ---------- 击杀后: 0.2s 原地等待 ----------
- def test_kill_then_wait_window() -> None:
- """目标死亡计杀后, KILL_WAIT_S 窗口内不选新怪不漫游."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[56751] = mk_mon(56751, 101, 100)
- w.monsters[999] = mk_mon(999, 105, 100)
- af = fresh_af(spy, w, kill_goal=5)
- tick_until(af, spy, w, n=2)
- assert af._target_key == 56751
- # SC31 hp=0 → is_dead
- w.monsters[56751] = mk_mon(56751, 101, 100, dead=True)
- af._now_override = time.time() + 1.0
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- w.events.clear()
- assert af.kills == 1
- assert af._kill_wait_until > 0, "击杀后应进入等待窗口"
- af._now_override = time.time() + 1.05 # 仍在等待窗口内
- spy.sent.clear()
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- assert af._target_key is None, "等待窗口内不应锁新目标"
- assert not [f for f in spy.sent if f[0] == 3014]
- af._now_override = af._kill_wait_until + 0.01 # 窗口已过
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- assert af._target_key == 999
- # ---------- 漫游: 随机中距点(真客户端 checkAimlessMove) ----------
- def test_roam_random_midrange_not_farthest() -> None:
- """无怪漫游: ±50 内随机中距点(|dx|+|dy|>20), 不再 BFS 最远点."""
- spy = SpyConn()
- w = mine_world(100, 100)
- af = fresh_af(spy, w, farthest_fn=None)
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- assert af._roam_dest is not None
- dx = abs(af._roam_dest[0] - 100)
- dy = abs(af._roam_dest[1] - 100)
- assert dx <= 50 and dy <= 50, "漫游点必须在 ±50 内"
- assert dx + dy > 20, "漫游点必须是中距点(太近=原地抖动)"
- def test_roam_dest_never_negative() -> None:
- """run9 实证: 出生在 (28,28) 时随机点出负坐标(-8,58)/(48,-12) —
- A* 必失败 → 0.6s 连刷 5 个漫游点空转. 漫游点必须钳制在地图内."""
- for _ in range(200):
- spy = SpyConn()
- w = mine_world(5, 5) # 地图角落地带
- af = fresh_af(spy, w, farthest_fn=None)
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- assert af._roam_dest is not None
- x, y = af._roam_dest
- assert x >= 0 and y >= 0, f"负坐标漫游点 ({x},{y})"
- def test_vision_no_longer_caps_targeting() -> None:
- """锁定 18 格外的怪不被弃目标(旧 VISION+2=10 会弃)."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[111] = mk_mon(111, 118, 100)
- af = fresh_af(spy, w)
- tick_until(af, spy, w, n=1)
- assert af._target_key == 111
- af._now_override = time.time() + 5.0
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- assert af._target_key == 111, "全视野目标不应因距离被弃"
- # ---------- SC10 宠物过滤(update.py) ----------
- def test_sc10_rejects_pet_ids() -> None:
- """SC10: 人形(race 0/1/150)入 humanoids 登记不入怪表; 7位数宠物量级
- 非人形 race 的仍拒(cap_client_mine2: 5-6位数才是怪)."""
- import json as _json
- from pbot.codec.bitbuf import encode6
- from pbot.codec.frame import Frame
- from pbot.world.update import WorldUpdater
- def sc10(info0: str, race: int) -> Frame:
- body = encode6(_json.dumps(
- {"info": [info0, "x", 1, 0, 1], "X": 100, "Y": 100,
- "race": race}).encode())
- return Frame(msg_id=10, recog=race, p1=(100 << 16) | 100, p2=0,
- p3=0, tail=0, pre=0, body=body)
- w = WorldMirror()
- w.me.map_name = MINE
- up = WorldUpdater()
- up.apply(w, sc10("288999", 150)) # 人形(英雄/玩家)
- assert 288999 not in w.monsters
- assert 288999 in w.humanoids
- up.apply(w, sc10("56751", 1049)) # 5位数怪
- assert 56751 in w.monsters
- def test_attack_radius_is_one() -> None:
- """近战结算距离=1(普攻 minDis=maxDis=1, 服务器权威)."""
- assert ATTACK_RADIUS == 1
- def test_skill_rotation_on_consecutive_hits() -> None:
- """真客户端攻击流(cap_client_mine2): skillID 在 {0,7,12,25,26} 轮换
- (普攻/刺杀/攻杀/半月/烈火) — 连续出刀轮换技能提高结算伤害."""
- spy = SpyConn()
- w = mine_world(100, 100)
- w.monsters[56751] = mk_mon(56751, 101, 100)
- af = fresh_af(spy, w)
- t = 1000.0
- seen = []
- for _i in range(8):
- af._now_override = t
- af.tick(spy, w) # pyright: ignore[reportArgumentType]
- seen.extend(
- json.loads(f[5])["skillID"]
- for f in spy.sent if f[0] == 3014)
- spy.sent.clear()
- t += ATTACK_INTERVAL + 0.01
- assert set(seen) >= {0, 12, 25, 26}, f"技能应轮换, 实际: {seen}"
- @pytest.mark.parametrize("bad", [PAPET_MIN_ID, PAPET_MIN_ID * 9])
- def test_pet_min_id_threshold(bad: int) -> None:
- """PAPET_MIN_ID 起为宠物/玩家量级."""
- assert bad >= PAPET_MIN_ID
- # ---------- run14 事故固化: 低血逃跑死循环 / hp=0 待机 ----------
- def test_flee_target_blacklisted_not_recomputed_same_point() -> None:
- """run14: 逃跑点(向量*30)拉黑后, 下一 tick 必须算出新逃跑点
- (垂直扰动), 而非同向量重算同点 → 60ms 死循环 24s 挨打致死."""
- w = mine_world(x=216, y=37)
- w.me.hp, w.me.max_hp = 300, 628
- w.monsters[98172] = mk_mon(98172, 217, 38, name="黑色恶蛆")
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- af.tick(spy, w)
- first = af._roam_dest
- assert first is not None, "低血必须立即产生逃跑点"
- # 模拟该点 A* 失败被拉黑
- af._roam_blacklist[first] = 1e9
- af.tick(spy, w)
- second = af._roam_dest
- assert second is not None and second not in af._roam_blacklist
- assert second != first, "拉黑后不得重算同一个逃跑点"
- def test_fleeing_never_selects_target() -> None:
- """run14: 逃跑中选怪→弃目标 60ms 循环 — 逃跑态禁止选怪."""
- w = mine_world(x=216, y=37)
- w.me.hp, w.me.max_hp = 300, 628 # < FLEE_RATIO=0.55
- w.monsters[98172] = mk_mon(98172, 217, 38)
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- for _ in range(5):
- af.tick(spy, w)
- assert af._target_key is None, "低血逃跑中绝不允许锁定目标"
- def test_dead_wait_blocks_all_actions() -> None:
- """run14: hp=0 死亡后仍出刀 4 次 — me_die 事件进入待机态,
- 屏蔽选怪/攻击/漫游直到换图 DONE."""
- from pbot.world.mirror import ActorEvent
- w = mine_world(x=216, y=37)
- w.me.hp, w.me.max_hp = 0, 628
- w.monsters[98172] = mk_mon(98172, 217, 38)
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- w.events.append(ActorEvent(kind="me_die", actor=None, detail="by=98217"))
- af.tick(spy, w)
- assert af._dead_wait is True
- w.events.clear()
- spy.sent.clear()
- for _ in range(3):
- af.tick(spy, w)
- assert not spy.sent, "死亡待机态不得发送任何帧(攻击/移动均禁止)"
- # ---------- 49区 T9 定案(2026-10-03): 逃跑迟滞带 + 低血风暴黑区 ----------
- def test_flee_hysteresis_band_no_reentry() -> None:
- """HP 回到 55%~75% 迟滞带不得恢复战斗(旧版单阈值: 逃2s→回→
- 3s后又逃, 7min 振荡 219 次)."""
- w = mine_world()
- w.me.hp, w.me.max_hp = 280, 500 # 0.56 — 已过 FLEE 线未到 RECOVER
- w.monsters[98172] = mk_mon(98172, 105, 100)
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- # 先制造 fleeing_last=True(上一 tick 真低血)
- w.me.hp = 260 # 0.52 < 0.55
- af.tick(spy, w)
- assert af._fleeing_last is True
- # 回血到迟滞带(0.56): 不得选怪(旧版在此恢复战斗)
- w.me.hp = 280 # 0.56
- af.tick(spy, w)
- assert af._target_key is None, "迟滞带内不得恢复战斗选怪"
- assert af._fleeing_last is True, "迟滞带维持逃跑态"
- def test_flee_recovery_at_75pct() -> None:
- """HP ≥75% 才恢复战斗, 记录恢复时刻供风暴窗口判定."""
- w = mine_world()
- w.monsters[98172] = mk_mon(98172, 105, 100)
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- w.me.hp, w.me.max_hp = 260, 500 # 低血
- af.tick(spy, w)
- assert af._fleeing_last is True
- af._now_override = 1010.0
- w.me.hp = 400 # 0.8 ≥ 0.75
- af.tick(spy, w)
- assert af._fleeing_last is False
- assert af._recover_t == 1010.0
- assert af._target_key is not None, "回满后应恢复选怪"
- def test_flee_storm_blacklists_region() -> None:
- """恢复战斗后 30s 内再低血 = 区域压制 — 黑掉当前区远跳,
- 不再原地逃→战→逃循环."""
- w = mine_world()
- w.monsters[98172] = mk_mon(98172, 105, 100)
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- w.me.hp, w.me.max_hp = 260, 500 # 低血 → 逃
- af.tick(spy, w)
- af._now_override = 1005.0
- w.me.hp = 400 # 恢复战斗
- af.tick(spy, w)
- assert af._target_key is not None
- af._now_override = 1015.0 # 10s 后 <30s 窗口
- w.me.hp = 260 # 又低血 → 风暴
- af.tick(spy, w)
- assert af._target_key is None
- center = (w.me.x // 16, w.me.y // 16)
- assert center in af._region_blacklist, "风暴必须拉黑当前区域"
- assert af._roam_dest is not None
- # ---------- 50区 cq/骷髅精灵定案(2026-10-03): 磨杀超时 ----------
- def test_engage_timeout_blacklists_elite() -> None:
- """500血精英每刀都结算但 60s 杀不死 — 超时拉黑换怪(实测磨17分钟)."""
- w = mine_world()
- w.monsters[98172] = mk_mon(98172, 101, 100, name="骷髅精灵")
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- af.tick(spy, w)
- assert af._target_key == 98172
- assert af._combat_start_t[98172] == 1000.0
- af._now_override = 1030.0 # 30s: 未超时, 继续打
- af.tick(spy, w)
- assert af._target_key == 98172
- af._now_override = 1061.0 # 61s: 超时 → 拉黑弃目标
- af.tick(spy, w)
- assert af._target_key is None, "60s 磨不死必须弃目标"
- assert 98172 in af._big_monsters, "精英必须拉黑(会话内不再选)"
- def test_engage_timeout_not_reset_by_relock() -> None:
- """重锁不清零磨杀计时(骷髅精灵 51 次重锁正是案例) — 从首次锁定起算."""
- w = mine_world()
- w.monsters[98172] = mk_mon(98172, 101, 100, name="骷髅精灵")
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- af.tick(spy, w)
- assert af._combat_start_t[98172] == 1000.0
- # 多轮"重锁→重选同一只"(tick 内零结算4刀会自行触发重锁)
- for t in (1010.0, 1020.0, 1030.0, 1040.0, 1050.0):
- af._now_override = t
- af._target_key = None # 模拟重锁置空
- af.tick(spy, w) # 重新选怪(最近=同一只)
- # tick 末尾可能又因零结算重锁置空 — 用计时断言而非目标断言
- assert af._combat_start_t.get(98172) == 1000.0, \
- "重锁不得清零磨杀计时"
- af._now_override = 1061.0 # 首锁后 61s → 超时拉黑
- af.tick(spy, w)
- assert af._target_key is None
- assert 98172 in af._big_monsters
- def test_engage_timer_resets_after_long_gap() -> None:
- """54区 353s 案例: 弃目标 6 分钟后重选同一只怪 — 旧计时不得
- 直接触发拉黑, 重选间隔>ENGAGE_TIMEOUT 即视为新一轮交战."""
- w = mine_world()
- w.monsters[98172] = mk_mon(98172, 101, 100, name="沃玛战士")
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- af.tick(spy, w)
- assert af._combat_start_t[98172] == 1000.0
- af._target_key = None # 被抢/低血弃目标
- af._now_override = 1400.0 # 400s 后重选(>60s)
- af.tick(spy, w)
- assert af._combat_start_t[98172] == 1400.0, "长间隔重选必须重置计时"
- af._now_override = 1430.0 # 新一轮 30s: 未超时
- af.tick(spy, w)
- assert af._target_key == 98172, "新计时未到 60s 不得拉黑"
- # ---------- 51区 cq/蜈蚣洞定案(2026-10-03): 逃跑墙点 + 漫游熔断 ----------
- def test_flee_never_picks_blocked_cell() -> None:
- """±30 盲算候选整组是墙((231,1)(171,1)(231,61)(171,61) 实证) —
- 墙点必须被跳过, 半距衰减后仍全墙则原地喝药而非撞墙循环."""
- w = mine_world(x=201, y=31)
- w.me.hp, w.me.max_hp = 150, 344 # 低血
- w.monsters[55555] = mk_mon(55555, 200, 30, name="蜈蚣")
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- # 把 ±30 全部 4 方向候选标成墙(模拟 d601 那组死点)
- for dx, dy in ((-1, -1), (1, -1), (-1, 1), (1, 1)):
- af._grid.mark_blocked((201 + dx * 30, 31 + dy * 30))
- af.tick(spy, w)
- dest = af._roam_dest
- if dest is not None:
- assert dest not in af._grid.blocked, "逃跑点不得是墙格"
- assert dest not in ((171, 1), (231, 1), (171, 61), (231, 61))
- # 半距(15/8)有活路则必选之; 全墙则 None(原地喝药)
- def test_roam_fail_streak_trips_breaker() -> None:
- """连续 12 次漫游不可达 = 区域对 A* 已废 — 黑区+清黑名单+远跳,
- 不再 150ms 高频刷点(51区实测 400 次/分钟站桩 10+ 分钟)."""
- w = mine_world(x=196, y=98)
- spy = SpyConn()
- af = fresh_af(spy, w)
- af._now_override = 1000.0
- # 把 me 周围 ±51 全标墙(采样域内全不可达)
- for x in range(140, 250):
- for y in range(40, 150):
- af._grid.mark_blocked((x, y))
- for i in range(13):
- af._now_override = 1000.0 + i * 0.2
- af.tick(spy, w)
- center = (196 // 16, 98 // 16)
- assert center in af._region_blacklist, "12 连不可达必须黑区"
- assert len(af._roam_blacklist) < 12, "熔断时清空采样黑名单"
|