Spaces:
Running
Running
File size: 11,984 Bytes
2149b73 1b09aea 2149b73 1b09aea 2149b73 62068da 83b8781 5695506 aa17a67 62068da 83b8781 aa17a67 83b8781 1b09aea 2149b73 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | """LiveAdapter 离线测试:用 SDK 真实模型伪造 Turn,验证正式世界适配层。
正式世界的事件名/字段和模拟器不同,这层翻译错了策略就是瞎的,但线上没法回归,
所以在这里离线锁住。
运行:.venv/bin/python tests/test_deploy_adapter.py
"""
import sys
from pathlib import Path
from uuid import UUID
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from arena_hero import Turn
from arena_hero.actions import CancelMoveAction, CommandPlan, DropBeaconAction
from arena_hero.enums import BeaconStatus, CoreState, PlayerStatus, UnitType
from arena_hero.models import (
ChampionBeacon, CoreView, PlayerState, ResolutionEvent, TerrainView, UnitView,
)
from deploy import LiveAdapter
from strategies.heuristic import HeuristicStrategy, make_default_genes
W_ID = UUID("00000000-0000-0000-0000-0000000000a1")
C_ID = UUID("00000000-0000-0000-0000-0000000000c1")
E_ID = UUID("00000000-0000-0000-0000-0000000000e1")
def make_turn(*, tick=10, worker_pos=(5, 5), core_pos=(4, 5), events=(),
beacon_status=None, carrier_id=None, obstacles=(), resources=()):
objects = [
UnitView(kind="UNIT", id=W_ID, controlled=True, position=worker_pos,
hp=2, unit_type=UnitType.WORKER, cargo=0),
CoreView(kind="CORE", id=C_ID, controlled=True, owner_username="tester",
position=core_pos, hp=5, shield=5, state=CoreState.NORMAL),
]
if obstacles:
objects.append(TerrainView(kind="OBSTACLE", positions=tuple(obstacles)))
if resources:
objects.append(TerrainView(kind="RESOURCE", positions=tuple(resources)))
state = PlayerState(
status=PlayerStatus.ACTIVE, respawn_at_tick=None, resources=7,
population=1,
# rules v0.14 / SDK 0.2.9:population_tier/upkeep_next_tick 已删除
champion_beacon=ChampionBeacon(position=(0, 0), status=beacon_status,
carrier_id=carrier_id),
objects=tuple(objects), events=tuple(events),
)
return Turn(tick=tick, state=state, submitter=lambda *a, **k: None)
def mk_adapter():
return LiveAdapter(HeuristicStrategy(genes=make_default_genes(), bounds=None))
# ----------------------------------------------------------------------
def test_move_failed_becomes_move_blocked():
"""UNIT_MOVE_FAILED(actor_id) + 上一 tick 提交的方向 → MOVE_BLOCKED。"""
a = mk_adapter()
a._sent_dirs = {W_ID: "RIGHT"} # 模拟上一 tick 提交过 MOVE RIGHT
ev = ResolutionEvent(event_id=UUID(int=1), tick=9,
event_type="UNIT_MOVE_FAILED",
reason_code="MOVE_BLOCKED_TERRAIN",
actor_id=W_ID, target_id=None, position=(5, 5),
values=None)
out = a.translate_events(make_turn(events=[ev]))
assert len(out) == 1, out
assert out[0] == {"type": "MOVE_BLOCKED", "obj_id": W_ID,
"direction": "RIGHT",
"reason": "MOVE_BLOCKED_TERRAIN",
"source_type": "UNIT_MOVE_FAILED",
"blocked_cell": (6, 5)}, out[0]
def test_move_failed_without_known_direction_is_skipped():
"""方向不明时必须跳过,否则会把错误的格子学成障碍。"""
a = mk_adapter()
ev = ResolutionEvent(event_id=UUID(int=2), tick=9,
event_type="UNIT_MOVE_FAILED", reason_code="MOVE_CONTESTED",
actor_id=W_ID, target_id=None, position=(5, 5), values=None)
out = a.translate_events(make_turn(events=[ev]))
assert out == [], out
def test_move_succeeded_becomes_moved():
a = mk_adapter()
ev = ResolutionEvent(event_id=UUID(int=3), tick=9,
event_type="UNIT_MOVE_SUCCEEDED", reason_code=None,
actor_id=W_ID, target_id=None, position=(6, 5), values=None)
out = a.translate_events(make_turn(events=[ev]))
assert out[0]["type"] == "MOVED" and out[0]["obj_id"] == W_ID, out
def test_core_move_failed_uses_migration_direction():
a = mk_adapter()
a._core_dir = "UP"
ev = ResolutionEvent(event_id=UUID(int=4), tick=9,
event_type="CORE_MOVE_FAILED",
reason_code="CORE_DESTINATION_TERRAIN_BLOCKED",
actor_id=C_ID, target_id=None, position=(4, 5), values=None)
out = a.translate_events(make_turn(events=[ev]))
assert out[0] == {"type": "MOVE_BLOCKED", "obj_id": C_ID, "direction": "UP",
"reason": "CORE_DESTINATION_TERRAIN_BLOCKED",
"source_type": "CORE_MOVE_FAILED",
"blocked_cell": (4, 4)}, out
def test_carries_beacon_derived_from_carrier_id():
a = mk_adapter()
turn = make_turn(beacon_status=BeaconStatus.CARRIED, carrier_id=W_ID)
obs = a.build_observation(turn)
assert obs.units[0]["carries_beacon"] is True
turn2 = make_turn(beacon_status=BeaconStatus.CARRIED, carrier_id=E_ID)
obs2 = a.build_observation(turn2)
assert obs2.units[0]["carries_beacon"] is False
def test_beacon_status_compares_as_plain_string():
"""策略里是 obs.beacon["status"] == "GROUND",StrEnum 必须能直接比。"""
a = mk_adapter()
obs = a.build_observation(make_turn(beacon_status=BeaconStatus.GROUND))
assert obs.beacon["status"] == "GROUND"
obs2 = a.build_observation(make_turn())
assert obs2.beacon["status"] == "UNKNOWN"
def test_v014_observation_and_status_have_no_upkeep_fields():
"""Removed v0.13 maintenance fields must not leak into local adapters."""
a = mk_adapter()
turn = make_turn()
obs = a.build_observation(turn)
assert not hasattr(obs, "population_tier")
assert not hasattr(obs, "upkeep_next_tick")
status = a.build_status(turn.tick, obs, {"units": {}, "core": None})
assert "upkeep_next" not in status
def test_events_come_from_current_turn_not_stale_copy():
"""turn.events 已经是上一 Tick 的结果,不能再延迟一拍。"""
a = mk_adapter()
a._sent_dirs = {W_ID: "RIGHT"}
ev = ResolutionEvent(event_id=UUID(int=5), tick=9,
event_type="UNIT_MOVE_FAILED", reason_code="X",
actor_id=W_ID, target_id=None, position=(5, 5), values=None)
obs = a.build_observation(make_turn(events=[ev]))
assert obs.prev_events and obs.prev_events[0]["type"] == "MOVE_BLOCKED"
def test_memory_learns_obstacle_end_to_end():
"""整条链路:连续两次撞同一格 → 临时避让(temp_blocked),
不学成永久障碍——MOVE_BLOCKED 无法区分地形与单位占位,
永久化会产生假障碍把可达资源围死。"""
a = mk_adapter()
for _ in range(2):
a._sent_dirs = {W_ID: "RIGHT"}
ev = ResolutionEvent(event_id=UUID(int=6), tick=9,
event_type="UNIT_MOVE_FAILED",
reason_code="MOVE_BLOCKED_TERRAIN",
actor_id=W_ID, target_id=None, position=(5, 5),
values=None)
obs = a.build_observation(make_turn(events=[ev]))
a.strategy.decide(obs)
assert (6, 5) in a.strategy.mem.temp_blocked, a.strategy.mem.temp_blocked
assert (6, 5) not in a.strategy.mem.obstacles, a.strategy.mem.obstacles
def test_dead_own_unit_is_classified_from_previous_observation():
"""The dead Unit is absent from the new state but remains ours for logging."""
from types import SimpleNamespace
a = mk_adapter()
a._known_own_ids = {W_ID, C_ID}
messages = []
a._log_key_event = lambda ev, et, ids, obs=None: messages.append(
a._fmt_key_event(ev, et, ids, obs))
death = ResolutionEvent(
event_id=UUID(int=7), tick=10, event_type="UNIT_DAMAGED",
reason_code="ATTACK", actor_id=None, target_id=W_ID,
position=(5, 5), values={"damage": 1, "hp": 0})
empty_obs = SimpleNamespace(units=[], core={"uid": C_ID}, enemies=[])
a.translate_events(make_turn(events=[death]), obs=empty_obs)
assert messages and "我方单位阵亡" in messages[0], messages
assert a.recent_events[-1]["target_is_own"] is True
def _run_all():
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
passed = 0
for t in tests:
try:
t()
print(f"PASS {t.__name__}")
passed += 1
except AssertionError as e:
print(f"FAIL {t.__name__}: {e}")
except Exception as e:
print(f"ERROR {t.__name__}: {type(e).__name__}: {e}")
print(f"{passed}/{len(tests)} passed")
return passed == len(tests)
def test_memory_from_dict_stationary_pos_tuple():
"""回归:memory.json 恢复的 stationary.pos 必须是 tuple——
否则 _pick_raid_target 返回 list → _raid_point → pf.find key 含 list
崩溃(2026-08-07 agent2 线上每 tick TypeError)。"""
from strategies.base import Memory
m = Memory.from_dict({
"stationary": [["core_abc", {"pos": [1, 2], "count": 3,
"first": 10, "last": 20}]]})
st = m.stationary["core_abc"]
assert isinstance(st["pos"], tuple), st["pos"]
assert st["pos"] == (1, 2)
def test_pathfinder_accepts_list_pos():
"""回归:pf.find 必须接受 list 坐标(调用方可能传 list)。"""
from strategies.base import Pathfinder
pf = Pathfinder(is_obstacle=lambda x, y: False, bounds=(-64, 63, -64, 63))
path = pf.find([0, 0], [3, 0])
assert path and tuple(path[0]) == (1, 0)
def test_legacy_result_cannot_override_frozen_v014_genes():
import json
import tempfile
from deploy import load_genes
payload = {"history": [{"gen": 3, "best": 99.0, "genes": {
"max_population": 19, "selfdestruct_pop": 19,
"worker_ratio": 0.6, "obsolete_gene": 123}}]}
with tempfile.NamedTemporaryFile("w", suffix=".json") as fh:
json.dump(payload, fh)
fh.flush()
genes = load_genes(fh.name)
assert genes["max_population"] == 32
assert genes["selfdestruct_pop"] == 999
assert genes["worker_ratio"] == 0.6
assert "obsolete_gene" not in genes
def test_status_exposes_manual_core_control():
a = mk_adapter()
a.strategy.set_manual_core_migration("RIGHT")
turn = make_turn()
obs = a.build_observation(turn)
status = a.build_status(turn.tick, obs, {"units": {}, "core": None})
assert status["core_control"] == {
"movement_mode": "migration",
"mode": "fixed", "direction": "RIGHT",
"heading": "RIGHT", "step_direction": None, "phase": "stationed",
"active": False,
"paused": False, "wait_reason": None,
}
def test_status_stationed_mode_blocks_migration():
a = mk_adapter()
a.strategy.station_core()
a.strategy.set_manual_core_migration("RIGHT")
turn = make_turn()
obs = a.build_observation(turn)
status = a.build_status(turn.tick, obs, {"units": {}, "core": None})
assert status["core_control"]["movement_mode"] == "stationed"
assert status["core_control"]["phase"] == "stationed"
assert status["core_control"]["active"] is False
assert status["core_control"]["paused"] is False
def test_core_move_start_failed_uses_submitted_direction():
a = mk_adapter()
a._core_dir = "UP"
a._sent_core_dir = "RIGHT"
ev = ResolutionEvent(
event_id=UUID(int=8), tick=9,
event_type="CORE_MOVE_START_FAILED",
reason_code="CORE_DESTINATION_TERRAIN_BLOCKED",
actor_id=C_ID, target_id=None, position=(4, 5), values=None)
out = a.translate_events(make_turn(events=[ev]))
assert out[0]["type"] == "MOVE_BLOCKED"
assert out[0]["direction"] == "RIGHT"
assert out[0]["blocked_cell"] == (5, 5)
assert out[0]["source_type"] == "CORE_MOVE_START_FAILED"
if __name__ == "__main__":
sys.exit(0 if _run_all() else 1)
|