Render actual generated dungeons through the AI pipeline — both top-down and first-person (Eye-of-the-Beholder style) — instead of synthetic test depth. All renderer-side: derived from the envelope's tiles+regions+theme, the core stores none of it. - depth.js: top-down height-field depth exporter (walls raised, pools recessed, pillars as bumps, subtle per-theme relief), calibrated to the proven room_depth levels so one tall room can't crush every floor dark. - fpv.js: first-person depth via a Wolfenstein-style grid raycaster (16:9), reports straight-ahead distance for opening detection. - explore.js: room->room nav graph derived from REAL tile openings (scan boundary gaps, bin by cardinal, flood the corridor to the destination room) — nav + open share one source of truth with the rendered passages. - bake_atlas.py: bakes per-room x4-facing pixel-art frames; per-theme + per-facing (wall vs passage) prompts, fixed diffusion seed for cross-frame style coherence, per-run unique prefixes so re-bakes don't silently skip. - explore.html + explore-viewer.js: WASD first-person dungeon explorer (room-to-room movement, turning, minimap), integer-pixel fullscreen. - serve.py: stdlib dev server — static web root + /out tree + POST /save, no-store. - main.js/render.js/style.css: export hooks, the depth button, distanceToWall export. - tools/README.md: Stage 0/1/2/3 docs; .dev/2026-06-01-fpv-prompts.md: prompt research (cfg-1 negatives are inert; trigger words summon hands; empty-ruin reframe; fixed seed = consistency). Proven end-to-end on seed 7 (17 rooms). Outputs organized under git-ignored tools/out/. Control strength: 0.80-0.85 top-down, 0.85 first-person. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
4.2 KiB
Python
105 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Dev server for the reikhelm playground, headless depth/FPV export, and the
|
|
pre-baked first-person explorer. Pure stdlib — no deps.
|
|
|
|
python3 tools/serve.py # serve reikhelm-web on :8000
|
|
|
|
Three things, one server:
|
|
GET /<path> static file from the web root (reikhelm-web/)
|
|
GET /out/<path> static file from the output tree (tools/out/) — lets the
|
|
explorer load baked atlas frames + manifests
|
|
POST /save/<path>.png write the raw request body (a PNG) under tools/out/<path>;
|
|
sub-dirs allowed and created (e.g. atlas/7/r3_N.png)
|
|
→ {"ok": true, "path": "...", "bytes": N}
|
|
|
|
All generated images live under tools/out/ (git-ignored, organized):
|
|
out/depth/ top-down + one-off depth maps out/fpv/ first-person stills
|
|
out/atlas/<seed>/ a baked explorable dungeon (frames + manifest.json)
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
from urllib.parse import unquote
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def safe_join(base, rel):
|
|
"""Join rel under base, collapsing any '..' so it can't escape base."""
|
|
rel = os.path.normpath("/" + rel).lstrip("/")
|
|
return os.path.join(base, rel)
|
|
|
|
|
|
class Handler(SimpleHTTPRequestHandler):
|
|
out_dir = "."
|
|
|
|
extensions_map = {
|
|
**SimpleHTTPRequestHandler.extensions_map,
|
|
".js": "text/javascript",
|
|
".mjs": "text/javascript",
|
|
".wasm": "application/wasm",
|
|
".json": "application/json",
|
|
}
|
|
|
|
# Route /out/<path> to the output tree; everything else from the web root (cwd).
|
|
def translate_path(self, path):
|
|
p = unquote(path.split("?", 1)[0].split("#", 1)[0])
|
|
if p == "/out" or p.startswith("/out/"):
|
|
return safe_join(self.out_dir, p[len("/out/"):]) if p != "/out" else self.out_dir
|
|
return super().translate_path(path)
|
|
|
|
def end_headers(self):
|
|
self.send_header("Cache-Control", "no-store") # dev server: always serve fresh modules
|
|
super().end_headers()
|
|
|
|
def do_POST(self):
|
|
if not self.path.startswith("/save/"):
|
|
self.send_error(404, "only /save/<path>.png is supported")
|
|
return
|
|
rel = unquote(self.path[len("/save/"):])
|
|
if not (rel.endswith(".png") or rel.endswith(".json")):
|
|
self.send_error(400, "path must end in .png or .json")
|
|
return
|
|
dest = safe_join(self.out_dir, rel)
|
|
length = int(self.headers.get("Content-Length", 0))
|
|
body = self.rfile.read(length)
|
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
with open(dest, "wb") as f:
|
|
f.write(body)
|
|
payload = json.dumps({"ok": True, "path": dest, "bytes": len(body)}).encode()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
sys.stderr.write(f"saved {os.path.relpath(dest, REPO)} ({len(body)} bytes)\n")
|
|
|
|
def log_message(self, fmt, *args): # quiet GETs; POSTs/errors print via do_POST/send_error
|
|
pass
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--port", type=int, default=8000)
|
|
ap.add_argument("--root", default=os.path.join(REPO, "reikhelm-web"), help="static web root")
|
|
ap.add_argument("--out", default=os.path.join(REPO, "tools", "out"), help="output tree (served at /out, target of /save)")
|
|
args = ap.parse_args()
|
|
|
|
Handler.out_dir = os.path.abspath(args.out)
|
|
os.makedirs(Handler.out_dir, exist_ok=True)
|
|
root = os.path.abspath(args.root)
|
|
os.chdir(root)
|
|
httpd = ThreadingHTTPServer(("127.0.0.1", args.port), Handler)
|
|
print(f"serving {os.path.relpath(root, REPO)} at http://127.0.0.1:{args.port}")
|
|
print(f" /out/ → {os.path.relpath(Handler.out_dir, REPO)} POST /save/<path>.png writes there")
|
|
try:
|
|
httpd.serve_forever()
|
|
except KeyboardInterrupt:
|
|
httpd.shutdown()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|