Mirrors the cucucaracha (lacucarachanews) toolkit pattern adapted for
the voxel game:
bridges.rs adds TestCommand + Telemetry plumbing:
- thread_local TEST_COMMANDS queue + TELEMETRY snapshot.
- drain_test_commands() called by App::tick at frame start.
- publish_telemetry(t) called at frame end.
- wasm_api exports: set_scene_time, teleport, look_at, plus
getters get_scene_time / get_position / get_camera_angles.
app.rs:
- drain_test_commands() applies SetSceneTime / Teleport / LookAt
before physics integrates. Teleport zeroes velocity and syncs the
camera to feet+EYE_HEIGHT.
- publish_telemetry() at end of tick exposes scene state to JS.
test/:
launch.py Open Chromium with persistent profile + CDP:9222.
Navigates to https://voxel.mxvs.art by default;
--url for local dev.
peek.py Attach via CDP, screenshot canvas, dump telemetry
(scene_time, position, camera angles, hp). Read-only.
run.py Execute a YAML scenario:
wait_for, wait, eval, key, mouse_move, mouse,
screenshot, assert
Key allowlist prevents stray scenarios from sending
arbitrary input.
requirements.txt playwright + PyYAML.
README.md Setup, grammar, available bindings, why this
exists.
scenarios/
lighting-times-of-day.yaml Screenshots at noon / afternoon /
sunset / civil twilight / midnight
/ sunrise. Verifies the Round A
sunset fixes by visual diff.
god-rays-look-at-sun.yaml Pointed at the sun at four altitudes
to inspect the Round B shafts.
voxel-construction-darkness.yaml Visual baseline for the sky_vis
bake from Round D.
.gitignore Excludes the browser profile +
screenshots directory.
Visual regression workflow:
1. python3 launch.py
2. (separate terminal) python3 run.py scenarios/lighting-times-of-day.yaml
3. Compare screenshots/lighting-times-of-day_*.png against baseline.
Tests still 63 passing. Native + wasm release clean.
114 lines
3.3 KiB
Python
114 lines
3.3 KiB
Python
"""
|
|
One-shot inspection: attach to the running launch.py browser via CDP,
|
|
screenshot the canvas, and dump current game telemetry (scene time,
|
|
player position, camera angles, hp).
|
|
|
|
Read-only. Cannot send inputs, change state, or close the browser.
|
|
|
|
Usage:
|
|
python3 peek.py
|
|
python3 peek.py --json
|
|
python3 peek.py --name baseline # custom screenshot filename
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
SCREENSHOT_DIR = HERE / "screenshots"
|
|
CDP_URL = "http://localhost:9222"
|
|
|
|
|
|
# JS evaluated in the page to gather game state. Pure reads only.
|
|
TELEMETRY_JS = r"""
|
|
() => {
|
|
const g = window.voxel_game;
|
|
if (!g) return { ready: false };
|
|
return {
|
|
ready: true,
|
|
scene_time: g.get_scene_time?.() ?? null,
|
|
position: g.get_position?.() ?? null,
|
|
camera_angles: g.get_camera_angles?.() ?? null,
|
|
hp: g.get_hp?.() ?? null,
|
|
alive: g.is_alive?.() ?? null,
|
|
canvas_size: (() => {
|
|
const c = document.getElementById('game-canvas');
|
|
return c ? { w: c.width, h: c.height } : null;
|
|
})(),
|
|
};
|
|
}
|
|
"""
|
|
|
|
|
|
def attach_page():
|
|
p = sync_playwright().start()
|
|
try:
|
|
browser = p.chromium.connect_over_cdp(CDP_URL)
|
|
except Exception as e: # noqa: BLE001
|
|
p.stop()
|
|
sys.exit(f"could not attach to launch.py via {CDP_URL}: {e}")
|
|
pages = [pg for c in browser.contexts for pg in c.pages]
|
|
if not pages:
|
|
browser.close()
|
|
p.stop()
|
|
sys.exit("no open pages")
|
|
return p, browser, pages[-1]
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--name", default="peek", help="screenshot name prefix")
|
|
ap.add_argument("--json", action="store_true", help="JSON output only")
|
|
args = ap.parse_args()
|
|
|
|
SCREENSHOT_DIR.mkdir(exist_ok=True)
|
|
p, browser, page = attach_page()
|
|
try:
|
|
try:
|
|
telemetry = page.evaluate(TELEMETRY_JS)
|
|
except Exception as e: # noqa: BLE001
|
|
telemetry = {"ready": False, "error": str(e)}
|
|
|
|
shot = SCREENSHOT_DIR / f"{int(time.time())}_{args.name}.png"
|
|
try:
|
|
page.screenshot(path=str(shot), full_page=False)
|
|
except Exception as e: # noqa: BLE001
|
|
shot = None
|
|
print(f"[!] screenshot failed: {e}", file=sys.stderr)
|
|
|
|
result = {
|
|
"url": page.url,
|
|
"screenshot": str(shot) if shot else None,
|
|
**telemetry,
|
|
}
|
|
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
print(f"URL: {result['url']}")
|
|
print(f"SHOT: {result.get('screenshot')}")
|
|
print(f"READY: {result.get('ready')}")
|
|
if result.get("ready"):
|
|
print(f"TIME: {result.get('scene_time'):.2f}s")
|
|
pos = result.get("position") or []
|
|
print(f"POS: ({', '.join(f'{v:.2f}' for v in pos)})")
|
|
ang = result.get("camera_angles") or []
|
|
print(f"YAW/PITCH: ({', '.join(f'{v:.3f}' for v in ang)})")
|
|
print(f"HP: {result.get('hp')}")
|
|
print(f"ALIVE: {result.get('alive')}")
|
|
finally:
|
|
browser.close()
|
|
p.stop()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|