#!/usr/bin/env python3 """ SHADES — dev server. Python stdlib only: no pip, no venv, no build step, no CDN. python3 server.py # serve the yard on :8801 python3 server.py --port 9000 python3 server.py --selfcheck # verify the tree, print URLs, exit House rule (PLAN3D §0): zero dependencies. If this file ever seems to need something from PyPI, it doesn't. It serves the repo root rather than web/, so the 2D prototype stays reachable next to the 3D game — you want to be able to flip between the reference implementation and the port without stopping the server. """ from __future__ import annotations import argparse import http.server import mimetypes import sys from pathlib import Path ROOT = Path(__file__).resolve().parent DEFAULT_PORT = 8801 GAME = "/web/world/index.html" SELFTEST = "/web/world/selftest.html" PROTOTYPE = "/prototype/index.html" # The files without which nothing boots. --selfcheck reports on exactly these. REQUIRED = ( "web/world/index.html", "web/world/selftest.html", "web/world/vendor/three.module.js", "web/world/vendor/three.core.js", "web/world/js/contracts.js", "web/world/js/main.js", "web/world/js/world.js", "web/world/js/camera.js", "web/world/js/testkit.js", "prototype/index.html", ) # Don't inherit whatever the OS thinks .js is — some macOS setups still map it # to application/x-javascript, which browsers refuse to load as a module. mimetypes.add_type("text/javascript", ".js") mimetypes.add_type("application/json", ".json") mimetypes.add_type("model/gltf-binary", ".glb") mimetypes.add_type("model/gltf+json", ".gltf") class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(ROOT), **kwargs) def end_headers(self): # Dev only, and deliberate: nothing costs more time than debugging a # stale module you already fixed. self.send_header("Cache-Control", "no-store, no-cache, must-revalidate") super().end_headers() def do_GET(self): if self.path in ("/", "/index.html"): self.send_response(302) self.send_header("Location", GAME) self.end_headers() return super().do_GET() def log_message(self, fmt, *args): # Quiet the 200s (vendor alone is a wall of them); shout about the rest. if len(args) > 1 and str(args[1]) == "200": return super().log_message(fmt, *args) def selfcheck(port: int) -> int: """Verify the tree and print where things live. Deliberately dumb: no node, no headless browser, no network. Exit 0 if the game can boot.""" missing = [] print(f"shades selfcheck — root {ROOT}") for rel in REQUIRED: ok = (ROOT / rel).exists() if not ok: missing.append(rel) print(f" {'ok ' if ok else 'MISS'} {rel}") vendor = ROOT / "web/world/vendor" n_vendor = len(list(vendor.rglob("*.js"))) if vendor.is_dir() else 0 print(f" {'ok ' if n_vendor else 'MISS'} vendored three.js files: {n_vendor}") print() if missing: print(f"FAIL — {len(missing)} required file(s) missing:") for rel in missing: print(f" {rel}") return 1 print("PASS — tree is complete. Run `python3 server.py`, then:") print(f" game http://localhost:{port}{GAME}") print(f" selftest http://localhost:{port}{SELFTEST}") print(f" prototype http://localhost:{port}{PROTOTYPE}") print() print(" The selftest asserts in-page and prints a JSON report to the") print(" console; it drives the sim with fixed-dt loops, so it does not") print(" need the tab focused.") return 0 def serve(port: int) -> None: with http.server.ThreadingHTTPServer(("", port), Handler) as httpd: print(f"shades on http://localhost:{port}") print(f" game http://localhost:{port}{GAME}") print(f" selftest http://localhost:{port}{SELFTEST}") print(f" prototype http://localhost:{port}{PROTOTYPE}") print("ctrl-c to stop") try: httpd.serve_forever() except KeyboardInterrupt: print("\nstopped") def main() -> None: ap = argparse.ArgumentParser(description="SHADES dev server (stdlib only)") ap.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"default {DEFAULT_PORT}") ap.add_argument("--selfcheck", action="store_true", help="verify the tree and print URLs, then exit") args = ap.parse_args() if args.selfcheck: sys.exit(selfcheck(args.port)) serve(args.port) if __name__ == "__main__": main()