#!/usr/bin/env python3 """Generate static scenario data for the mission-player website. Maintainer-only preprocessing step. Reads every active scenario pack, renders map thumbnails, generates bilingual (EN/ZH) human-friendly descriptions deterministically (no AI API), and writes: public/scenarios.json — full scenario metadata public/maps/.png — per-pack map thumbnail Usage: python site/generate.py # generate all python site/generate.py --dry-run # print counts, don't write """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) PUBLIC = Path(__file__).resolve().parent / "public" MAPS_DIR = PUBLIC / "maps" # ── Bilingual description generation (deterministic, no AI API) ──────── # Capability descriptions _CAP_ZH = { "perception": "感知", "reasoning": "推理", "action": "行动", "adversarial": "对抗", } _CAP_DESC_EN = { "perception": "Tests the agent's ability to observe and interpret the battlefield.", "reasoning": "Tests the agent's ability to plan and make strategic decisions.", "action": "Tests the agent's ability to execute commands precisely.", "adversarial": "Tests the agent's ability to compete against a reactive opponent.", } _CAP_DESC_ZH = { "perception": "测试智能体观察和理解战场的能力。", "reasoning": "测试智能体规划和做出战略决策的能力。", "action": "测试智能体精确执行命令的能力。", "adversarial": "测试智能体与反应型对手竞争的能力。", } _DIFF_ZH = {"easy": "简单", "medium": "中等", "hard": "困难"} _translate_cache: dict[str, str] = {} def _google_translate_zh(text: str) -> str: """Translate English text to Chinese via Google Translate API.""" if not text or not text.strip(): return text if text in _translate_cache: return _translate_cache[text] import time import urllib.parse import urllib.request url = ( "https://translate.googleapis.com/translate_a/single" "?client=gtx&sl=en&tl=zh-CN&dt=t&q=" + urllib.parse.quote(text) ) try: req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=15) as resp: import json as _json data = _json.loads(resp.read().decode()) result = "".join(seg[0] for seg in data[0] if seg[0]) result = _fixup_game_terms(result) _translate_cache[text] = result time.sleep(0.15) return result except Exception as e: print(f" [warn] Google Translate failed: {e}") return text def _fixup_game_terms(zh: str) -> str: """Fix game-specific terms that Google Translate consistently gets wrong.""" fixes = [ ("游戏勾选", "游戏刻"), ("游戏滴答", "游戏刻"), ("游戏刻度", "游戏刻"), ("游戏蜱虫", "游戏刻"), ("游戏壁虱", "游戏刻"), ("游戏报价", "游戏刻"), ("决策轮次", "决策回合"), ("决策转弯", "决策回合"), ("决策回合", "决策回合"), ("勾号", "刻"), ] for wrong, right in fixes: zh = zh.replace(wrong, right) return zh def _translate_objective_zh(en: str) -> str: """Translate a full objective text to Chinese. Canonicalises the load-bearing objective prefixes ("WIN WHEN:" / "YOU LOSE IF:") deterministically — the Google Translate output drifts across runs ("获胜时间" / "如果您失败" / "胜利条件" all appear depending on context) — then forwards the rest of each line to Google Translate so non-prefix wording stays natural. """ if not en or not en.strip(): return en out_lines: list[str] = [] for line in en.split("\n"): out_lines.append(_translate_line_zh(line)) return "\n".join(out_lines) def _translate_line_zh(line: str) -> str: """Translate one line, with deterministic prefix replacement for the canonical objective markers.""" stripped = line.lstrip() indent = line[: len(line) - len(stripped)] # Canonical prefixes: replace the English head with the canonical # Chinese head and only Google-translate the remainder. Mirrors the # objective_brief output in openra_bench/game_knowledge.py. PREFIXES = ( ("WIN WHEN:", "胜利条件:"), ("YOU LOSE IF:", "失败条件:"), ) for en_pref, zh_pref in PREFIXES: if stripped.startswith(en_pref): rest = stripped[len(en_pref):].strip() zh_rest = _google_translate_zh(rest) if rest else "" return f"{indent}{zh_pref}{zh_rest}" return _google_translate_zh(line) if line.strip() else line def _annotator_hints_en(pack) -> list[str]: """Generate annotator-facing hints from pack metadata.""" hints = [] cap = pack.meta.capability hints.append(f"This scenario tests {cap} capability.") if pack.meta.benchmark_anchor: hints.append(f"Benchmark anchors: {', '.join(pack.meta.benchmark_anchor)}.") hints.append(f"Map: {pack.base_map if isinstance(pack.base_map, str) else 'procedurally generated'}.") return hints def _annotator_hints_zh(pack) -> list[str]: """Generate annotator-facing hints in Chinese.""" hints = [] cap = _CAP_ZH.get(pack.meta.capability, pack.meta.capability) hints.append(f"此场景测试{cap}能力。") if pack.meta.benchmark_anchor: hints.append(f"基准锚点:{', '.join(pack.meta.benchmark_anchor)}。") map_name = pack.base_map if isinstance(pack.base_map, str) else "程序生成" hints.append(f"地图:{map_name}。") return hints def build_scenario(pack, objective_brief_fn) -> dict: """Build the internal scenario model for one pack.""" cap = pack.meta.capability levels = {} for lv_name in ("easy", "medium", "hard"): try: cl = pack.compile(lv_name) en_obj = objective_brief_fn( cl.scenario.description, cl.win_condition, cl.fail_condition, cl.max_turns, getattr(cl, "objective_coords", "exact"), ) zh_obj = _translate_objective_zh(en_obj) levels[lv_name] = { "description": cl.scenario.description, "maxTurns": cl.max_turns, "startingCash": cl.starting_cash, "fogMode": getattr(cl, "fog_mode", "vision"), "objectiveCoords": getattr(cl, "objective_coords", "exact"), "objective": {"en": en_obj, "zh": zh_obj}, } except Exception as e: levels[lv_name] = {"error": str(e)} anchors = ", ".join(pack.meta.benchmark_anchor) if pack.meta.benchmark_anchor else "" return { "scenarioId": pack.meta.id, "title": pack.meta.title, "capability": cap, "capabilityLabel": {"en": cap.title(), "zh": _CAP_ZH.get(cap, cap)}, "map": { "type": "image", "src": f"maps/{pack.meta.id}.png", "baseMap": pack.base_map if isinstance(pack.base_map, str) else "generated", }, "raw": { "realWorldMeaning": pack.meta.real_world_meaning, "roboticsAnalogue": pack.meta.robotics_analogue, "benchmarkAnchor": anchors, "author": pack.meta.author, }, "humanReadable": { "summary": { "en": pack.meta.real_world_meaning, "zh": _translate_objective_zh(pack.meta.real_world_meaning), }, "playerInstructions": { "en": [levels[lv].get("objective", {}).get("en", "") for lv in ("easy", "medium", "hard") if "objective" in levels.get(lv, {})], "zh": [levels[lv].get("objective", {}).get("zh", "") for lv in ("easy", "medium", "hard") if "objective" in levels.get(lv, {})], }, "annotatorHints": { "en": _annotator_hints_en(pack), "zh": _annotator_hints_zh(pack), }, "ambiguities": {"en": [], "zh": []}, }, "levels": levels, } def render_map_thumbnail(pack, out_path: Path) -> bool: """Render a map thumbnail for one pack.""" try: from openra_bench.scenarios.loader import compile_level cl = compile_level(pack, "easy") from openra_bench.minimap import render_tactical_minimap rs = {"minimap": "", "units_summary": [], "enemy_summary": []} # Try to get a real render state from a compiled level from openra_bench.eval_core import make_env env = make_env(cl, seed=1) obs = env.reset() from openra_bench.rust_adapter import RustObsAdapter adapter = RustObsAdapter(cl.scenario) rs = adapter.render_state(obs) env.close() img = render_tactical_minimap(rs, scale=3, grid=True, legend=False) if img: out_path.parent.mkdir(parents=True, exist_ok=True) img.save(str(out_path)) return True except Exception: pass # Fallback: try minimap from scenario description alone try: from openra_bench.minimap import render_tactical_minimap cl = pack.compile("easy") actors = [] for a in getattr(cl.scenario, 'actors', []) or []: if hasattr(a, 'position') and a.position: actors.append({ "cell_x": a.position[0] if isinstance(a.position, (list, tuple)) else getattr(a.position, 'x', 0), "cell_y": a.position[1] if isinstance(a.position, (list, tuple)) else getattr(a.position, 'y', 0), "type": getattr(a, 'actor_type', '?'), "id": "0", }) rs = { "minimap": "", "units_summary": actors[:10], "enemy_summary": [], } img = render_tactical_minimap(rs, scale=3, grid=True, legend=False) if img: out_path.parent.mkdir(parents=True, exist_ok=True) img.save(str(out_path)) return True except Exception: pass return False def generate(dry_run: bool = False) -> dict: """Generate all static data. Returns summary stats.""" from openra_bench.game_knowledge import objective_brief from openra_bench.scenarios import discover_packs packs = [p for p in discover_packs() if p.meta.status == "active"] scenarios = [] maps_ok = 0 for p in packs: s = build_scenario(p, objective_brief) scenarios.append(s) if not dry_run: img_path = MAPS_DIR / f"{p.meta.id}.png" if render_map_thumbnail(p, img_path): maps_ok += 1 has_en = sum(1 for s in scenarios if s["humanReadable"]["playerInstructions"]["en"]) has_zh = sum(1 for s in scenarios if s["humanReadable"]["playerInstructions"]["zh"]) stats = { "total": len(scenarios), "maps": maps_ok if not dry_run else "skipped", "english_instructions": has_en, "chinese_instructions": has_zh, "capabilities": {}, } from collections import Counter for cap, n in Counter(s["capability"] for s in scenarios).items(): stats["capabilities"][cap] = n if not dry_run: PUBLIC.mkdir(parents=True, exist_ok=True) out = PUBLIC / "scenarios.json" out.write_text(json.dumps(scenarios, indent=2, ensure_ascii=False), encoding="utf-8") print(f"Wrote {out} ({len(scenarios)} scenarios)") print(f"Maps rendered: {maps_ok}/{len(scenarios)}") else: print(f"[dry-run] Would generate {len(scenarios)} scenarios") print(f"English instructions: {has_en}/{len(scenarios)}") print(f"Chinese instructions: {has_zh}/{len(scenarios)}") for cap, n in sorted(stats["capabilities"].items()): print(f" {cap}: {n}") return stats if __name__ == "__main__": dry = "--dry-run" in sys.argv generate(dry_run=dry)