"""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