DeltaZN
feat: rename god -> world
c58d3eb
Raw
History Blame Contribute Delete
12.7 kB
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Literal, cast
TerrainKind = Literal["plain_green"]
ConnectorKind = Literal["deterministic", "openai_compatible"]
OverseerMode = Literal["off", "advisor", "autopilot"]
FORCE_DETERMINISTIC_ENV = "WORLD_SIMULATOR_FORCE_DETERMINISTIC"
@dataclass(frozen=True, slots=True)
class WorldConfig:
width: int
depth: int
terrain: TerrainKind
seed: int
survival: bool = False
@dataclass(frozen=True, slots=True)
class NpcConfig:
count: int
@dataclass(frozen=True, slots=True)
class SimulationConfig:
tick_ms: int
@dataclass(frozen=True, slots=True)
class ServerConfig:
host: str
port: int
@dataclass(frozen=True, slots=True)
class ConnectorConfig:
type: ConnectorKind
base_url: str | None = None
api_url: str | None = None
model: str | None = None
api_key_env: str | None = None
timeout_seconds: int = 60
max_tokens: int = 1400
temperature: float = 0.6
top_p: float = 0.95
tool_choice: str | dict[str, Any] | None = None
max_parallel_npc_calls: int = 5
fallback_to_deterministic: bool = True
extra_body: dict[str, Any] | None = None
@dataclass(frozen=True, slots=True)
class OverseerConfig:
mode: OverseerMode = "off"
cycle_ticks: int = 8
max_directives: int = 3
connector: ConnectorConfig = field(
default_factory=lambda: ConnectorConfig(type="deterministic")
)
@dataclass(frozen=True, slots=True)
class GameConfig:
world: WorldConfig
npcs: NpcConfig
simulation: SimulationConfig
server: ServerConfig
connector: ConnectorConfig = field(
default_factory=lambda: ConnectorConfig(type="deterministic")
)
god_console: ConnectorConfig | None = None
overseer: OverseerConfig = field(default_factory=OverseerConfig)
secondary_connectors: dict[str, ConnectorConfig] = field(default_factory=dict)
def load_game_config(path: Path) -> GameConfig:
_load_dotenv_for_config(path)
with path.open("r", encoding="utf-8") as config_file:
raw = json.load(config_file)
return parse_game_config(raw)
def apply_runtime_env_overrides(config: GameConfig) -> GameConfig:
if not _truthy_env(os.getenv(FORCE_DETERMINISTIC_ENV)):
return config
deterministic_connector = ConnectorConfig(type="deterministic")
return GameConfig(
world=config.world,
npcs=config.npcs,
simulation=config.simulation,
server=config.server,
connector=deterministic_connector,
god_console=None,
overseer=OverseerConfig(
mode=config.overseer.mode,
cycle_ticks=config.overseer.cycle_ticks,
max_directives=config.overseer.max_directives,
connector=deterministic_connector,
),
)
def parse_game_config(raw: dict[str, Any]) -> GameConfig:
world = _required_mapping(raw, "world")
npcs = _required_mapping(raw, "npcs")
simulation = _required_mapping(raw, "simulation")
server = _required_mapping(raw, "server")
connector = _optional_mapping(raw, "connector") or {"type": "deterministic"}
god_console = _optional_mapping(raw, "god_console")
overseer = _optional_mapping(raw, "overseer")
terrain_raw = _required_str(world, "terrain")
if terrain_raw != "plain_green":
raise ValueError(f"Unsupported terrain kind: {terrain_raw}")
terrain: TerrainKind = "plain_green"
secondary_raw = raw.get("secondary_connectors", {})
if not isinstance(secondary_raw, dict):
raise ValueError("'secondary_connectors' must be an object.")
secondary_connectors = {
cid: _parse_connector_config(cfg)
for cid, cfg in secondary_raw.items()
if isinstance(cfg, dict)
}
return GameConfig(
world=WorldConfig(
width=_required_positive_int(world, "width"),
depth=_required_positive_int(world, "depth"),
terrain=terrain,
seed=_required_int(world, "seed"),
survival=_optional_bool(world, "survival", default=False),
),
npcs=NpcConfig(count=_required_non_negative_int(npcs, "count")),
simulation=SimulationConfig(tick_ms=_required_positive_int(simulation, "tick_ms")),
server=ServerConfig(
host=_required_str(server, "host"),
port=_required_positive_int(server, "port"),
),
connector=_parse_connector_config(connector),
god_console=_parse_connector_config(god_console) if god_console else None,
overseer=_parse_overseer_config(overseer),
secondary_connectors=secondary_connectors,
)
def _parse_overseer_config(raw: dict[str, Any] | None) -> OverseerConfig:
if raw is None:
return OverseerConfig()
mode_raw = _optional_str(raw, "mode") or "off"
if mode_raw not in ("off", "advisor", "autopilot"):
raise ValueError(f"Unsupported overseer mode: {mode_raw}")
connector_raw = _optional_mapping(raw, "connector")
if connector_raw is None:
connector_keys = {
"type",
"base_url",
"api_url",
"model",
"api_key_env",
"timeout_seconds",
"max_tokens",
"temperature",
"top_p",
"tool_choice",
"max_parallel_npc_calls",
"fallback_to_deterministic",
"extra_body",
"base_url_env",
"api_url_env",
"model_env",
}
connector_raw = {
key: value
for key, value in raw.items()
if key in connector_keys
} or {"type": "deterministic"}
return OverseerConfig(
mode=cast(OverseerMode, mode_raw),
cycle_ticks=_optional_positive_int(raw, "cycle_ticks") or 8,
max_directives=_optional_positive_int(raw, "max_directives") or 3,
connector=_parse_connector_config(connector_raw),
)
def _parse_connector_config(raw: dict[str, Any]) -> ConnectorConfig:
connector_type_raw = _optional_str(raw, "type") or "deterministic"
if connector_type_raw not in ("deterministic", "openai_compatible"):
raise ValueError(f"Unsupported connector type: {connector_type_raw}")
connector_type = cast(ConnectorKind, connector_type_raw)
base_url = _optional_str(raw, "base_url")
api_url = _optional_str(raw, "api_url")
model = _optional_str(raw, "model")
base_url_env = _optional_str(raw, "base_url_env")
api_url_env = _optional_str(raw, "api_url_env")
model_env = _optional_str(raw, "model_env")
env_base_url = os.getenv(base_url_env) if base_url_env else None
env_api_url = os.getenv(api_url_env) if api_url_env else None
if env_base_url:
base_url = env_base_url
elif env_api_url:
api_url = env_api_url
base_url = None
if model_env:
model = os.getenv(model_env) or model
temperature = _optional_non_negative_float(raw, "temperature")
top_p = _optional_probability(raw, "top_p")
return ConnectorConfig(
type=connector_type,
base_url=base_url,
api_url=api_url,
model=model,
api_key_env=_optional_str(raw, "api_key_env"),
timeout_seconds=_optional_positive_int(raw, "timeout_seconds") or 60,
max_tokens=_optional_positive_int(raw, "max_tokens") or 1400,
temperature=0.6 if temperature is None else temperature,
top_p=0.95 if top_p is None else top_p,
tool_choice=_optional_tool_choice(raw, "tool_choice"),
max_parallel_npc_calls=_optional_positive_int(raw, "max_parallel_npc_calls") or 5,
fallback_to_deterministic=_optional_bool(raw, "fallback_to_deterministic", default=True),
extra_body=_optional_mapping(raw, "extra_body"),
)
def _required_mapping(raw: dict[str, Any], key: str) -> dict[str, Any]:
value = raw.get(key)
if not isinstance(value, dict):
raise ValueError(f"Expected '{key}' to be an object.")
return value
def _optional_mapping(raw: dict[str, Any], key: str) -> dict[str, Any] | None:
value = raw.get(key)
if value is None:
return None
if not isinstance(value, dict):
raise ValueError(f"Expected '{key}' to be an object.")
return value
def _required_int(raw: dict[str, Any], key: str) -> int:
value = raw.get(key)
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"Expected '{key}' to be an integer.")
return value
def _required_positive_int(raw: dict[str, Any], key: str) -> int:
value = _required_int(raw, key)
if value <= 0:
raise ValueError(f"Expected '{key}' to be positive.")
return value
def _required_non_negative_int(raw: dict[str, Any], key: str) -> int:
value = _required_int(raw, key)
if value < 0:
raise ValueError(f"Expected '{key}' to be non-negative.")
return value
def _optional_positive_int(raw: dict[str, Any], key: str) -> int | None:
value = raw.get(key)
if value is None:
return None
if not isinstance(value, int) or isinstance(value, bool):
raise ValueError(f"Expected '{key}' to be an integer.")
if value <= 0:
raise ValueError(f"Expected '{key}' to be positive.")
return value
def _optional_non_negative_float(raw: dict[str, Any], key: str) -> float | None:
value = raw.get(key)
if value is None:
return None
if not isinstance(value, int | float) or isinstance(value, bool):
raise ValueError(f"Expected '{key}' to be a number.")
if value < 0:
raise ValueError(f"Expected '{key}' to be non-negative.")
return float(value)
def _optional_probability(raw: dict[str, Any], key: str) -> float | None:
value = raw.get(key)
if value is None:
return None
if not isinstance(value, int | float) or isinstance(value, bool):
raise ValueError(f"Expected '{key}' to be a number.")
if value <= 0 or value > 1:
raise ValueError(f"Expected '{key}' to be between 0 and 1.")
return float(value)
def _required_str(raw: dict[str, Any], key: str) -> str:
value = raw.get(key)
if not isinstance(value, str) or not value:
raise ValueError(f"Expected '{key}' to be a non-empty string.")
return value
def _optional_str(raw: dict[str, Any], key: str) -> str | None:
value = raw.get(key)
if value is None:
return None
if not isinstance(value, str) or not value:
raise ValueError(f"Expected '{key}' to be a non-empty string.")
return value
def _optional_bool(raw: dict[str, Any], key: str, *, default: bool) -> bool:
value = raw.get(key)
if value is None:
return default
if not isinstance(value, bool):
raise ValueError(f"Expected '{key}' to be a boolean.")
return value
def _optional_tool_choice(raw: dict[str, Any], key: str) -> str | dict[str, Any] | None:
value = raw.get(key)
if value is None:
return None
if isinstance(value, str):
if not value:
raise ValueError(f"Expected '{key}' to be a non-empty string or object.")
return value
if isinstance(value, dict):
return value
raise ValueError(f"Expected '{key}' to be a string or object.")
def _load_dotenv_for_config(config_path: Path) -> None:
candidates = [
Path.cwd() / ".env",
config_path.resolve().parent / ".env",
config_path.resolve().parent.parent / ".env",
]
seen: set[Path] = set()
for candidate in candidates:
if candidate in seen or not candidate.is_file():
continue
seen.add(candidate)
_load_dotenv(candidate)
def _load_dotenv(path: Path) -> None:
with path.open("r", encoding="utf-8") as dotenv_file:
for raw_line in dotenv_file:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line.removeprefix("export ").strip()
key, separator, value = line.partition("=")
if not separator:
continue
key = key.strip()
if not key:
continue
os.environ.setdefault(key, _dotenv_value(value))
def _dotenv_value(raw: str) -> str:
value = raw.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
return value[1:-1]
return value
def _truthy_env(value: str | None) -> bool:
return value is not None and value.strip().lower() in ("1", "true", "yes", "on")