docker000 / game /sim /relationship.py
seachen's picture
Deploy Quiet Town Corner Shop Docker demo (Gradio + Python sim) (part 5)
3831e97 verified
Raw
History Blame Contribute Delete
4.37 kB
"""Relationship affinity / stage / memory (R6)."""
from __future__ import annotations
from typing import Dict, List, Optional
STAGE_ORDER = ["STRANGER", "ACQUAINTANCE", "FAMILIAR", "TRUSTED", "BOND"]
class RelationshipStore:
def __init__(self, data: Optional[Dict[str, dict]] = None, rules: Optional[dict] = None):
self.data: Dict[str, dict] = {}
self.rules = rules or {}
for npc_id, raw in (data or {}).items():
self.data[npc_id] = {
"affinity": int(raw.get("affinity", 0)),
"visit_count": int(raw.get("visit_count", 0)),
"stage": raw.get("stage", "STRANGER"),
"memories": list(raw.get("memories") or []),
"deep_chats_done": list(raw.get("deep_chats_done") or []),
}
self.recompute_stage(npc_id, day_index=0)
@classmethod
def from_state(cls, state: dict, rules: Optional[dict] = None) -> "RelationshipStore":
return cls(state.get("relationships") or {}, rules=rules)
def ensure(self, npc_id: str) -> dict:
if npc_id not in self.data:
self.data[npc_id] = {
"affinity": 0,
"visit_count": 0,
"stage": "STRANGER",
"memories": [],
"deep_chats_done": [],
}
return self.data[npc_id]
def apply_visit(self, npc_id: str, day_index: int) -> None:
r = self.ensure(npc_id)
# +1 once per day tracked via visit_count increment on enter
r["visit_count"] += 1
r["affinity"] = min(100, r["affinity"] + int(self._delta("visit", 1)))
self.recompute_stage(npc_id, day_index)
def apply_purchase(self, npc_id: str, item_match_score: float, day_index: int) -> None:
r = self.ensure(npc_id)
if item_match_score >= 0.5:
d = int(self._delta("buy_good", 3))
else:
d = int(self._delta("buy_weak", 1))
r["affinity"] = min(100, r["affinity"] + d)
self.recompute_stage(npc_id, day_index)
def can_deep_chat(self, npc_id: str) -> bool:
"""Deep chat unlocks at FAMILIAR (affinity ≥ 35) or higher."""
r = self.ensure(npc_id)
stage = r.get("stage") or "STRANGER"
return STAGE_ORDER.index(stage) >= STAGE_ORDER.index("FAMILIAR")
def apply_deep_chat(self, npc_id: str, chat_id: str, kind: bool, day_index: int) -> bool:
r = self.ensure(npc_id)
if not self.can_deep_chat(npc_id):
return False
if chat_id in r["deep_chats_done"]:
return False
r["deep_chats_done"].append(chat_id)
d = int(self._delta("deep_chat_kind" if kind else "deep_chat_neutral", 5 if kind else 1))
r["affinity"] = min(100, r["affinity"] + d)
self.recompute_stage(npc_id, day_index)
return True
def add_memory(self, npc_id: str, memory_id: str, text: str) -> None:
r = self.ensure(npc_id)
if any(m.get("id") == memory_id for m in r["memories"]):
return
r["memories"].append({"id": memory_id, "text": text})
def _delta(self, key: str, default: int) -> int:
return int((self.rules.get("affinity_delta") or {}).get(key, default))
def recompute_stage(self, npc_id: str, day_index: int = 0) -> str:
r = self.ensure(npc_id)
aff = r["affinity"]
visits = r["visit_count"]
chats = len(r["deep_chats_done"])
has_arc = any(m.get("id", "").startswith("arc_") for m in r["memories"])
best = "STRANGER"
# sticky upgrade only
candidates = [
("STRANGER", aff >= 0, True),
("ACQUAINTANCE", aff >= 15, visits >= 2),
("FAMILIAR", aff >= 35, True),
("TRUSTED", aff >= 60, chats >= 1),
("BOND", aff >= 85, day_index >= 5 and has_arc),
]
for name, aff_ok, hard in candidates:
if aff_ok and hard:
best = name
# never downgrade
if STAGE_ORDER.index(best) < STAGE_ORDER.index(r.get("stage", "STRANGER")):
best = r["stage"]
r["stage"] = best
return best
def sync_to_state(self, state: dict) -> None:
state["relationships"] = {k: dict(v, memories=list(v["memories"]), deep_chats_done=list(v["deep_chats_done"])) for k, v in self.data.items()}