| """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 |
|
|
|
|
| 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 |
| tool_calls: list[dict] = field(default_factory=list) |
| 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 = "" |
| 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: |
| |
| 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, |
| } |
|
|