"""Tests for the NEMOCITY deterministic city engine. Covers: genesis validity + rebuild equality, synonym/clamp/name behavior, placement determinism + the never-fails property, the queue worker grant path (act -> placement -> events), and the demand-driven infill rule. Engine tests import no fastapi/mind — pure engine surface. """ import asyncio import sys import zlib from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from engine import constants as C from engine import placement, tools from engine.city import WATER_CELLS, CityState from engine.genesis import GENESIS_FEATURES, genesis_features from engine.moderation import Moderator from engine.queue_worker import QueueWorker, TrafficSmooth from engine.world import World NOW = C.CITY_EPOCH_S + 10_000 # everything in genesis long complete def make_world() -> World: return World.load(genesis_features()) def make_city(world=None) -> CityState: return CityState.from_events((world or make_world()).features) # --------------------------------------------------------------------- genesis def test_genesis_shape_and_seeds(): assert [f["id"] for f in GENESIS_FEATURES] == [f"e_{i:06d}" for i in range(19)] for i, f in enumerate(GENESIS_FEATURES): assert f["wish_id"] == "genesis" assert f["t"] == float(C.CITY_EPOCH_S) assert f["seed"] == zlib.crc32(f"genesis:{i}".encode()) & 0x7FFFFFFF assert GENESIS_FEATURES[-1]["tool"] == "note" def test_genesis_city_facts(): city = make_city() assert city.population(NOW) == 20 assert city.jobs == 85 assert city.housing_capacity == 20 assert city.road_version == 6 # the Old Bridge is the only crossing, street class, on water for cell in ((8, 0), (9, 0)): rc = city.roads[cell] assert rc.bridge and rc.klass == "street" and rc.name == "Old Bridge" assert all( not rc.bridge for cell, rc in city.roads.items() if rc.name != "Old Bridge" ) def test_genesis_buildings_valid(): city = CityState() for ev in genesis_features(): if ev["tool"] == "place_building": a = ev["args"] cells = [ (a["cx"] + dx, a["cz"] + dz) for dx in range(a["w"]) for dz in range(a["d"]) ] assert all(city.is_empty(c) for c in cells), a["name"] city.apply(ev) full = make_city() for b in full.buildings: assert full.door(b) is not None, f"{b.name} touches no road" def test_world_rebuild_equality(): w1 = make_world() w2 = World.load([f.to_dict() for f in w1.features]) assert [f.to_dict() for f in w1.features] == [f.to_dict() for f in w2.features] assert w1.version == w2.version == 19 assert w1.epoch == 0 # ------------------------------------------------------------ synonyms / clamps def test_resolve_kind_synonyms(): assert tools.resolve_kind("skyscraper") == "tower" assert tools.resolve_kind("Coffee Shop") == "cafe" assert tools.resolve_kind("apartment building") == "apartments" assert tools.resolve_kind("police station") == "fire_station" assert tools.resolve_kind("city hall") == "town_hall" assert tools.resolve_kind("grocery") == "market" assert tools.resolve_kind("hotel") == "apartments" assert tools.resolve_kind("ramen stand") == "house" # unknown -> house assert tools.resolve_kind(None) == "house" assert tools.resolve_kind("towers") == "tower" def test_place_building_clamps(): args, err = tools.validate_call("place_building", { "kind": "office", "name": "A" * 80, "cx": -999, "cz": 999, "floors": 99, "hue": 9999, "variant": 42, }) assert err is None assert args["floors"] == 8 # office max assert args["cx"] == C.COORD_MIN and args["cz"] == C.COORD_MAX assert len(args["name"]) <= C.NAME_MAX_LEN assert args["hue"] == 360 and args["variant"] == 7 assert args["w"] == 1 and args["d"] == 1 def test_unknown_tool_rejected(): args, err = tools.validate_call("summon_dragon", {}) assert args is None and err.startswith("rejected:") def test_sanitize_name_wordlist_and_length(): assert tools.sanitize_name("Cafe Luna") == "Cafe Luna" assert tools.sanitize_name("X" * 99) == "X" * C.NAME_MAX_LEN assert tools.sanitize_name("hitler plaza", default="The Plaza") == "The Plaza" assert tools.sanitize_name(None, default="D") == "D" name = tools.default_name("cafe", "a ramen shop near the park", "w_000001") assert 0 < len(name) <= C.NAME_MAX_LEN assert name == tools.default_name("cafe", "a ramen shop near the park", "w_000001") def test_note_moderated(): args, err = tools.validate_call("note", {"text": "nazi parade", "kind": "milestone"}) assert args is None and err.startswith("rejected:") # ------------------------------------------------------------------- placement def test_placement_deterministic(): runs = [] for _ in range(2): city = make_city() events = placement.place( city, kind="cafe", name="Cafe Luna", floors=None, hue=None, near="the park", wish_id="w_000007", call_index=0, ) runs.append(events) assert runs[0] == runs[1] args = runs[0][0]["args"] assert args["kind"] == "cafe" and args["w"] == 1 and args["d"] == 1 def test_placement_near_park_lands_near_park(): city = make_city() events = placement.place( city, "cafe", "Cafe Luna", None, None, "the park", "w_000002", 0, ) a = events[0]["args"] park = next(b for b in city.buildings if b.kind == "park") px, pz = park.centroid assert abs(a["cx"] - px) <= 6 and abs(a["cz"] - pz) <= 6 def test_placement_never_fails_50_requests(): world = make_world() city = make_city(world) kinds = list(C.BUILDINGS) nears = ("the park", "downtown", "the river", "Main St", "", "Riverside Works") for i in range(50): kind = kinds[zlib.crc32(f"k:{i}".encode()) % len(kinds)] near = nears[zlib.crc32(f"n:{i}".encode()) % len(nears)] wish_id = f"w_{i + 10:06d}" events = placement.place(city, kind, f"Test {i}", None, None, near, wish_id, 0) assert events and events[0]["tool"] == "place_building" a = events[0]["args"] cells = [ (a["cx"] + dx, a["cz"] + dz) for dx in range(a["w"]) for dz in range(a["d"]) ] for c in cells: assert city.is_empty(c), f"request {i} ({kind}) overlaps at {c}" for j, ev in enumerate(events): feature, obs = world.apply(wish_id, j, ev, t=NOW) assert feature is not None, obs city.apply(feature) b = city.buildings[-1] assert city.door(b) is not None, f"{kind} #{i} ended up roadless" def test_connector_when_no_frontage(): # Anchor far from roads: a remote park forces an auto-routed connector. city = make_city() events = placement.place(city, "stadium", "Big Bowl", None, None, "", "w_000099", 0) for ev in events: args, err = tools.validate_call(ev["tool"], ev["args"]) assert err is None if len(events) == 2: assert events[1]["tool"] == "lay_road" assert events[1]["args"]["klass"] == "street" assert 1 <= len(events[1]["args"]["cells"]) <= 2 * C.CONNECTOR_MAX # ----------------------------------------------------------------- queue worker class _OneShotPlanner: """Calls act once per scripted building request, like mind.planner.""" def __init__(self, requests): self.requests = requests async def grant(self, wish, world_summary, act, emit): await emit({"type": "plan", "plan": {"buildings": self.requests}}) turns = [] for req in self.requests: obs = await act(req) turns.append({"thought": "", "call": req, "observation": obs}) return { "reading": "Permit approved.", "turns": turns, "epitaph": "A fine addition to the skyline.", "ms_total": 1, "model": "test", "backend": "test", } def _run_worker(planner, world): events = [] async def emit(event): events.append(event) worker = QueueWorker( world=world, moderator=Moderator(), planner=planner, emit=emit, ) async def go(): wish_id, position = await worker.submit("a tall tower downtown", "client-1") assert position == 1 await worker.start() await asyncio.wait_for(worker._queue.join(), 10) await worker.stop() return wish_id wish_id = asyncio.run(go()) return wish_id, events, worker def test_grant_path_places_building_and_emits(): world = make_world() planner = _OneShotPlanner([{"kind": "skyscraper", "name": "Apex Tower", "near": "downtown", "floors": 12, "hue": 210}]) wish_id, events, _ = _run_worker(planner, world) types = [e["type"] for e in events] assert "plan" in types and "wish_granted" in types plan_event = next(e for e in events if e["type"] == "plan") assert plan_event["wish_id"] == wish_id placed = [f for f in world.features if f.wish_id == wish_id and f.tool == "place_building"] assert placed and placed[0].args["kind"] == "tower" # synonym repaired deltas = [e for e in events if e["type"] == "world_delta"] assert len(deltas) == len([f for f in world.features if f.wish_id == wish_id]) def test_infill_rule_one_bonus_home(): # Genesis jobs (85) already exceed housing (20) * 1.15 -> a granted petition # that adds MORE jobs must pull in exactly ONE bonus home + note. world = make_world() planner = _OneShotPlanner([{"kind": "office", "name": "More Jobs Inc", "near": "downtown"}]) wish_id, events, _ = _run_worker(planner, world) mine = [f for f in world.features if f.wish_id == wish_id] homes = [f for f in mine if f.tool == "place_building" and f.args["kind"] in C.RESIDENTIAL_KINDS] notes = [f for f in mine if f.tool == "note"] assert len(homes) == 1 assert len(notes) == 1 and notes[0].args["kind"] == "infill" # jobs - housing >= 16 -> the infill upgrades to apartments assert homes[0].args["kind"] == "apartments" def test_moderation_rejects_before_planning(): world = make_world() planner = _OneShotPlanner([{"kind": "house"}]) events = [] async def emit(event): events.append(event) worker = QueueWorker(world=world, moderator=Moderator(), planner=planner, emit=emit) async def go(): await worker.submit("build a nazi museum", "client-2") await worker.start() await asyncio.wait_for(worker._queue.join(), 10) await worker.stop() asyncio.run(go()) assert any(e["type"] == "wish_rejected" for e in events) assert world.version == 19 # nothing landed def test_summary_under_600_chars(): world = make_world() text = world.summary(NOW) assert len(text) < 600 assert "NEMOCITY" in text and "pop 20" in text def test_submit_fix_gate_smooth_when_no_trips(): # A roads-only world has zero demand -> TrafficSmooth. world = World.load([f for f in genesis_features() if f["tool"] == "lay_road"]) worker = QueueWorker(world=world, moderator=Moderator(), planner=None, emit=_noop) async def go(): with pytest.raises(TrafficSmooth): await worker.submit_fix("client-3") asyncio.run(go()) async def _noop(event): return None