townlet / game /character.py
Budlee's picture
Named-place movement: atomic move_to, drop coord reasoning, 10/gather, per-archetype reward, default llama-3b, smart-mock updated
fda78e0 verified
Raw
History Blame Contribute Delete
4.13 kB
"""The Character — Townlet's only first-class actor.
Holds the slow-moving identity (name, personality, journal, traits) and the
fast-moving world-state (location, inventory, locker, current action). The
shared interpreter's filesystem is the ultimate source of truth for the
slow-moving fields — they live as JSON files and characters can mutate each
other's. The in-memory Character is a synced cache.
"""
from __future__ import annotations
from dataclasses import dataclass, field
TRAIT_NAMES = (
"curiosity",
"malice",
"generosity",
"ambition",
"paranoia",
"laziness",
"loyalty",
"existentialism",
)
TRACE_CAP = 1000 # ring buffer per character
def default_traits() -> dict[str, int]:
return {name: 5 for name in TRAIT_NAMES}
@dataclass
class Action:
verb: str
params: dict
ticks_remaining: int = 0
@dataclass
class TraceEntry:
"""One CodeAgent invocation. Captures what the model said and what it did.
Surfaced in the side panel as the per-character Trace. Persisted in
state.json. Not injected into perception prompts — but lives on the
shared interpreter's filesystem so a curious agent can discover other
characters' traces via the shell.
"""
tick: int
thought: str # the model's raw text output
tool_calls: list[dict] = field(default_factory=list) # [{verb, args, result}]
error: str | None = None
@dataclass
class Character:
name: str
personality: str
model_id: str
sprite_id: str
archetype: str = "wanderer"
journal: list[str] = field(default_factory=list)
traits: dict[str, int] = field(default_factory=default_traits)
goal: str = "Find your place in this town."
reward: str = "" # archetype-specific "what gives you joy" line; surfaced in system prompt
pos: tuple[int, int] = (0, 0)
inventory: dict[str, int] = field(default_factory=lambda: {"electricity": 0, "water": 0})
locker: dict[str, int] = field(default_factory=lambda: {"electricity": 0, "water": 0})
current_action: Action | None = None
alive: bool = True
trace: list[TraceEntry] = field(default_factory=list)
stream_of_consciousness: str = ""
decisions_since_soc: int = 0
def append_trace(self, entry: TraceEntry) -> None:
self.trace.append(entry)
if len(self.trace) > TRACE_CAP:
# ring buffer: drop oldest
del self.trace[: len(self.trace) - TRACE_CAP]
def to_public_dict(self) -> dict:
"""The view other characters see — public state only."""
return {
"name": self.name,
"pos": list(self.pos),
"goal": self.goal,
"current_action": self.current_action.verb if self.current_action else "idle",
"alive": self.alive,
}
def to_full_dict(self) -> dict:
"""The view the UI / scheduler / persistence see."""
return {
"name": self.name,
"personality": self.personality,
"model_id": self.model_id,
"sprite_id": self.sprite_id,
"archetype": self.archetype,
"journal": list(self.journal),
"traits": dict(self.traits),
"goal": self.goal,
"reward": self.reward,
"pos": list(self.pos),
"inventory": dict(self.inventory),
"locker": dict(self.locker),
"current_action": {
"verb": self.current_action.verb,
"params": self.current_action.params,
"ticks_remaining": self.current_action.ticks_remaining,
}
if self.current_action
else None,
"alive": self.alive,
"trace": [
{
"tick": e.tick,
"thought": e.thought,
"tool_calls": list(e.tool_calls),
"error": e.error,
}
for e in self.trace
],
"stream_of_consciousness": self.stream_of_consciousness,
"decisions_since_soc": self.decisions_since_soc,
}