maindlock / src /mindlock /character.py
arbios's picture
Update: forgotten/epitaph/conviction, instant demo, VoxCPM2 voices, docs, card
9a41b58 verified
Raw
History Blame Contribute Delete
3.91 kB
"""A character: one fixed brain architecture + a unique biography and state.
The thesis of Mindlock is that *same architecture + different experience -> different
behavior*. So the six-region cascade is shared; everything that makes a character who
they are lives here β€” chiefly the biography (hippocampus content) and their fear.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
@dataclass
class Character:
name: str
persona: str
biography: str
voice: str
fear: str
key_holder: bool
key_location: str
life_max: int = 1000 # "a thousand tokens to think with" β€” the track name, literally
arousal: float = 4.0
# --- world/story fields (optional; absent in the slice character) ---
title: str = ""
gender: str = "" # "male"/"female" β€” so a generated character maps to a matching sprite
portrait: str = "" # path (relative to project root) to the portrait image
sprite_key: str = "" # roster slug β†’ the client loads /static/sprites/npc/{slug}/ directly
secrets: list = field(default_factory=list) # staged disclosures (Design v2)
key_condition: str = "" # (legacy) superseded by key_approach
key_approach: list = field(default_factory=list) # words that ARE the earned approach to the key
goal: str = "the key" # what the holder withholds (procedural goals: 'the car keys', …)
reveals: str = "" # (legacy, unused) one-shot hint
reveals_about: str = "" # (legacy, unused)
needs_reputation: int | None = None # won't engage if world reputation is below this
known_people: list = field(default_factory=list) # names from their life (lie-catching: "I'm Mara")
scripted: list = field(default_factory=list) # fixed beats instead of a brain β€” a scene, not a mind
scripted_idx: int = 0 # runtime: which beat plays next
yield_line: str = "" # canonical story beat spoken verbatim when the key is given
# --- runtime state (not in the JSON) ---
life_tokens: int | None = None
rapport: float = 0.0 # accumulated relationship 0..10 (Design v2)
history: list = field(default_factory=list) # recent (player, reply) exchanges
peers: list = field(default_factory=list) # other characters in the room β€” for lie-catching
relations: dict = field(default_factory=dict) # name -> tie ("cousin"); so a holder recalls a peer
trust: float = 0.0
decision: str = "REFUSE"
alive: bool = True
gave_key: bool = False # sticky: set once the key-holder yields (Design v2)
revealed: bool = False
approach_landed: bool = False # the soft-spot word resonates fully only ONCE
fear_pressure: int = 0 # consecutive hostile turns; sustained terror can break a holder
forgotten: list = field(default_factory=list) # memories burned away with life β€” never return
def __post_init__(self) -> None:
if self.life_tokens is None:
self.life_tokens = self.life_max
self._arousal0 = self.arousal # remembered so reset()/reputation can restore it
for s in self.secrets:
s.setdefault("told", False)
@classmethod
def load(cls, path: str) -> "Character":
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
return cls(**data)
def reset(self) -> None:
self.life_tokens = self.life_max
self.arousal = self._arousal0
self.rapport = 0.0
self.history = []
for s in self.secrets:
s["told"] = False
self.trust = 0.0
self.decision = "REFUSE"
self.alive = True
self.gave_key = False
self.revealed = False
self.approach_landed = False
self.fear_pressure = 0
self.scripted_idx = 0
self.forgotten = []