agent-parkour / scripts /generate_local_map.py
anngo-1's picture
Deploy Agent Parkour app
386f43a verified
Raw
History Blame Contribute Delete
8.49 kB
#!/usr/bin/env python3
"""Generate a parkour map locally and save it to app/maps/.
The CLI drives the same mapgen path that the app server uses. The LLM is
optional: if MAPGEN_LLM_API_KEY (or OPENROUTER_API_KEY) is set, the LLM is
asked to extract knobs/features from the vibe prompt. Otherwise the heuristic
keyword extractor in app.app runs. Either way, the actual map is built by
`configure_motif_mapgen(**params) + FastFloatingBeaconEnv().reset()`, so the
saved JSON is in-distribution with training and any LLM OOD-ness is just
extreme knob combinations.
Usage:
uv run python scripts/generate_local_map.py --vibe "spiral with false summits" --difficulty standard
uv run python scripts/generate_local_map.py --vibe "" --difficulty cursed --seed 17
uv run python scripts/generate_local_map.py --vibe "..." --difficulty standard --llm
uv run python scripts/generate_local_map.py --list
uv run python scripts/generate_local_map.py --open
"""
from __future__ import annotations
import argparse
import json
import os
import random
import sys
import time
import webbrowser
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from app.app import ( # noqa: E402
MAPS_DIR,
_build_map_snapshot_from_env,
_build_vibe_plan,
_difficulty_spec,
_knobs,
_llm_in_use,
_make_env,
_mapgen_llm_report,
_save_map_snapshot,
)
def _list() -> None:
entries = sorted(MAPS_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
if not entries:
print(f"(no maps in {MAPS_DIR})")
return
for path in entries:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
print(f" ! {path.name}: unreadable")
continue
tags = ",".join(payload.get("tags", []) or [])
print(
f" {path.name} diff={payload.get('difficulty', '?'):>8} "
f"seed={payload.get('seed', 0):>6} "
f"platforms={len(payload.get('platforms', []) or []):>3} "
f"tags={tags}"
)
def _generate(
vibe: str,
difficulty: str,
seed: int | None,
knobs: dict[str, float],
name: str | None,
*,
use_llm_direct: bool = False,
) -> dict[str, object]:
spec = _difficulty_spec(difficulty)
if seed is None:
seed = int.from_bytes(os.urandom(4), "little") % 1_000_000
route_jumps = int(spec["route_jumps"]) + int(round(knobs["length"] * 4.0))
distractors = int(spec["distractors"]) + int(round(knobs["length"] * 6.0))
if use_llm_direct:
from app.app import _apply_direct_platforms, _request_direct_platforms_with_llm
result = _request_direct_platforms_with_llm(vibe, str(spec["key"]), route_jumps, distractors)
if not result.get("ok"):
raise RuntimeError(f"LLM did not produce a usable platform list: {result.get('reason')}")
env = _make_env(
seed=int(seed),
route_jumps=route_jumps,
distractors=distractors,
direct_platforms=result,
)
vibe_plan = {
"theme": "llm direct platforms",
"mood": "out of distribution",
"tags": ["llm-direct", "ood"],
"knobs": {},
"params": {},
"raw_vibe": str(vibe or "").strip(),
"direct_platforms": {
"source": "llm",
"route_count": len(result.get("route", [])),
"extras_count": len(result.get("extras", [])),
},
}
generation_mode = "llm_direct"
else:
vibe_plan = _build_vibe_plan(vibe, str(spec["key"]), knobs)
params = vibe_plan.get("params", {})
if not params:
raise RuntimeError("vibe plan produced no mapgen params")
env = _make_env(
seed=int(seed),
route_jumps=route_jumps,
distractors=distractors,
motif_params=params,
)
env.reset()
generation_mode = "procedural"
snapshot = _build_map_snapshot_from_env(
env,
difficulty_key=str(spec["key"]),
seed=int(seed),
vibe_plan=vibe_plan,
name=name,
)
_save_map_snapshot(snapshot)
return {
"map_id": snapshot["id"],
"seed": int(seed),
"difficulty": str(spec["key"]),
"route_jumps": route_jumps,
"distractors": distractors,
"platforms": len(snapshot["platforms"]),
"tags": snapshot["tags"],
"theme": snapshot["theme"],
"mood": snapshot["mood"],
"knobs": vibe_plan.get("knobs", {}),
"direct_platforms": vibe_plan.get("direct_platforms", {}),
"vibe": vibe,
"generation_mode": generation_mode,
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--vibe", default="", help="Vibe prompt. Empty for the default heuristic.")
parser.add_argument(
"--difficulty",
default="standard",
choices=["rookie", "standard", "cursed", "nightmare"],
)
parser.add_argument("--seed", type=int, default=None, help="Deterministic seed (default: random).")
parser.add_argument("--name", default=None, help="Map display name (default: 'Difficulty seed').")
parser.add_argument("--verticality", type=float, default=0.5)
parser.add_argument("--traps", type=float, default=0.5)
parser.add_argument("--loopiness", type=float, default=0.5)
parser.add_argument("--precision", type=float, default=0.5)
parser.add_argument("--length", type=float, default=0.5)
parser.add_argument("--llm", action="store_true", help="Force the LLM path even if no vibe is given.")
parser.add_argument("--no-llm", action="store_true", help="Force the heuristic path even if the LLM is configured.")
parser.add_argument("--list", action="store_true", help="List cached maps in app/maps/ and exit.")
parser.add_argument("--open", action="store_true", help="Open the saved map in the running app after generation.")
parser.add_argument("--json", action="store_true", help="Emit the result as JSON to stdout.")
args = parser.parse_args()
if args.list:
_list()
return 0
if args.no_llm and "MAPGEN_LLM" not in os.environ:
os.environ["MAPGEN_LLM"] = "off"
if args.llm and not _llm_in_use():
report = _mapgen_llm_report()
print(
f"LLM not active: mode={report['mode']} configured={report['configured']} "
f"model={report['model']}. Set MAPGEN_LLM_API_KEY (or OPENROUTER_API_KEY) "
f"and MAPGEN_LLM=auto.",
file=sys.stderr,
)
if not args.vibe:
print("Add --vibe 'your prompt' to give the LLM something to interpret.", file=sys.stderr)
return 1
knobs = _knobs(args.verticality, args.traps, args.loopiness, args.precision, args.length)
result = _generate(
vibe=args.vibe,
difficulty=args.difficulty,
seed=args.seed,
knobs=knobs,
name=args.name,
use_llm_direct=bool(args.llm),
)
if args.json:
print(json.dumps(result, indent=2, default=str))
else:
mode = result.get("generation_mode", "procedural")
print(
f"generated map {result['map_id']} diff={result['difficulty']} seed={result['seed']} "
f"route_jumps={result['route_jumps']} distractors={result['distractors']} "
f"platforms={result['platforms']} mode={mode}"
)
print(f" tags: {','.join(result['tags'])}")
print(f" theme: {result['theme']} mood: {result['mood']}")
if result.get("direct_platforms"):
dp = result["direct_platforms"]
print(f" direct_platforms: source={dp.get('source')} model={dp.get('model')} route={dp.get('route_count')} extras={dp.get('extras_count')}")
if args.vibe:
print(f" vibe: {args.vibe}")
if args.open:
url = os.environ.get("PARKOUR_OPEN_URL", "http://127.0.0.1:7860")
map_url = f"{url}/?map={result['map_id']}"
print(f"opening {map_url}")
try:
webbrowser.open(map_url)
except Exception as exc:
print(f"could not open browser: {exc}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())