Spaces:
Sleeping
Sleeping
Town Mode: close town view by default, build_district + place_road, bank/market/house, grow-one-town steering, behold-the-world reveal, tamed needle
b0d758d verified | """Compact world description for the LLM prompt (<600 chars, always). | |
| Covers: the town (name + center + building tally), epoch, terrain count + | |
| notable coords, flora kinds, structures with coords, current sky/weather, last | |
| 2 epitaphs. Duck-types over World so this module stays import-free of | |
| engine.world (no cycles). | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import zlib | |
| from typing import Any | |
| MAX_LEN = 599 # contract: strictly under 600 chars | |
| _NOTABLE_TERRAIN = 2 | |
| _MAX_FLORA_KINDS = 6 | |
| _MAX_STRUCTURES = 4 | |
| _EPITAPH_CLIP = 70 | |
| # -------------------------------------------------------------------------- | |
| # Town anchor — the seeded-weighted centroid of the built world (v1.2) | |
| # -------------------------------------------------------------------------- | |
| # | |
| # townCenter is a CROSS-COMPONENT contract: the JS renderer's camera computes | |
| # the SAME value to frame the default TOWN view, so this math must be matched | |
| # byte-for-byte. It is the unit-vector mean of every non-genesis place_structure | |
| # + build_district direction, renormalized back to (lat, lon). With nothing | |
| # built yet it falls back to the genesis monolith. | |
| # The genesis monolith (f_000002) — the town's seed when nothing else is built. | |
| GENESIS_MONOLITH = (14.0, 38.0) | |
| # Town-anchoring tools (their lat/lon define where the town sits). | |
| _TOWN_TOOLS = ("place_structure", "build_district") | |
| # A curated name pool; crc32(round(lat), round(lon)) selects one deterministically. | |
| TOWN_NAMES: tuple[str, ...] = ( | |
| "Lowmere", "Ashford", "Tessen", "Harrowfen", "Mossgate", "Brighthollow", | |
| "Stilwater", "Emberlyn", "Caldmoor", "Vesper", "Thornwick", "Glasswend", | |
| "Dunmarrow", "Sorrel", "Wyndholt", "Pellan", | |
| ) | |
| def town_center(world: Any) -> tuple[float, float]: | |
| """Return the town anchor (lat, lon) for a world. | |
| The unit-vector mean of all non-genesis ``place_structure`` + | |
| ``build_district`` directions, renormalized to a (lat, lon) pair. Falls back | |
| to the genesis monolith at (14, 38) when nothing has been built. The result | |
| lat/lon are rounded to 4 places (stable across the engine/renderer split). | |
| """ | |
| features = list(getattr(world, "features", ()) or ()) | |
| x = y = z = 0.0 | |
| n = 0 | |
| for f in features: | |
| if f.wish_id == "genesis" or f.tool not in _TOWN_TOOLS: | |
| continue | |
| lat = f.args.get("lat") | |
| lon = f.args.get("lon") | |
| if lat is None or lon is None: | |
| continue | |
| rlat = math.radians(float(lat)) | |
| rlon = math.radians(float(lon)) | |
| cos_lat = math.cos(rlat) | |
| x += cos_lat * math.cos(rlon) | |
| y += cos_lat * math.sin(rlon) | |
| z += math.sin(rlat) | |
| n += 1 | |
| if n == 0: | |
| return GENESIS_MONOLITH | |
| norm = math.sqrt(x * x + y * y + z * z) | |
| if norm == 0.0: # antipodal directions cancel; fall back rather than divide by zero | |
| return GENESIS_MONOLITH | |
| x, y, z = x / norm, y / norm, z / norm | |
| lat = math.degrees(math.asin(max(-1.0, min(1.0, z)))) | |
| lon = math.degrees(math.atan2(y, x)) | |
| return round(lat, 4), round(lon, 4) | |
| def town_name(lat: float, lon: float) -> str: | |
| """Deterministic town name from its rounded center (crc32 -> curated pool).""" | |
| key = f"{round(lat)},{round(lon)}".encode("utf-8") | |
| return TOWN_NAMES[zlib.crc32(key) % len(TOWN_NAMES)] | |
| def _coord(args: dict) -> str: | |
| lat, lon = args.get("lat"), args.get("lon") | |
| if lat is None or lon is None: | |
| path = args.get("path") or [] | |
| if path and isinstance(path[0], (list, tuple)) and len(path[0]) >= 2: | |
| lat, lon = path[0][0], path[0][1] | |
| if lat is None or lon is None: | |
| return "" | |
| return f"({round(float(lat))},{round(float(lon))})" | |
| def _clip(text: str, limit: int) -> str: | |
| text = str(text) | |
| return text if len(text) <= limit else text[: limit - 3] + "..." | |
| _A_OR_AN_VOWEL = "aeiou" | |
| _MAX_TALLY_KINDS = 5 # cap the building tally so the line stays short | |
| def _count_phrase(noun: str, n: int) -> str: | |
| """'3 towers' / 'a cafe' (singular gets an article).""" | |
| if n == 1: | |
| article = "an" if noun[:1].lower() in _A_OR_AN_VOWEL else "a" | |
| return f"{article} {noun}" | |
| plural = noun + ("es" if noun.endswith(("s", "x", "z", "ch", "sh")) else "s") | |
| return f"{n} {plural}" | |
| def _town_tally(features: list) -> str: | |
| """A short building/house tally, e.g. '3 towers, a cafe, 2 districts'.""" | |
| counts: dict[str, int] = {} | |
| order: list[str] = [] | |
| for f in features: | |
| if f.wish_id == "genesis": | |
| continue | |
| if f.tool == "place_structure": | |
| kind = f.args.get("kind", "building") | |
| elif f.tool == "build_district": | |
| kind = "district" | |
| else: | |
| continue | |
| if kind not in counts: | |
| order.append(kind) | |
| counts[kind] = counts.get(kind, 0) + 1 | |
| if not counts: | |
| return "" | |
| bits = [_count_phrase(kind, counts[kind]) for kind in order[:_MAX_TALLY_KINDS]] | |
| if len(order) > _MAX_TALLY_KINDS: | |
| bits.append("more") | |
| return ", ".join(bits) | |
| def summarize(world: Any) -> str: | |
| features = list(getattr(world, "features", ()) or ()) | |
| # The town — where the god should keep building (v1.2 town mode). | |
| lat, lon = town_center(world) | |
| name = town_name(lat, lon) | |
| tally = _town_tally(features) | |
| town_line = f"The town of {name} stands near ({round(lat)},{round(lon)})" | |
| town_line += f": {tally}." if tally else "; nothing built yet — start it here." | |
| parts: list[str] = [town_line, f"epoch {getattr(world, 'epoch', 0)}"] | |
| # terrain | |
| raised = [f for f in features if f.tool == "raise_terrain"] | |
| lowered = [f for f in features if f.tool == "lower_terrain"] | |
| terrain = f"terrain: {len(raised)} risen, {len(lowered)} carved" | |
| notable = [_coord(f.args) for f in raised[-_NOTABLE_TERRAIN:] if _coord(f.args)] | |
| if notable: | |
| terrain += " at " + ",".join(notable) | |
| parts.append(terrain) | |
| # flora kinds (unique, first-seen order) | |
| kinds: list[str] = [] | |
| for f in features: | |
| if f.tool == "spawn_flora": | |
| kind = f.args.get("kind") | |
| if kind and kind not in kinds: | |
| kinds.append(kind) | |
| parts.append("flora: " + (", ".join(kinds[:_MAX_FLORA_KINDS]) if kinds else "none")) | |
| # structures with coords (most recent few) | |
| structures = [f for f in features if f.tool == "place_structure"] | |
| if structures: | |
| bits = [f"{f.args.get('kind', '?')}{_coord(f.args)}" for f in structures[-_MAX_STRUCTURES:]] | |
| extra = len(structures) - _MAX_STRUCTURES | |
| tail = f" +{extra} more" if extra > 0 else "" | |
| parts.append("structures: " + ", ".join(bits) + tail) | |
| else: | |
| parts.append("structures: none") | |
| # sky / weather (last-write-wins views) | |
| sky = getattr(world, "sky", None) | |
| if sky: | |
| moons = sky.get("moons", 0) | |
| parts.append(f"sky: {sky.get('palette', '?')}, {moons} moon{'s' if moons != 1 else ''}") | |
| weather = getattr(world, "weather", None) | |
| if weather: | |
| parts.append(f"weather: {weather.get('kind', 'clear')}") | |
| # last 2 epitaphs | |
| epitaphs = list(getattr(world, "epitaphs", ()) or ())[-2:] | |
| if epitaphs: | |
| quoted = " / ".join(f"\"{_clip(e, _EPITAPH_CLIP)}\"" for e in epitaphs) | |
| parts.append("last words: " + quoted) | |
| text = " | ".join(parts) | |
| if len(text) > MAX_LEN: | |
| text = text[: MAX_LEN - 3] + "..." | |
| return text | |