Spaces:
Running
Running
| """Per-turn goal tracking for playback. | |
| Two complementary views of "how is the agent doing toward the | |
| objective", recorded every turn so a replay shows the trajectory of | |
| intent, not just the final verdict: | |
| * ``leaf_progress`` — walks the scenario's declarative win-condition | |
| tree and, for every leaf predicate, reports current vs. target and a | |
| 0..1 ratio. Scenario-specific, directly tied to *this* objective. | |
| * ``reward_vector`` — a fixed, normalized, monotone-cumulative vector | |
| (economy / military / territory / scouting / objective) that is | |
| scenario-agnostic and therefore comparable across runs and on the | |
| leaderboard. Mirrors the training rollout's per-turn reward_vector. | |
| ``turn_goal`` bundles both side by side plus a scalar | |
| ``objective_progress`` and the boolean ``won``. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| from .scenarios.win_conditions import LEAF_KEYS, WinContext, evaluate | |
| # Current-value extractors for the numeric leaf predicates. Anything not | |
| # listed is treated as a pure boolean gate (ratio = 1.0 iff satisfied). | |
| _CURRENT: dict[str, Any] = { | |
| "explored_pct_gte": lambda c: c.signals.explored_percent, | |
| "enemies_discovered_gte": lambda c: len(c.signals.enemies_seen_ids), | |
| "buildings_discovered_gte": lambda c: len(c.signals.enemy_buildings_seen_ids), | |
| "units_killed_gte": lambda c: c.signals.units_killed, | |
| "enemy_buildings_destroyed_gte": lambda c: getattr( | |
| c.signals, "enemy_buildings_destroyed", 0 | |
| ), | |
| "units_lost_lte": lambda c: c.signals.units_lost, | |
| "within_ticks": lambda c: c.signals.game_tick, | |
| "after_ticks": lambda c: c.signals.game_tick, | |
| "own_units_gte": lambda c: len(c.render_state.get("units_summary", []) or []), | |
| "cash_gte": lambda c: c.signals.cash, | |
| "resources_gte": lambda c: c.signals.resources, | |
| "economy_value_gte": lambda c: c.signals.cash + c.signals.resources, | |
| "power_surplus_gte": lambda c: c.signals.power_provided | |
| - c.signals.power_drained, | |
| "buildings_owned_gte": lambda c: len(c.signals.own_building_types), | |
| "building_total_gte": lambda c: len(c.signals.own_buildings), | |
| } | |
| # `_lte` predicates are satisfied while *below* the bound (constraints). | |
| _LTE = frozenset({"units_lost_lte", "within_ticks"}) | |
| def _clamp(x: float) -> float: | |
| return 0.0 if x < 0.0 else 1.0 if x > 1.0 else float(x) | |
| def _leaves(node: Any) -> list[dict]: | |
| """Flatten a win-condition tree to its leaf predicate dicts.""" | |
| if node is None: | |
| return [] | |
| if not isinstance(node, dict): | |
| node = dict(getattr(node, "__pydantic_extra__", {}) or {}) | |
| out: list[dict] = [] | |
| for k, v in node.items(): | |
| if k in ("all_of", "any_of"): | |
| for child in v: | |
| out.extend(_leaves(child)) | |
| elif k == "not": | |
| out.extend(_leaves(v)) | |
| elif k in LEAF_KEYS: | |
| out.append({k: v}) | |
| return out | |
| def leaf_progress(win_condition: Any, ctx: WinContext) -> list[dict]: | |
| rows: list[dict] = [] | |
| for leaf in _leaves(win_condition): | |
| (name, target), = leaf.items() | |
| satisfied = bool(evaluate({name: target}, ctx)) | |
| cur_fn = _CURRENT.get(name) | |
| if cur_fn is None or not isinstance(target, (int, float)): | |
| rows.append({ | |
| "name": name, "target": target, "current": None, | |
| "ratio": 1.0 if satisfied else 0.0, "satisfied": satisfied, | |
| }) | |
| continue | |
| cur = cur_fn(ctx) | |
| tgt = float(target) | |
| if name in _LTE: | |
| ratio = 1.0 if satisfied else _clamp(tgt / cur) if cur else 1.0 | |
| else: | |
| ratio = 1.0 if satisfied else (_clamp(cur / tgt) if tgt else 0.0) | |
| rows.append({ | |
| "name": name, "target": target, "current": cur, | |
| "ratio": round(ratio, 4), "satisfied": satisfied, | |
| }) | |
| return rows | |
| def reward_vector(signals: Any) -> dict[str, float]: | |
| """Normalized, scenario-agnostic cumulative progress vector. | |
| All dimensions are 0..1 and (because the underlying signals are | |
| monotone over an episode) non-decreasing turn over turn — a true | |
| cumulative tracker, comparable across scenarios. | |
| """ | |
| cash = getattr(signals, "cash", 0) + getattr(signals, "resources", 0) | |
| kills = getattr(signals, "units_killed", 0) | |
| seen = len(getattr(signals, "enemies_seen_ids", []) or []) + len( | |
| getattr(signals, "enemy_buildings_seen_ids", []) or [] | |
| ) | |
| return { | |
| "economy": round(_clamp(cash / 10000.0), 4), | |
| "military": round(_clamp(kills / 10.0), 4), | |
| "territory": round(_clamp( | |
| getattr(signals, "explored_percent", 0.0) / 100.0), 4), | |
| "scouting": round(_clamp(seen / 10.0), 4), | |
| "objective": 0.0, # filled by turn_goal once the win cond is known | |
| } | |
| def turn_goal(win_condition: Any, ctx: WinContext) -> dict: | |
| leaves = leaf_progress(win_condition, ctx) | |
| won = bool(evaluate(win_condition, ctx)) | |
| obj = 1.0 if won else ( | |
| round(sum(r["ratio"] for r in leaves) / len(leaves), 4) | |
| if leaves else 0.0 | |
| ) | |
| rv = reward_vector(ctx.signals) | |
| rv["objective"] = obj | |
| return { | |
| "leaves": leaves, | |
| "reward_vector": rv, | |
| "objective_progress": obj, | |
| "won": won, | |
| } | |