"""Generate web/city/constants.js + web/mock/world.json from engine constants and genesis — the ONE source of truth crossing the Python/JS boundary. Run from the repo root (the integrator runs this; output is deterministic, no timestamps): python tools/gen_web.py """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from engine import constants as C # noqa: E402 from engine.genesis import genesis_features # noqa: E402 def _js(value) -> str: return json.dumps(value, indent=2, sort_keys=False) def constants_js() -> str: buildings = { kind: { "w": spec["w"], "d": spec["d"], "floors": list(spec["floors"]), "residents": spec["residents"], "jobs": spec["jobs"], "attract": spec["attract"], # both spellings: engine uses duration_s, the JS kit reads duration "duration": spec["duration_s"], "duration_s": spec["duration_s"], } for kind, spec in C.BUILDINGS.items() } durations = {kind: spec["duration_s"] for kind, spec in C.BUILDINGS.items()} parts = [ "// GENERATED by tools/gen_web.py from engine/constants.py — do not edit.", "// Values are the ARCHITECTURE.md contract; regenerate, never hand-tune.", "", f"export const GRID = {C.GRID};", f"export const CELL = {C.CELL};", f"export const COORD_MIN = {C.COORD_MIN};", f"export const COORD_MAX = {C.COORD_MAX};", f"export const CITY_EPOCH_S = {C.CITY_EPOCH_S};", f"export const DAY_S = {C.DAY_S};", "", "export function riverCols(cz) {", " return cz <= -5 ? [7, 8] : cz <= 3 ? [8, 9] : [9, 10];", "}", "", f"export const BUILDINGS = {_js(buildings)};", "", f"export const ROAD_CLASSES = {_js(C.ROAD_CLASSES)};", f"export const ROAD_DURATION_S = {C.ROAD_DURATION_S};", f"export const DURATIONS = {_js(durations)};", f"export const SYNONYMS = {_js(C.SYNONYMS)};", f"export const STREET_NAMES = {_js(list(C.STREET_NAMES))};", "", "// traffic (mirrors engine/traffic.py — the Python side owns the facts)", f"export const RUSH_HOURS = {_js(list(C.RUSH_HOURS))};", f"export const RUSH_SIGMA = {C.RUSH_SIGMA};", f"export const RUSH_BASE = {C.RUSH_BASE};", f"export const RUSH_AMP = {C.RUSH_AMP};", f"export const CAR_RATE = {C.CAR_RATE};", f"export const CAR_MIN = {C.CAR_MIN};", f"export const CAR_MAX = {C.CAR_MAX};", f"export const CITIZENS_PER_POP = {C.CITIZENS_PER_POP};", f"export const CITIZENS_MIN = {C.CITIZENS_MIN};", f"export const CITIZENS_MAX = {C.CITIZENS_MAX};", f"export const EMA_TAU_S = {C.EMA_TAU_S};", f"export const TRIP_ATTRACT_FACTOR = {C.TRIP_ATTRACT_FACTOR};", f"export const TRIP_DIST_EXP = {C.TRIP_DIST_EXP};", f"export const DEMAND_PER_COMMUTER = {C.DEMAND_PER_COMMUTER};", f"export const CONGESTION_COST_FACTOR = {C.CONGESTION_COST_FACTOR};", f"export const TRAFFIC_TOP_CELLS = {C.TRAFFIC_TOP_CELLS};", f"export const JAM_RATIO = {C.JAM_RATIO};", f"export const GROWTH_RADIUS_MIN = {C.GROWTH_RADIUS_MIN};", "", ] return "\n".join(parts) def world_json() -> str: features = genesis_features() payload = { "world": { "version": len(features), "epoch": 0, "features": features, } } return json.dumps(payload, indent=2) + "\n" def main() -> None: constants_path = ROOT / "web" / "city" / "constants.js" world_path = ROOT / "web" / "mock" / "world.json" world_path.parent.mkdir(parents=True, exist_ok=True) constants_path.write_text(constants_js(), encoding="utf-8") world_path.write_text(world_json(), encoding="utf-8") print(f"wrote {constants_path.relative_to(ROOT)}") print(f"wrote {world_path.relative_to(ROOT)}") if __name__ == "__main__": main()