Spaces:
Running on Zero
Running on Zero
| """NEMOCITY canonical constants — the single source of truth for grid, river, | |
| buildings, roads, traffic, and placement numbers (ARCHITECTURE.md mirrors these; | |
| tools/gen_web.py emits the JS copy). Pure data, stdlib only. | |
| """ | |
| from __future__ import annotations | |
| GRID = 64 | |
| CELL = 4 # world units per cell (1 unit = 1 m) | |
| COORD_MIN, COORD_MAX = -32, 31 # cell coords (cx, cz) inclusive | |
| CITY_EPOCH_S = 1781222400 # June 12 2026 00:00 UTC | |
| DAY_S = 240 # one in-game day in real seconds | |
| def river_cols(cz: int) -> tuple[int, int]: | |
| """Water cell columns for a row. IDENTICAL to riverCols in web constants.""" | |
| if cz <= -5: | |
| return (7, 8) | |
| if cz <= 3: | |
| return (8, 9) | |
| return (9, 10) | |
| # kind: w, d, floors (min, max), residents, jobs, attract, duration_s | |
| BUILDINGS: dict[str, dict] = { | |
| "house": {"w": 1, "d": 1, "floors": (1, 2), "residents": 4, "jobs": 0, "attract": 0, "duration_s": 20}, | |
| "townhouse": {"w": 1, "d": 1, "floors": (2, 3), "residents": 6, "jobs": 0, "attract": 0, "duration_s": 20}, | |
| "apartments": {"w": 2, "d": 2, "floors": (4, 7), "residents": 16, "jobs": 0, "attract": 0, "duration_s": 45}, | |
| "cafe": {"w": 1, "d": 1, "floors": (1, 1), "residents": 0, "jobs": 3, "attract": 5, "duration_s": 20}, | |
| "shop": {"w": 1, "d": 1, "floors": (1, 2), "residents": 0, "jobs": 4, "attract": 4, "duration_s": 20}, | |
| "market": {"w": 1, "d": 2, "floors": (1, 1), "residents": 0, "jobs": 8, "attract": 6, "duration_s": 30}, | |
| "bank": {"w": 2, "d": 1, "floors": (2, 4), "residents": 0, "jobs": 12, "attract": 2, "duration_s": 30}, | |
| "office": {"w": 1, "d": 1, "floors": (3, 8), "residents": 0, "jobs": 40, "attract": 1, "duration_s": 30}, | |
| "tower": {"w": 2, "d": 2, "floors": (8, 16), "residents": 0, "jobs": 60, "attract": 2, "duration_s": 45}, | |
| "school": {"w": 2, "d": 2, "floors": (1, 2), "residents": 0, "jobs": 10, "attract": 3, "duration_s": 30}, | |
| "hospital": {"w": 3, "d": 3, "floors": (3, 6), "residents": 0, "jobs": 25, "attract": 2, "duration_s": 45}, | |
| "fire_station": {"w": 2, "d": 1, "floors": (2, 2), "residents": 0, "jobs": 8, "attract": 1, "duration_s": 30}, | |
| "warehouse": {"w": 2, "d": 2, "floors": (1, 1), "residents": 0, "jobs": 10, "attract": 0, "duration_s": 30}, | |
| "factory": {"w": 3, "d": 2, "floors": (1, 2), "residents": 0, "jobs": 18, "attract": 0, "duration_s": 45}, | |
| "park": {"w": 2, "d": 2, "floors": (0, 0), "residents": 0, "jobs": 0, "attract": 8, "duration_s": 20}, | |
| "plaza": {"w": 1, "d": 1, "floors": (0, 0), "residents": 0, "jobs": 0, "attract": 6, "duration_s": 20}, | |
| "stadium": {"w": 4, "d": 4, "floors": (1, 1), "residents": 0, "jobs": 15, "attract": 9, "duration_s": 45}, | |
| "church": {"w": 2, "d": 1, "floors": (1, 1), "residents": 0, "jobs": 2, "attract": 3, "duration_s": 30}, | |
| "town_hall": {"w": 2, "d": 2, "floors": (2, 2), "residents": 0, "jobs": 6, "attract": 4, "duration_s": 45}, | |
| } | |
| RESIDENTIAL_KINDS = ("house", "townhouse", "apartments") | |
| INDUSTRIAL_KINDS = ("factory", "warehouse") | |
| # The model can never pick a kind we lack; keys are space/hyphen-folded to "_". | |
| SYNONYMS: dict[str, str] = { | |
| "skyscraper": "tower", "highrise": "tower", "high_rise": "tower", | |
| "apartment": "apartments", "apartment_building": "apartments", "condo": "apartments", | |
| "hotel": "apartments", | |
| "coffee_shop": "cafe", "coffee": "cafe", "diner": "cafe", "restaurant": "cafe", | |
| "bakery": "cafe", | |
| "store": "shop", "boutique": "shop", "pharmacy": "shop", "gym": "shop", | |
| "mall": "market", "grocery": "market", "supermarket": "market", | |
| "police_station": "fire_station", "police": "fire_station", "firehouse": "fire_station", | |
| "library": "town_hall", "museum": "town_hall", "city_hall": "town_hall", | |
| "temple": "church", "mosque": "church", "chapel": "church", | |
| "garden": "park", "playground": "park", | |
| "fountain": "plaza", "square": "plaza", | |
| "arena": "stadium", | |
| "clinic": "hospital", | |
| "plant": "factory", "mill": "factory", | |
| "home": "house", "cottage": "house", "cabin": "house", | |
| } | |
| ROAD_CLASSES: dict[str, dict] = { | |
| "street": {"capacity": 2, "speed": 1.0}, | |
| "avenue": {"capacity": 6, "speed": 1.6}, | |
| } | |
| ROAD_DURATION_S = 4 # roads paint in over 4 s client-side | |
| # Curated street-name pool (genesis names — Main St, 1st Ave, Old Bridge, | |
| # River Rd, Elm St, 2nd St — are fixed in events and deliberately not here). | |
| STREET_NAMES: tuple[str, ...] = ( | |
| "Oak St", "Maple Ave", "Cedar Ln", "Birch St", "Willow Way", "Juniper St", | |
| "Laurel Ave", "Magnolia Blvd", "Poplar St", "Chestnut St", "Sycamore Ave", | |
| "Alder Row", "Hazel St", "Linden Ave", "Aspen Ct", "Rowan St", "Holly Ave", | |
| "Ivy Ln", "Clover St", "Fern Way", "Meadow Ln", "Orchard St", "Garden Ave", | |
| "Harbor St", "Mill Rd", "Canal St", "Foundry St", "Station Rd", "Depot Ln", | |
| "Prospect Ave", "Summit St", "Vista Way", "Sunrise Blvd", "Larkspur St", | |
| "Primrose Ave", "Bluebell Ln", "Copper St", "Granite Ave", "Beacon St", | |
| "Quay Rd", | |
| ) | |
| # --------------------------------------------------------------------- traffic | |
| RUSH_HOURS = (8.0, 18.0) # bell-curve centers in dayHours | |
| RUSH_SIGMA = 1.2 | |
| RUSH_BASE = 0.35 # rushFactor = base + (1-base) * min(1, bell(8) + bell(18)) | |
| RUSH_AMP = 0.65 # = 1 - RUSH_BASE; mirrored to JS for the same formula | |
| CAR_RATE = 0.8 # visible cars N = clamp(round(pop * CAR_RATE * rushFactor), MIN, MAX) | |
| CAR_MIN, CAR_MAX = 8, 120 | |
| EMA_TAU_S = 5.0 # client congestion EMA time constant | |
| CITIZENS_PER_POP = 5 # pedestrians = clamp(ceil(pop / per), MIN, MAX) — client-side | |
| CITIZENS_MIN, CITIZENS_MAX = 6, 80 | |
| TRIP_ATTRACT_FACTOR = 2.0 # weight = (jobs + 2*attract) / (1 + manhattanDist^1.5) | |
| TRIP_DIST_EXP = 1.5 | |
| # Static-assignment demand each commuter adds to every cell of their route, at | |
| # peak rush (rushFactor 1.0). TUNED (June 12, measured): genesis alone puts 3 | |
| # crossers on the Old Bridge -> ratio 0.75 (under the 0.8 fix gate); +3 west | |
| # houses -> 5 crossers -> ratio 1.25 (>= 1.0, the acceptance test passes with | |
| # margin). At 0.8 the whole genesis grid read jammed. | |
| DEMAND_PER_COMMUTER = 0.5 | |
| CONGESTION_COST_FACTOR = 2.0 # A* cost = (1/speed) * (1 + factor*demandRatio) | |
| TRAFFIC_TOP_CELLS = 15 # traffic index = round(100 * mean(top-N ratios)) | |
| FIX_GATE_RATIO = 0.8 # below this max ratio, /api/fix gets a 409 | |
| JAM_RATIO = 1.0 | |
| FIX_AVOID_RATIO = 0.9 # bypass search forbids cells at/above this ratio | |
| FIX_NEW_CELL_COST = 3.0 # bypass Dijkstra: cost per NEW road cell | |
| FIX_WATER_COST_MULT = 4.0 # water crossable at 4x (fixes may build bridges) | |
| FIX_MAX_NEW_CELLS = 16 | |
| # ------------------------------------------------------------------- placement | |
| GROWTH_RADIUS_MIN = 6 | |
| GROWTH_RING_SLACK = 2 # penalty beyond growth_radius + slack | |
| SCORE_ANCHOR = 30.0 # +30 * (1 - dist_to_anchor/R) | |
| SCORE_FRONTAGE = 20.0 # footprint perimeter touches a road | |
| SCORE_NEIGHBOR = 3.0 # per developed cell in the one-cell ring | |
| SCORE_NEIGHBOR_CAP = 18.0 | |
| SCORE_RING_PENALTY = 8.0 # -8 * max(0, cheb_from_center - (growth_radius+slack)) | |
| AFF_SHOP_NEAR_HOMES = 12.0 # cafe/shop, residential within 3 | |
| AFF_DOWNTOWN = 10.0 # office/bank/tower: +10*(1 - dist_to_center/12) | |
| AFF_HOME_NEAR_AMENITY = 8.0 # residential, cafe/shop/park within 4 | |
| AFF_HOME_NEAR_INDUSTRY = -6.0 # residential, factory/warehouse within 3 | |
| AFF_INDUSTRY_NEAR_HOMES = -10.0 | |
| AFF_INDUSTRY_CLUSTER = 8.0 # factory/warehouse, industrial within 4 | |
| AFF_PARK_SPACING = -25.0 # park, another park within 6 | |
| PLACE_R_START = 6 | |
| PLACE_R_STEP = 4 | |
| CONNECTOR_MAX = 8 # connector BFS length cap (cells of new road) | |
| # -------------------------------------------------------------------- petitions | |
| NAME_MAX_LEN = 24 | |
| INFILL_RATIO = 1.15 # jobs > housing*1.15 -> one bonus home | |
| INFILL_APARTMENTS_DEFICIT = 16 # jobs-housing gap that upgrades the infill to apartments | |