Spaces:
Running
Training-parity minimap (real terrain + legend) + viewer (system/thinking/debrief)
Browse filesModel-facing fix โ 'is the minimap correctly sent?': it now is, with
an interpretable legend.
- minimap.terrain_png_for(): extract map.png bytes from the resolved
.oramap (zip), lru-cached
- minimap.render_b64(): prefer the training render_minimap (real
terrain + embedded legend the model can read) when terrain is
available; bench-native PNG only as fallback
- ModelAgent(base_map=โฆ) resolves terrain once; _user_message +
eval_core playback both render via the training path so the saved
PNGs match exactly what the model saw; run_eval factories pass
base_map
Viewer parity with training view_v7_pipeline.py โ show everything:
- battle_viewer.episode_view exposes system_prompt, debrief (the
briefing/scoped re-prompt handed to the model), tool_result
- app.py Battle Viewer: collapsible system prompt (char count),
open DEBRIEF/briefing (full, interrupt-tagged), reasoning(thinking),
tool calls, tool result, signals, goal
- Streamlit scripts/view_playback.py: system-prompt expander + model/
run caption (same format)
tests: test_minimap.py +2 (terrain extract + training renderer used,
agent terrain wiring). 15 touched-module tests green.
- app.py +23 -7
- openra_bench/agent.py +20 -6
- openra_bench/battle_viewer.py +18 -3
- openra_bench/eval_core.py +4 -1
- openra_bench/minimap.py +50 -0
- openra_bench/playback_view.py +17 -0
- openra_bench/run_eval.py +2 -0
- tests/test_minimap.py +34 -0
|
@@ -247,22 +247,38 @@ def _bv_turn_md(v: dict, heading: str) -> str:
|
|
| 247 |
"reward vector: "
|
| 248 |
+ " ".join(f"`{k}={float(x):.2f}`" for k, x in rv.items()),
|
| 249 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
if v.get("reasoning"):
|
| 251 |
-
lines += ["", "**reasoning**",
|
| 252 |
-
|
| 253 |
if v.get("assistant_text"):
|
| 254 |
lines += ["", "**model said**", str(v["assistant_text"])]
|
| 255 |
cmds = v.get("commands", [])
|
| 256 |
-
lines += ["", "**
|
| 257 |
"\n".join(cmds) if cmds else "(none)") + "\n```"]
|
|
|
|
|
|
|
| 258 |
sig = v.get("signals", {})
|
| 259 |
if sig:
|
| 260 |
lines += ["signals: " + " ".join(
|
| 261 |
f"`{k}={sig[k]}`" for k in sig)]
|
| 262 |
-
if v.get("briefing"):
|
| 263 |
-
b = str(v["briefing"])
|
| 264 |
-
lines += ["", "<details><summary>briefing</summary>\n\n```\n"
|
| 265 |
-
+ b[:2000] + "\n```\n</details>"]
|
| 266 |
return "\n\n".join(s for s in lines if s != "")
|
| 267 |
|
| 268 |
|
|
|
|
| 247 |
"reward vector: "
|
| 248 |
+ " ".join(f"`{k}={float(x):.2f}`" for k, x in rv.items()),
|
| 249 |
]
|
| 250 |
+
# System prompt (the deterministic scenario knowledge the model
|
| 251 |
+
# was given) โ collapsible, shown with char count like the
|
| 252 |
+
# training pipeline viewer.
|
| 253 |
+
sp = str(v.get("system_prompt") or "")
|
| 254 |
+
if sp:
|
| 255 |
+
lines += [
|
| 256 |
+
"", f"<details><summary>๐ง system prompt ({len(sp)} chars)"
|
| 257 |
+
"</summary>\n\n```\n" + sp[:6000] + "\n```\n</details>"
|
| 258 |
+
]
|
| 259 |
+
# DEBRIEF / briefing actually handed to the model this turn
|
| 260 |
+
# (post-interrupt this is the scoped re-prompt).
|
| 261 |
+
deb = str(v.get("debrief") or v.get("briefing") or "")
|
| 262 |
+
if deb:
|
| 263 |
+
tag = "โก DEBRIEF (interrupt)" if v.get("interrupt") else "briefing"
|
| 264 |
+
lines += [
|
| 265 |
+
"", f"<details open><summary>{tag}</summary>\n\n```\n"
|
| 266 |
+
+ deb[:8000] + "\n```\n</details>"
|
| 267 |
+
]
|
| 268 |
if v.get("reasoning"):
|
| 269 |
+
lines += ["", "**๐ค reasoning (thinking)**",
|
| 270 |
+
"> " + str(v["reasoning"]).replace("\n", "\n> ")]
|
| 271 |
if v.get("assistant_text"):
|
| 272 |
lines += ["", "**model said**", str(v["assistant_text"])]
|
| 273 |
cmds = v.get("commands", [])
|
| 274 |
+
lines += ["", "**tool calls**", "```\n" + (
|
| 275 |
"\n".join(cmds) if cmds else "(none)") + "\n```"]
|
| 276 |
+
if v.get("tool_result"):
|
| 277 |
+
lines += [f"tool result: `{v['tool_result']}`"]
|
| 278 |
sig = v.get("signals", {})
|
| 279 |
if sig:
|
| 280 |
lines += ["signals: " + " ".join(
|
| 281 |
f"`{k}={sig[k]}`" for k in sig)]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
return "\n\n".join(s for s in lines if s != "")
|
| 283 |
|
| 284 |
|
|
@@ -374,13 +374,16 @@ def build_briefing(render_state: dict, objective: str = "") -> str:
|
|
| 374 |
return "\n".join(lines)
|
| 375 |
|
| 376 |
|
| 377 |
-
def _render_minimap_b64(
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
| 380 |
try:
|
| 381 |
-
from .minimap import
|
| 382 |
|
| 383 |
-
return
|
| 384 |
except Exception as e: # noqa: BLE001 โ vision is optional
|
| 385 |
logger.debug("minimap render skipped: %s", e)
|
| 386 |
return None
|
|
@@ -466,11 +469,22 @@ class ModelAgent:
|
|
| 466 |
objective: str = "",
|
| 467 |
provider: ChatProvider | None = None,
|
| 468 |
system_extra: str = "",
|
|
|
|
| 469 |
):
|
| 470 |
self.cfg = cfg
|
| 471 |
self.objective = objective
|
| 472 |
self.tools = _tool_schemas(allowed_tools)
|
| 473 |
self.provider = provider or make_provider(cfg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
sys_content = SYSTEM_PROMPT
|
| 475 |
if system_extra:
|
| 476 |
# Deterministic scenario-scoped game knowledge (glossary,
|
|
@@ -483,7 +497,7 @@ class ModelAgent:
|
|
| 483 |
def _user_message(self, render_state: dict) -> dict:
|
| 484 |
text = build_briefing(render_state, self.objective)
|
| 485 |
if self.cfg.vision:
|
| 486 |
-
b64 = _render_minimap_b64(render_state)
|
| 487 |
if b64:
|
| 488 |
return {
|
| 489 |
"role": "user",
|
|
|
|
| 374 |
return "\n".join(lines)
|
| 375 |
|
| 376 |
|
| 377 |
+
def _render_minimap_b64(
|
| 378 |
+
render_state: dict, terrain_png: bytes | None = None
|
| 379 |
+
) -> str | None:
|
| 380 |
+
"""Best-effort minimap PNG. With `terrain_png` uses the training
|
| 381 |
+
renderer (real terrain + an embedded legend the model can read);
|
| 382 |
+
else the bench fallback. None โ graceful text-only."""
|
| 383 |
try:
|
| 384 |
+
from .minimap import render_b64
|
| 385 |
|
| 386 |
+
return render_b64(render_state, terrain_png)
|
| 387 |
except Exception as e: # noqa: BLE001 โ vision is optional
|
| 388 |
logger.debug("minimap render skipped: %s", e)
|
| 389 |
return None
|
|
|
|
| 469 |
objective: str = "",
|
| 470 |
provider: ChatProvider | None = None,
|
| 471 |
system_extra: str = "",
|
| 472 |
+
base_map: str = "",
|
| 473 |
):
|
| 474 |
self.cfg = cfg
|
| 475 |
self.objective = objective
|
| 476 |
self.tools = _tool_schemas(allowed_tools)
|
| 477 |
self.provider = provider or make_provider(cfg)
|
| 478 |
+
# Real terrain (map.png from the .oramap) so the model gets the
|
| 479 |
+
# training renderer's terrain+legend minimap, not colour dots.
|
| 480 |
+
self._terrain: bytes | None = None
|
| 481 |
+
if base_map:
|
| 482 |
+
try:
|
| 483 |
+
from .minimap import terrain_png_for
|
| 484 |
+
|
| 485 |
+
self._terrain = terrain_png_for(base_map)
|
| 486 |
+
except Exception: # noqa: BLE001
|
| 487 |
+
self._terrain = None
|
| 488 |
sys_content = SYSTEM_PROMPT
|
| 489 |
if system_extra:
|
| 490 |
# Deterministic scenario-scoped game knowledge (glossary,
|
|
|
|
| 497 |
def _user_message(self, render_state: dict) -> dict:
|
| 498 |
text = build_briefing(render_state, self.objective)
|
| 499 |
if self.cfg.vision:
|
| 500 |
+
b64 = _render_minimap_b64(render_state, self._terrain)
|
| 501 |
if b64:
|
| 502 |
return {
|
| 503 |
"role": "user",
|
|
@@ -145,9 +145,20 @@ def episode_view(ep_dir: str | Path, turn_idx: int) -> dict:
|
|
| 145 |
return {"n_turns": 0, "manifest": ep["manifest"]}
|
| 146 |
i = max(0, min(turn_idx, n - 1))
|
| 147 |
t = turns[i]
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
g = t.get("goal") or {}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
return {
|
| 152 |
"n_turns": n,
|
| 153 |
"turn_idx": i,
|
|
@@ -156,11 +167,15 @@ def episode_view(ep_dir: str | Path, turn_idx: int) -> dict:
|
|
| 156 |
"interrupt": t.get("interrupt"),
|
| 157 |
"minimap_png": t.get("minimap_png"),
|
| 158 |
"ascii_minimap": t.get("ascii_minimap", ""),
|
| 159 |
-
"
|
|
|
|
|
|
|
| 160 |
"reasoning": (assts[i].get("reasoning", "")
|
| 161 |
if i < len(assts) else ""),
|
| 162 |
"assistant_text": (_text(assts[i].get("content"))
|
| 163 |
if i < len(assts) else ""),
|
|
|
|
|
|
|
| 164 |
"commands": t.get("commands", []),
|
| 165 |
"signals": t.get("signals", {}),
|
| 166 |
"goal": g,
|
|
|
|
| 145 |
return {"n_turns": 0, "manifest": ep["manifest"]}
|
| 146 |
i = max(0, min(turn_idx, n - 1))
|
| 147 |
t = turns[i]
|
| 148 |
+
msgs = ep["messages"]
|
| 149 |
+
sys_prompt = next(
|
| 150 |
+
(m.get("content", "") for m in msgs if m.get("role") == "system"), ""
|
| 151 |
+
)
|
| 152 |
+
if isinstance(sys_prompt, list):
|
| 153 |
+
sys_prompt = _text(sys_prompt)
|
| 154 |
+
users = _role_turns(msgs, "user")
|
| 155 |
+
assts = _role_turns(msgs, "assistant")
|
| 156 |
+
tools = _role_turns(msgs, "tool")
|
| 157 |
g = t.get("goal") or {}
|
| 158 |
+
# The model's "debrief" for a turn = the briefing it was handed
|
| 159 |
+
# (post-interrupt this is the scoped re-prompt). Surface it
|
| 160 |
+
# explicitly + the interrupt reason, matching the training viewer.
|
| 161 |
+
briefing = _text(users[i]["content"]) if i < len(users) else ""
|
| 162 |
return {
|
| 163 |
"n_turns": n,
|
| 164 |
"turn_idx": i,
|
|
|
|
| 167 |
"interrupt": t.get("interrupt"),
|
| 168 |
"minimap_png": t.get("minimap_png"),
|
| 169 |
"ascii_minimap": t.get("ascii_minimap", ""),
|
| 170 |
+
"system_prompt": sys_prompt,
|
| 171 |
+
"briefing": briefing,
|
| 172 |
+
"debrief": briefing, # alias: what the model was (re-)briefed with
|
| 173 |
"reasoning": (assts[i].get("reasoning", "")
|
| 174 |
if i < len(assts) else ""),
|
| 175 |
"assistant_text": (_text(assts[i].get("content"))
|
| 176 |
if i < len(assts) else ""),
|
| 177 |
+
"tool_result": (_text(tools[i].get("content"))
|
| 178 |
+
if i < len(tools) else ""),
|
| 179 |
"commands": t.get("commands", []),
|
| 180 |
"signals": t.get("signals", {}),
|
| 181 |
"goal": g,
|
|
@@ -219,8 +219,11 @@ def run_level(
|
|
| 219 |
_png = None
|
| 220 |
try:
|
| 221 |
from .agent import _render_minimap_b64
|
|
|
|
| 222 |
|
| 223 |
-
_png = _render_minimap_b64(
|
|
|
|
|
|
|
| 224 |
except Exception: # noqa: BLE001 โ playback never breaks a run
|
| 225 |
pass
|
| 226 |
from .goal_tracker import turn_goal
|
|
|
|
| 219 |
_png = None
|
| 220 |
try:
|
| 221 |
from .agent import _render_minimap_b64
|
| 222 |
+
from .minimap import terrain_png_for
|
| 223 |
|
| 224 |
+
_png = _render_minimap_b64(
|
| 225 |
+
rs, terrain_png_for(compiled.scenario.base_map)
|
| 226 |
+
)
|
| 227 |
except Exception: # noqa: BLE001 โ playback never breaks a run
|
| 228 |
pass
|
| 229 |
from .goal_tracker import turn_goal
|
|
@@ -12,9 +12,59 @@ from __future__ import annotations
|
|
| 12 |
import base64
|
| 13 |
import io
|
| 14 |
import logging
|
|
|
|
|
|
|
| 15 |
|
| 16 |
logger = logging.getLogger(__name__)
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
CELL = 6 # px per map cell (โ768ร240 for a 128ร40 map โ legible)
|
| 19 |
|
| 20 |
# (R,G,B)
|
|
|
|
| 12 |
import base64
|
| 13 |
import io
|
| 14 |
import logging
|
| 15 |
+
import zipfile
|
| 16 |
+
from functools import lru_cache
|
| 17 |
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
| 20 |
+
|
| 21 |
+
@lru_cache(maxsize=32)
|
| 22 |
+
def terrain_png_for(base_map: str) -> bytes | None:
|
| 23 |
+
"""Raw `map.png` bytes from the resolved `.oramap` (a zip), cached.
|
| 24 |
+
None when the map can't be resolved โ caller falls back."""
|
| 25 |
+
if not base_map:
|
| 26 |
+
return None
|
| 27 |
+
try:
|
| 28 |
+
from .scenarios.loader import resolve_map_path
|
| 29 |
+
|
| 30 |
+
p = resolve_map_path(base_map)
|
| 31 |
+
if not p:
|
| 32 |
+
return None
|
| 33 |
+
with zipfile.ZipFile(p, "r") as zf:
|
| 34 |
+
if "map.png" in zf.namelist():
|
| 35 |
+
return zf.read("map.png")
|
| 36 |
+
except Exception as e: # noqa: BLE001
|
| 37 |
+
logger.debug("terrain extract failed for %s: %s", base_map, e)
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def render_b64(render_state: dict, terrain_png: bytes | None = None) -> str | None:
|
| 42 |
+
"""Preferred minimap: the training renderer (real terrain + an
|
| 43 |
+
embedded legend the model can read) when terrain is available;
|
| 44 |
+
otherwise the self-contained bench fallback. Either way a *valid*
|
| 45 |
+
base64 PNG, or None for graceful text-only."""
|
| 46 |
+
if terrain_png:
|
| 47 |
+
try:
|
| 48 |
+
from openra_rl_training.training.minimap_renderer import (
|
| 49 |
+
render_minimap,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
b64 = render_minimap(
|
| 53 |
+
terrain_png=terrain_png,
|
| 54 |
+
map_width=int(render_state.get("map_width", 64) or 64),
|
| 55 |
+
map_height=int(render_state.get("map_height", 64) or 64),
|
| 56 |
+
bounds_x=int(render_state.get("bounds_x", 0) or 0),
|
| 57 |
+
bounds_y=int(render_state.get("bounds_y", 0) or 0),
|
| 58 |
+
own_units=render_state.get("units_summary", []) or [],
|
| 59 |
+
enemy_units=render_state.get("enemy_summary", []) or [],
|
| 60 |
+
ascii_minimap=render_state.get("minimap", "") or "",
|
| 61 |
+
)
|
| 62 |
+
if b64:
|
| 63 |
+
return b64
|
| 64 |
+
except Exception as e: # noqa: BLE001 โ fall back below
|
| 65 |
+
logger.debug("training minimap failed, using fallback: %s", e)
|
| 66 |
+
return render_png_b64(render_state)
|
| 67 |
+
|
| 68 |
CELL = 6 # px per map cell (โ768ร240 for a 128ร40 map โ legible)
|
| 69 |
|
| 70 |
# (R,G,B)
|
|
@@ -72,10 +72,27 @@ def render_streamlit(root: str) -> None: # pragma: no cover - UI glue
|
|
| 72 |
m = ep["manifest"]
|
| 73 |
st.title(f"{m.get('scenario', pick)} โ {m.get('outcome', '?')}")
|
| 74 |
st.caption(
|
|
|
|
| 75 |
f"turns {m.get('turns')}/{m.get('max_turns')} ยท "
|
| 76 |
f"capability {m.get('capability')} ยท seed {m.get('seed')}"
|
| 77 |
)
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
asst = _assistant_turns(ep["messages"])
|
| 80 |
for i, t in enumerate(ep["turns"]):
|
| 81 |
with st.expander(
|
|
|
|
| 72 |
m = ep["manifest"]
|
| 73 |
st.title(f"{m.get('scenario', pick)} โ {m.get('outcome', '?')}")
|
| 74 |
st.caption(
|
| 75 |
+
f"{m.get('model','?')} ยท run {m.get('run_id','?')} ยท "
|
| 76 |
f"turns {m.get('turns')}/{m.get('max_turns')} ยท "
|
| 77 |
f"capability {m.get('capability')} ยท seed {m.get('seed')}"
|
| 78 |
)
|
| 79 |
|
| 80 |
+
# System prompt once at top (the deterministic scenario knowledge
|
| 81 |
+
# the model was given) โ same as the training pipeline viewer.
|
| 82 |
+
sysp = next(
|
| 83 |
+
(x.get("content", "") for x in ep["messages"]
|
| 84 |
+
if x.get("role") == "system"), ""
|
| 85 |
+
)
|
| 86 |
+
if isinstance(sysp, list):
|
| 87 |
+
sysp = " ".join(
|
| 88 |
+
p.get("text", "") for p in sysp if isinstance(p, dict)
|
| 89 |
+
)
|
| 90 |
+
if sysp:
|
| 91 |
+
with st.expander(f"๐ง System prompt ({len(sysp)} chars)",
|
| 92 |
+
expanded=False):
|
| 93 |
+
st.code(sysp, language="text")
|
| 94 |
+
|
| 95 |
+
users = [x for x in ep["messages"] if x.get("role") == "user"]
|
| 96 |
asst = _assistant_turns(ep["messages"])
|
| 97 |
for i, t in enumerate(ep["turns"]):
|
| 98 |
with st.expander(
|
|
@@ -53,6 +53,7 @@ def _default_agent_factory(provider_cfg) -> AgentFactory:
|
|
| 53 |
compiled.max_turns,
|
| 54 |
),
|
| 55 |
system_extra=scenario_primer(compiled),
|
|
|
|
| 56 |
)
|
| 57 |
return agent.agent_fn
|
| 58 |
|
|
@@ -145,6 +146,7 @@ def evaluate(
|
|
| 145 |
),
|
| 146 |
provider=shared,
|
| 147 |
system_extra=scenario_primer(compiled),
|
|
|
|
| 148 |
).agent_fn
|
| 149 |
|
| 150 |
# Run/model identity so a single playback root can hold many runs
|
|
|
|
| 53 |
compiled.max_turns,
|
| 54 |
),
|
| 55 |
system_extra=scenario_primer(compiled),
|
| 56 |
+
base_map=compiled.scenario.base_map,
|
| 57 |
)
|
| 58 |
return agent.agent_fn
|
| 59 |
|
|
|
|
| 146 |
),
|
| 147 |
provider=shared,
|
| 148 |
system_extra=scenario_primer(compiled),
|
| 149 |
+
base_map=compiled.scenario.base_map,
|
| 150 |
).agent_fn
|
| 151 |
|
| 152 |
# Run/model identity so a single playback root can hold many runs
|
|
@@ -77,3 +77,37 @@ def test_agent_attaches_image_when_vision_on():
|
|
| 77 |
assert url.startswith("data:image/png;base64,")
|
| 78 |
# the attached payload is itself a valid PNG
|
| 79 |
Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1]))).verify()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
assert url.startswith("data:image/png;base64,")
|
| 78 |
# the attached payload is itself a valid PNG
|
| 79 |
Image.open(io.BytesIO(base64.b64decode(url.split(",", 1)[1]))).verify()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# โโ training renderer (real terrain + embedded legend) โโโโโโโโโโโโโโโโโโโโโ
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_terrain_png_extracted_and_training_renderer_used():
|
| 86 |
+
pytest.importorskip("matplotlib")
|
| 87 |
+
from openra_bench.minimap import render_b64, terrain_png_for
|
| 88 |
+
|
| 89 |
+
t = terrain_png_for("rush-hour-arena")
|
| 90 |
+
assert t and t[:4] == b"\x89PNG" # real map.png from .oramap
|
| 91 |
+
rs = {
|
| 92 |
+
"minimap": "\n".join("#" * 128 for _ in range(40)),
|
| 93 |
+
"map_width": 128, "map_height": 40, "bounds_x": 0, "bounds_y": 0,
|
| 94 |
+
"units_summary": [{"id": "1", "cell_x": 6, "cell_y": 8}],
|
| 95 |
+
"enemy_summary": [],
|
| 96 |
+
}
|
| 97 |
+
with_terrain = render_b64(rs, t)
|
| 98 |
+
fallback = render_b64(rs, None)
|
| 99 |
+
im = Image.open(io.BytesIO(base64.b64decode(with_terrain)))
|
| 100 |
+
im.verify()
|
| 101 |
+
assert im.format == "PNG"
|
| 102 |
+
assert with_terrain != fallback # training path actually used
|
| 103 |
+
assert terrain_png_for("no-such-map") is None # graceful
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_agent_uses_terrain_when_base_map_given():
|
| 107 |
+
from openra_bench.agent import ModelAgent
|
| 108 |
+
from openra_bench.providers import ProviderConfig
|
| 109 |
+
|
| 110 |
+
a = ModelAgent(ProviderConfig(vision=True), allowed_tools=["observe"],
|
| 111 |
+
provider=type("P", (), {"complete": lambda *x, **k: None})(),
|
| 112 |
+
base_map="rush-hour-arena")
|
| 113 |
+
assert a._terrain and a._terrain[:4] == b"\x89PNG"
|