| """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 |
|
|
|
|
| def make_world() -> World: |
| return World.load(genesis_features()) |
|
|
|
|
| def make_city(world=None) -> CityState: |
| return CityState.from_events((world or make_world()).features) |
|
|
|
|
| |
|
|
|
|
| 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 |
| |
| 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 |
|
|
|
|
| |
|
|
|
|
| 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" |
| 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 |
| 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:") |
|
|
|
|
| |
|
|
|
|
| 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(): |
| |
| 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 |
|
|
|
|
| |
|
|
|
|
| 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" |
| 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(): |
| |
| |
| 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" |
| |
| 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 |
|
|
|
|
| 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(): |
| |
| 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 |
|
|