Spaces:
Running on Zero
Running on Zero
| """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 | |