DeltaZN
feat: improved perception
54db442
Raw
History Blame Contribute Delete
9.35 kB
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from typing import Any, Literal
GoalType = Literal[
"survive",
"pursue_and_attack_target",
"retaliate",
"call_for_help",
"help_or_defend",
"respond_to_event",
"investigate",
"find_food",
"routine_life",
]
CitizenMode = Literal[
"routine",
"threatened",
"hostile_pursuit",
"helping",
"investigating",
"fleeing",
"recovering",
]
NpcRole = Literal["guard", "builder", "gatherer"]
HouseState = Literal["under_construction", "completed", "destroyed"]
GameStatus = Literal["running", "win_population", "win_survival", "lose"]
@dataclass(frozen=True, slots=True)
class Vec3:
x: float
y: float
z: float
@dataclass(slots=True)
class MemoryEntry:
tick: int
text: str
@dataclass(slots=True)
class MemoryEpisode:
tick: int
kind: str
actor_id: str
target_id: str | None
object_id: str | None
subject_kind: str
perspective: str
summary: str
emotional_weight: float = 0.5
tags: list[str] = field(default_factory=list)
@dataclass(slots=True)
class NpcMemory:
episodes: list[MemoryEpisode] = field(default_factory=list)
facts: list[str] = field(default_factory=list)
@dataclass(slots=True)
class NpcConversationContext:
messages: list[dict[str, str]] = field(default_factory=list)
max_turns: int = 5
def add_turn(self, user_content: str, assistant_content: str) -> None:
self.messages.append({"role": "user", "content": user_content})
self.messages.append({"role": "assistant", "content": assistant_content})
max_messages = self.max_turns * 2
if len(self.messages) > max_messages:
self.messages = self.messages[-max_messages:]
@dataclass(slots=True)
class CitizenNeeds:
safety: int = 20
food: int = 25
social: int = 30
status: int = 30
curiosity: int = 35
rest: int = 20
@dataclass(slots=True)
class CitizenEmotions:
fear: int = 0
anger: int = 0
trust_baseline: int = 50
stress: int = 0
@dataclass(slots=True)
class CitizenGoal:
goal_type: GoalType
priority: int
reason: str
target_npc_id: str | None = None
target_entity_id: str | None = None
expires_at_tick: int | None = None
@dataclass(slots=True)
class Npc:
id: str
name: str
role: str
position: Vec3
health: int = 100
attack_damage: int = 10
personality: str = "neutral"
god_directive: str | None = None
directive_source: str | None = None
directive_issued_tick: int | None = None
directive_ttl_ticks: int | None = None
intention: str = "idle"
# Latest model chain-of-thought ("thinking") and the tick it was produced on,
# surfaced to the UI. Empty for non-thinking turns.
last_reasoning: str | None = None
last_reasoning_tick: int | None = None
memory_summary: str | None = None
# True once the NPC has written its OWN memory_summary (the model owns it).
# While set, the engine never appends raw "Tick N: ..." lines to it.
memory_summary_self_authored: bool = False
memory: list[MemoryEntry] = field(default_factory=list)
structured_memory: NpcMemory = field(default_factory=NpcMemory)
conversation_context: NpcConversationContext = field(default_factory=NpcConversationContext)
relationships: dict[str, float] = field(default_factory=dict)
needs: CitizenNeeds = field(default_factory=CitizenNeeds)
emotions: CitizenEmotions = field(default_factory=CitizenEmotions)
current_goal: CitizenGoal | None = None
mode: CitizenMode = "routine"
# Survival core-loop state (deterministic, engine-owned).
hunger: float = 25.0
fear: float = 0.0
safety: float = 0.0
age: int = 0
max_age: int = 400
reproduction_cooldown: int = 0
importance: float = 1.0
survival_goal: str = "routine_life"
inventory_food: int = 0
inventory_herbs: int = 0
inventory_wood: int = 0
inventory_weapon: int = 0
inventory_coins: int = 0
help_target_id: str | None = None
# Whom the NPC is currently engaging (talk/attack/trade); drives facing in the renderer.
focus_target_id: str | None = None
home_house_id: str | None = None
build_target_house_id: str | None = None
patrol_index: int = 0
resources_transferred: int = 0
beasts_killed: int = 0
children_count: int = 0
houses_built: int = 0
# Set when this NPC is controlled by an external agent over MCP rather than
# by the built-in deterministic/LLM planner.
is_player: bool = False
player_owner: str | None = None
country_id: str | None = None
special_status: str | None = None
model_profile_id: str | None = None
# When true, the engine does not role-gate this NPC's actions: it may take
# any action regardless of role. Player NPCs set this, but it can also be
# given to special non-player NPCs.
unrestricted_actions: bool = False
# Routes this NPC to a named connector (from secondary_connectors in config).
# None means the default connector handles this NPC.
connector_id: str | None = None
@dataclass(slots=True)
class ResourceNode:
id: str
resource_type: str # "food" | "herbs" | "wood" | "weapon"
position: Vec3
amount: int
max_amount: int = 0 # regrowth cap; 0 means "no regrowth" (set to amount at spawn)
@dataclass(slots=True)
class Beast:
id: str
position: Vec3
health: float = 180.0
damage: float = 12.0
speed: float = 1.0 # in tiles per tick
target_npc_id: str | None = None
target_house_id: str | None = None
retreat_ticks: int = 0
state: str = "hunting" # hunting | attacking | retreating | dead
@dataclass(slots=True)
class House:
id: str
position: Vec3
owner_ids: list[str] = field(default_factory=list)
hp: float = 60.0
max_hp: float = 60.0
state: HouseState = "completed"
build_progress: int = 10
capacity: int = 3
occupant_ids: list[str] = field(default_factory=list)
@dataclass(slots=True)
class TreasuryState:
id: str
position: Vec3
resources: dict[str, int] = field(default_factory=dict)
@dataclass(slots=True)
class CannonState:
id: str
position: Vec3
operator_id: str | None = None
cooldown_until_tick: int = 0
damage: int = 35
radius: int = 5
@dataclass(slots=True)
class CountryState:
id: str
name: str
color: str
badge: str
citizen_ids: list[str] = field(default_factory=list)
ruler_id: str | None = None
policy: str = "Gather resources, protect citizens, and avoid reckless war."
treasury: TreasuryState | None = None
cannon: CannonState | None = None
next_election_tick: int = 50
@dataclass(slots=True)
class ElectionState:
country_id: str
start_tick: int
end_tick: int
candidate_ids: list[str]
votes: dict[str, str] = field(default_factory=dict)
completed: bool = False
@dataclass(slots=True)
class WorldLogEvent:
tick: int
type: str
summary: str
severity: Literal["good", "neutral", "warning", "danger"] = "neutral"
actor_id: str | None = None
target_id: str | None = None
object_id: str | None = None
@dataclass(slots=True)
class Terrain:
kind: Literal["plain_green"]
width: int
depth: int
color: str = "#43a047"
@dataclass(slots=True)
class WorldEvent:
tick_created: int
kind: str
description: str
position: Vec3 | None
radius: float = 8.0
duration_ticks: int = 5
@dataclass(slots=True)
class WorldState:
tick: int
seed: int
terrain: Terrain
npcs: list[Npc]
last_tick_source: str = "initial"
last_action_debug: list[dict[str, Any]] = field(default_factory=list)
resource_nodes: list[ResourceNode] = field(default_factory=list)
beasts: list[Beast] = field(default_factory=list)
houses: list[House] = field(default_factory=list)
active_events: list[WorldEvent] = field(default_factory=list)
event_log: list[WorldLogEvent] = field(default_factory=list)
countries: list[CountryState] = field(default_factory=list)
elections: list[ElectionState] = field(default_factory=list)
game_status: GameStatus = "running"
population: int = 0
population_cap: int = 12
total_births: int = 0
deaths_by_cause: dict[str, int] = field(default_factory=dict)
houses_built: int = 0
beasts_killed: int = 0
peak_population: int = 0
overseer_score: int = 0
chaos_score: int = 0
chaos_intervention_until: int = -1
famine_until: int = -1
famine_saved_max: dict[str, int] = field(default_factory=dict)
overseer_mode: Literal["off", "advisor", "autopilot"] = "off"
overseer_cycle_ticks: int = 8
overseer_last_tick: int | None = None
overseer_last_thoughts: str | None = None
overseer_priority_alert: str | None = None
overseer_last_directives: list[dict[str, Any]] = field(default_factory=list)
overseer_status: str = "idle"
def living_npcs(self) -> list[Npc]:
return [npc for npc in self.npcs if npc.health > 0]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def to_json(self, *, indent: int | None = None) -> str:
return json.dumps(self.to_dict(), indent=indent, sort_keys=True)