server.py serves the repo root rather than web/, so the 2D prototype stays reachable next to the 3D port — you want to flip between the reference implementation and the thing you're porting without stopping the server. --selfcheck verifies the tree and prints URLs; no node, no network. three r175 is copied in from 90sDJsim rather than referenced, so the game has no runtime dependency on a sibling checkout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
#!/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()
|