"""对局统计收集:MOVE 失败 reason 分布 / Worker 排队 / Core 被占时长。 接入方式:evaluate_live(..., stats=stats) —— 循环里每 tick 喂 sample(), 评估结束调用 summary() 输出报告(与 fitness 一起打印,数据驱动后续优化)。 """ from collections import Counter class GameStats: def __init__(self): self.move_fail_reasons = Counter() # MOVE_BLOCKED reason 分布 self.terrain_blocked = 0 # 撞地形障碍总 tick(TERRAIN 类) self.core_occupied_ticks = 0 # Core 格被非满载单位占的总 tick self.core_occupied_events = 0 # 连续占格事件次数 self._core_occ_streak = 0 self._core_occ_max_streak = 0 self.queue_unit_ticks = 0 # 满载 Worker 距 Core<=4 的累计单位-tick self.queue_sightings = 0 # 满载 Worker 距 Core<=4 的观测次数 self.deposited = 0 # DEPOSIT 成功次数 self.harvested = 0 # 采集成功次数 self.heal_count = 0 # 单位治疗次数(消耗 Core 资源) self.spawn_count = 0 # 生产次数(消耗 Core 资源) self.core_damaged = 0 # Core 受伤次数(触发维修/治疗消耗) # ---- KPI ---- self.move_ok = 0 # 移动成功次数(MOVED 事件) self.move_fail = 0 # 移动失败次数(MOVE_BLOCKED 事件) self.core_zero_res_ticks = 0 # Core 资源=0 的总 tick self._zero_streak = 0 self._zero_max = 0 self.worker_full_ticks = 0 # Worker 满载累计 tick(往返时长近似) self._full_from = {} # uid -> 满载起始 tick self.worker_full_trips = 0 # 满载→空载的完成次数 def sample(self, obs): """每 tick 调用一次(obs = 我方 build_observation)。""" if obs.core is None: return core = tuple(obs.core["pos"]) # 1) 移动成功/失败 + reason 分布 for ev in obs.prev_events or []: et = ev.get("type") if et == "MOVE_BLOCKED": self.move_fail += 1 self.move_fail_reasons[ev.get("reason") or "(unknown)"] += 1 elif et == "MOVED": self.move_ok += 1 elif et in ("DEPOSITED", "DEPOSIT"): self.deposited += 1 elif et in ("HARVESTED", "HARVEST_SUCCEEDED"): self.harvested += 1 elif et == "UNIT_HEALED": self.heal_count += 1 elif et == "UNIT_SPAWNED": self.spawn_count += 1 elif et == "CORE_DAMAGED": self.core_damaged += 1 # 2) Core 格被非满载单位占(DEPOSIT 中的满载 Worker 是正常行为) on_core = [u for u in obs.units if tuple(u["pos"]) == core and not (u["utype"] == "WORKER" and u["cargo"] > 0)] if on_core: self.core_occupied_ticks += 1 self._core_occ_streak += 1 self._core_occ_max_streak = max(self._core_occ_max_streak, self._core_occ_streak) elif self._core_occ_streak > 0: self.core_occupied_events += 1 self._core_occ_streak = 0 # 3) 满载 Worker 排队负载(距 Core<=4 的单位-tick) for u in obs.units: if u["utype"] == "WORKER" and u["cargo"] > 0 \ and abs(u["pos"][0] - core[0]) + abs(u["pos"][1] - core[1]) <= 4: self.queue_unit_ticks += 1 self.queue_sightings += 1 # 4) Core 资源=0 连续时长(KPI:经济枯竭风险) if obs.core["resources"] == 0: self.core_zero_res_ticks += 1 self._zero_streak += 1 self._zero_max = max(self._zero_max, self._zero_streak) else: self._zero_streak = 0 # 5) Worker 满载→空载周期(往返时长近似 KPI) now_full = {} for u in obs.units: if u["utype"] == "WORKER" and u["cargo"] > 0: now_full[u["uid"]] = True if u["uid"] not in self._full_from: self._full_from[u["uid"]] = obs.tick for uid in list(self._full_from): if uid not in now_full: self.worker_full_ticks += obs.tick - self._full_from[uid] self.worker_full_trips += 1 del self._full_from[uid] def summary(self): """输出统计报告(与 fitness 一起展示)。""" total_fail = sum(self.move_fail_reasons.values()) dist = {r: round(n / total_fail * 100, 1) if total_fail else 0 for r, n in self.move_fail_reasons.most_common()} return { "move_fail_total": total_fail, "reason_dist": dist, "core_occupied_ticks": self.core_occupied_ticks, "core_occupied_events": self.core_occupied_events, "core_occ_max_streak": self._core_occ_max_streak, "queue_avg_tick": (self.queue_unit_ticks / max(1, self.queue_sightings)), "queue_unit_ticks": self.queue_unit_ticks, "deposited": self.deposited, "harvested": self.harvested, "heal_count": self.heal_count, "spawn_count": self.spawn_count, "core_damaged": self.core_damaged, "move_ok": self.move_ok, "move_fail": self.move_fail, "move_success_rate": (self.move_ok / max(1, self.move_ok + self.move_fail)), "core_zero_res_max": self._zero_max, "core_zero_res_ticks": self.core_zero_res_ticks, "worker_roundtrip_avg": (self.worker_full_ticks / max(1, self.worker_full_trips)), "worker_full_trips": self.worker_full_trips, "heal_per_1000tick": self.heal_count, } def print_report(self, label="对局统计"): s = self.summary() print(f"\n== {label} ==") print(f" MOVE 失败总数: {s['move_fail_total']}") for r, pct in s["reason_dist"].items(): print(f" {r}: {pct}%") print(f" Core 被非满载单位占: {s['core_occupied_ticks']} tick" f"({s['core_occupied_events']} 次,最长连续 {s['core_occ_max_streak']} tick)") print(f" 满载 Worker 排队: 平均 {s['queue_avg_tick']:.2f} 单位/观测" f"(累计 {s['queue_unit_ticks']} 单位-tick)") print(f" DEPOSIT {s['deposited']} 次 / HARVEST {s['harvested']} 次") print(f" 治疗 {s['heal_count']} 次 / 生产 {s['spawn_count']} 次 / Core 受伤 {s['core_damaged']} 次") print(f" MOVE 成功率: {s['move_success_rate']*100:.1f}%" f"(成功 {s['move_ok']} / 失败 {s['move_fail']})") print(f" Core 资源=0: 最长连续 {s['core_zero_res_max']} tick" f"(累计 {s['core_zero_res_ticks']} tick)") print(f" Worker 满载周期: 平均 {s['worker_roundtrip_avg']:.1f} tick" f"({s['worker_full_trips']} 次)")