HardYards/server.py
m3ultra 664e378578 Vendor three.js r175, add stdlib dev server and launch config
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>
2026-07-16 21:33:42 +10:00

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()