Spaces:
Running on Zero
Running on Zero
| """NEMOCITY placement — deterministic, NEVER fails, Python-only. | |
| The LLM gives {kind, name, near, floors, hue}; this module turns it into | |
| exact cells: anchor resolution from the "near" text, a scored candidate | |
| search over an expanding Chebyshev radius, and an auto-routed road connector | |
| when the chosen footprint has no frontage. Output is partial event dicts | |
| ({"tool", "args"}) — world.apply assigns id/seed/t. | |
| All randomness is crc32-seeded from (wish_id, indices); no wall clock here. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import zlib | |
| from collections import deque | |
| from typing import Any, Optional | |
| from . import constants as C | |
| from .city import WATER_CELLS, Building, Cell, CityState, in_bounds, neighbors4 | |
| from .tools import street_name_for | |
| _CENTER_WORDS = frozenset(("center", "centre", "downtown", "middle", "heart")) | |
| _RIVER_WORDS = frozenset(("river", "water", "waterfront", "riverside", "shore")) | |
| _STREET_SUFFIXES = frozenset(("st", "ave", "rd", "ln", "blvd", "way", "ct", "row")) | |
| _STOP = frozenset(("the", "a", "an", "of", "near", "by", "next", "to", "at", "in", "on")) | |
| _R_MAX = 2 * C.GRID # covers the whole grid from any anchor | |
| def _u01(key: str) -> float: | |
| return (zlib.crc32(key.encode()) & 0xFFFFFFFF) / 0xFFFFFFFF | |
| def _tokens(text: str) -> list[str]: | |
| return [ | |
| t for t in re.findall(r"[a-z0-9']+", str(text or "").lower()) | |
| if t not in _STOP | |
| ] | |
| def _round_cell(x: float, z: float) -> Cell: | |
| cx = min(max(int(round(x)), C.COORD_MIN), C.COORD_MAX) | |
| cz = min(max(int(round(z)), C.COORD_MIN), C.COORD_MAX) | |
| return (cx, cz) | |
| def resolve_anchor(city: CityState, near: Any) -> Cell: | |
| """Fuzzy anchor: named building > street > center > river > kind word > | |
| centroid of all buildings > (0,0).""" | |
| text = str(near or "").strip().lower() | |
| toks = _tokens(text) | |
| if toks: | |
| for b in reversed(city.buildings): | |
| name_toks = set(_tokens(b.name)) | |
| if name_toks and (set(toks) & name_toks): | |
| return _round_cell(*b.centroid) | |
| for name, cells in city.street_cells.items(): | |
| low = name.lower() | |
| name_toks = set(_tokens(low)) - _STREET_SUFFIXES | |
| if low in text or (name_toks and set(toks) & name_toks): | |
| return cells[len(cells) // 2] | |
| if set(toks) & _CENTER_WORDS: | |
| return (0, 0) | |
| if set(toks) & _RIVER_WORDS: | |
| developed = set(city.roads) | set(city.building_cells) | |
| banks = sorted( | |
| (c for c in developed if any(n in WATER_CELLS for n in neighbors4(c))), | |
| key=lambda c: (max(abs(c[0]), abs(c[1])), c[1], c[0]), | |
| ) | |
| if banks: | |
| return banks[0] | |
| for t in toks: | |
| kind = t if t in C.BUILDINGS else C.SYNONYMS.get(t) | |
| if kind: | |
| for b in reversed(city.buildings): | |
| if b.kind == kind: | |
| return _round_cell(*b.centroid) | |
| if city.buildings: | |
| xs = [b.centroid[0] for b in city.buildings] | |
| zs = [b.centroid[1] for b in city.buildings] | |
| return _round_cell(sum(xs) / len(xs), sum(zs) / len(zs)) | |
| return (0, 0) | |
| # ----------------------------------------------------------------- connectors | |
| def _road_distance_field(city: CityState, max_dist: int) -> dict[Cell, int]: | |
| """Multi-source BFS over EMPTY cells from the road network. dist=1 means | |
| 'this empty cell is adjacent to a road' (= a 1-cell connector).""" | |
| dist: dict[Cell, int] = {} | |
| queue: deque[Cell] = deque() | |
| seeds = set() | |
| for road_cell in city.roads: | |
| for n in neighbors4(road_cell): | |
| if city.is_empty(n) and n not in seeds: | |
| seeds.add(n) | |
| for cell in sorted(seeds, key=lambda c: (c[1], c[0])): | |
| dist[cell] = 1 | |
| queue.append(cell) | |
| while queue: | |
| cell = queue.popleft() | |
| d = dist[cell] | |
| if d >= max_dist: | |
| continue | |
| for n in neighbors4(cell): | |
| if n not in dist and city.is_empty(n): | |
| dist[n] = d + 1 | |
| queue.append(n) | |
| return dist | |
| def _connector_path( | |
| city: CityState, footprint: set[Cell], max_len: int | |
| ) -> Optional[list[Cell]]: | |
| """Shortest Manhattan path of NEW road cells from the footprint ring to a | |
| cell adjacent to the existing road network. Deterministic BFS order.""" | |
| starts = sorted( | |
| { | |
| n for cell in footprint for n in neighbors4(cell) | |
| if city.is_empty(n) and n not in footprint | |
| }, | |
| key=lambda c: (c[1], c[0]), | |
| ) | |
| prev: dict[Cell, Optional[Cell]] = {} | |
| queue: deque[tuple[Cell, int]] = deque() | |
| for s in starts: | |
| prev[s] = None | |
| queue.append((s, 1)) | |
| while queue: | |
| cell, length = queue.popleft() | |
| if any(n in city.roads for n in neighbors4(cell)): | |
| path = [cell] | |
| while prev[path[-1]] is not None: | |
| path.append(prev[path[-1]]) | |
| path.reverse() | |
| return path | |
| if length >= max_len: | |
| continue | |
| for n in neighbors4(cell): | |
| if n not in prev and city.is_empty(n) and n not in footprint: | |
| prev[n] = cell | |
| queue.append((n, length + 1)) | |
| return None | |
| # -------------------------------------------------------------------- scoring | |
| def _rect_cheb(b: Building, cx: int, cz: int, w: int, d: int) -> int: | |
| """Exact min Chebyshev distance between a building and a candidate rect.""" | |
| dx = max(0, b.cx - (cx + w - 1), cx - (b.cx + b.w - 1)) | |
| dz = max(0, b.cz - (cz + d - 1), cz - (b.cz + b.d - 1)) | |
| return max(dx, dz) | |
| def _affinity(city: CityState, kind: str, cx: int, cz: int, w: int, d: int) -> float: | |
| res = ind = amen = park = 10 ** 6 | |
| for b in city.buildings: | |
| dist = _rect_cheb(b, cx, cz, w, d) | |
| if b.kind in C.RESIDENTIAL_KINDS: | |
| res = min(res, dist) | |
| if b.kind in C.INDUSTRIAL_KINDS: | |
| ind = min(ind, dist) | |
| if b.kind in ("cafe", "shop", "park"): | |
| amen = min(amen, dist) | |
| if b.kind == "park": | |
| park = min(park, dist) | |
| score = 0.0 | |
| if kind in ("cafe", "shop") and res <= 3: | |
| score += C.AFF_SHOP_NEAR_HOMES | |
| if kind in ("office", "bank", "tower"): | |
| dist_c = max(abs(cx), abs(cz)) | |
| score += C.AFF_DOWNTOWN * (1 - dist_c / 12.0) | |
| if kind in C.RESIDENTIAL_KINDS: | |
| if amen <= 4: | |
| score += C.AFF_HOME_NEAR_AMENITY | |
| if ind <= 3: | |
| score += C.AFF_HOME_NEAR_INDUSTRY | |
| if kind in C.INDUSTRIAL_KINDS: | |
| if res <= 3: | |
| score += C.AFF_INDUSTRY_NEAR_HOMES | |
| if ind <= 4: | |
| score += C.AFF_INDUSTRY_CLUSTER | |
| if kind == "park" and park <= 6: | |
| score += C.AFF_PARK_SPACING | |
| return score | |
| def _search( | |
| city: CityState, kind: str, anchor: Cell, wish_id: str, connector_max: int | |
| ) -> Optional[tuple[Cell, bool]]: | |
| """Best (top_left, has_frontage) within the smallest radius that holds a | |
| valid candidate, or None.""" | |
| spec = C.BUILDINGS[kind] | |
| w, d = spec["w"], spec["d"] | |
| growth = city.growth_radius | |
| ring_limit = growth + C.GROWTH_RING_SLACK | |
| road_dist = _road_distance_field(city, max(connector_max, 1) + 1) | |
| ax, az = anchor | |
| radius = C.PLACE_R_START | |
| while radius <= _R_MAX: | |
| best: Optional[tuple[float, Cell, bool]] = None | |
| index = 0 | |
| for cz in range(az - radius, az + radius + 1): | |
| for cx in range(ax - radius, ax + radius + 1): | |
| index += 1 | |
| cells = [(cx + dx, cz + dz) for dx in range(w) for dz in range(d)] | |
| if not all(city.is_empty(c) for c in cells): | |
| continue | |
| footprint = set(cells) | |
| ring = { | |
| n for c in cells for n in neighbors4(c) if n not in footprint | |
| } | |
| frontage = any(n in city.roads for n in ring) | |
| if not frontage and not any( | |
| road_dist.get(n, 10 ** 9) <= connector_max for n in ring | |
| ): | |
| continue | |
| dist_anchor = max(abs(cx - ax), abs(cz - az)) | |
| score = C.SCORE_ANCHOR * (1 - dist_anchor / radius) | |
| if frontage: | |
| score += C.SCORE_FRONTAGE | |
| developed = sum( | |
| 1 for n in ring if n in city.roads or n in city.building_cells | |
| ) | |
| score += min(C.SCORE_NEIGHBOR * developed, C.SCORE_NEIGHBOR_CAP) | |
| score += _affinity(city, kind, cx, cz, w, d) | |
| cheb_center = max(abs(cx), abs(cz)) | |
| score -= C.SCORE_RING_PENALTY * max(0, cheb_center - ring_limit) | |
| score += _u01(f"{wish_id}:{index}") * 0.5 # seeded tie-break | |
| if best is None or score > best[0]: | |
| best = (score, (cx, cz), frontage) | |
| if best is not None: | |
| return best[1], best[2] | |
| radius += C.PLACE_R_STEP | |
| return None | |
| # ----------------------------------------------------------------------- place | |
| def place( | |
| city: CityState, | |
| kind: str, | |
| name: str, | |
| floors: Any, | |
| hue: Any, | |
| near: Any, | |
| wish_id: str, | |
| call_index: int, | |
| ) -> list[dict]: | |
| """Resolve a building request into 1-2 events (place_building + optional | |
| connector lay_road). Never fails: the radius grows, the connector cap | |
| relaxes, and a footprint that fits nowhere degrades to a house.""" | |
| if kind not in C.BUILDINGS: | |
| kind = "house" | |
| anchor = resolve_anchor(city, near) | |
| found = _search(city, kind, anchor, wish_id, C.CONNECTOR_MAX) | |
| if found is None: | |
| found = _search(city, kind, anchor, wish_id, 2 * C.CONNECTOR_MAX) | |
| if found is None and kind != "house": | |
| return place(city, "house", name, 1, hue, near, wish_id, call_index) | |
| if found is None: # a 64x64 grid with any road at all always has a slot | |
| found = _search(city, kind, anchor, wish_id, 10 ** 6) | |
| if found is None: | |
| raise RuntimeError("placement impossible: no empty cell on the grid") | |
| (cx, cz), frontage = found | |
| spec = C.BUILDINGS[kind] | |
| lo, hi = spec["floors"] | |
| base = None | |
| if isinstance(floors, (int, float)) and not isinstance(floors, bool): | |
| base = int(round(floors)) | |
| if kind in ("office", "tower"): | |
| if base is None: | |
| base = (lo + hi) // 2 | |
| dist_c = max(abs(cx), abs(cz)) | |
| base = base + round(4 * (1 - dist_c / 10.0)) # taller downtown | |
| elif base is None: | |
| base = lo + zlib.crc32(f"{wish_id}:{call_index}:floors".encode()) % (hi - lo + 1) | |
| final_floors = min(max(base, lo), hi) | |
| if isinstance(hue, (int, float)) and not isinstance(hue, bool): | |
| final_hue = int(round(hue)) % 361 | |
| else: | |
| final_hue = zlib.crc32(f"{wish_id}:{call_index}:hue".encode()) % 360 | |
| variant = zlib.crc32(f"{wish_id}:{call_index}:variant".encode()) % 8 | |
| events: list[dict] = [{ | |
| "tool": "place_building", | |
| "args": { | |
| "kind": kind, "name": str(name or ""), "cx": cx, "cz": cz, | |
| "w": spec["w"], "d": spec["d"], "floors": final_floors, | |
| "hue": final_hue, "variant": variant, | |
| }, | |
| }] | |
| if not frontage: | |
| footprint = { | |
| (cx + dx, cz + dz) for dx in range(spec["w"]) for dz in range(spec["d"]) | |
| } | |
| path = _connector_path(city, footprint, 2 * C.CONNECTOR_MAX) or \ | |
| _connector_path(city, footprint, 10 ** 6) | |
| if path: | |
| events.append({ | |
| "tool": "lay_road", | |
| "args": { | |
| "cells": [[c[0], c[1]] for c in path], | |
| "klass": "street", | |
| "name": street_name_for(f"{wish_id}:{call_index}:road"), | |
| }, | |
| }) | |
| return events | |