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