nemocity / engine /constants.py
AndresCarreon's picture
NEMOCITY v0 — mock backend, gradio 6.16.0 (pre-SSR)
d72231c verified
Raw
History Blame Contribute Delete
8.01 kB
"""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