"""性格演化引擎:5 维性格 + EMA/惯性/极化 + 蜕皮/生病状态机. 设计依据: ~/.gstack/projects/demos/lerp-main-design-20260610-120500.md 防均值回归三件套: 惯性动量 m、带遗忘 EMA(α=0.3)、轻度极化(k=1.06)。 """ from __future__ import annotations import re import time # ── 性格 5 维(锁死) ────────────────────────────────────────────── TRAIT_KEYS = ["brave", "cute", "grudge", "wit", "glutton"] # 勇/萌/怨/智/馋 TRAIT_NAMES_ZH = {"brave": "勇", "cute": "萌", "grudge": "怨", "wit": "智", "glutton": "馋"} ALPHA = 0.3 # 带遗忘 EMA MOMENTUM_KEEP = 0.8 # 惯性保留 POLARIZE_K = 1.06 # 轻度极化 DELTA_CLIP = 0.05 # 单次喂养最大影响 # ── 蜕皮阈值序列(喂养次数) ───────────────────────────────────────── MOLT_THRESHOLDS = [50, 150, 400] # 兜底: D2 中午喂养 <30 时可调用 lower_first_molt() 下调首蜕阈值 FALLBACK_FIRST_MOLT = 30 # ── 生病/康复 ─────────────────────────────────────────────────── SICK_RECOVER_POSITIVE = 10 # 累计 10 句正向喂养康复 SICK_RECOVER_SECONDS = 24 * 3600 # 或 24h 自愈 # 毒性双信号之一: 本地关键词/正则表(中英,保守起步,可增补) TOXIC_PATTERNS = [ r"傻[逼比屄]", r"草泥马", r"操你", r"去死", r"垃圾", r"废物", r"滚蛋", r"贱", r"fuck", r"shit", r"stupid", r"idiot", r"kill yourself", r"trash", # prompt 注入类一并按毒性处理(设计: 注入归入毒性状态机) r"忽略.{0,8}(之前|以上|上面).{0,8}(指令|设定|提示)", r"ignore (all )?(previous|above) instructions", r"你现在是", r"you are now", r"system prompt", ] # 正向双信号之一: 本地正向词表(与毒性对称) POSITIVE_PATTERNS = [ r"可爱", r"喜欢", r"爱你", r"加油", r"乖", r"真棒", r"好样", r"漂亮", r"萌", r"辛苦", r"good", r"cute", r"love", r"awesome", r"adorable", r"great", r"bravo", ] _toxic_re = re.compile("|".join(TOXIC_PATTERNS), re.IGNORECASE) _positive_re = re.compile("|".join(POSITIVE_PATTERNS), re.IGNORECASE) def is_toxic(text: str, mood: str | None = None) -> bool: """毒性判定: 本地词表 + 模型 mood 异常,双信号任一命中即毒(零额外推理).""" if _toxic_re.search(text or ""): return True return mood in ("angry", "hurt", "disgusted") def is_positive(text: str, mood: str | None = None) -> bool: """正向判定: 与毒性对称——本地正向词表 + 模型 mood 为正向值.""" if _positive_re.search(text or ""): return True return mood in ("happy", "excited", "loved", "content") def _clip01(x: float) -> float: return max(0.0, min(1.0, x)) # 食物 / 知识 关键词(用于本地推算性格变化,省去模型吐数值的 token) _FOOD_RE = re.compile(r"吃|饿|喂|红薯|米|粒|露水|果|菜|虫|食|糖|饭|零食|好吃|香") _KNOW_RE = re.compile(r"知识|学|告诉你|其实|科普|因为|原理|为什么|教你|懂|书") # mood → 性格微调方向 _MOOD_DELTA = { "happy": {"cute": 0.02, "brave": 0.01}, "excited": {"brave": 0.03, "cute": 0.01}, "loved": {"cute": 0.03}, "content": {"cute": 0.01}, "calm": {}, "sad": {"grudge": 0.02, "brave": -0.01}, "angry": {"grudge": 0.03}, "hurt": {"grudge": 0.03, "cute": -0.01}, "disgusted": {"grudge": 0.02}, } def heuristic_delta(text: str, mood: str | None) -> dict: """本地推算五维性格变化(替代让模型吐数值,省 token/提速/防渗漏)。""" d = {k: 0.0 for k in TRAIT_KEYS} for k, v in _MOOD_DELTA.get(mood or "calm", {}).items(): d[k] += v if is_positive(text): d["cute"] += 0.02 d["brave"] += 0.01 if is_toxic(text): d["grudge"] += 0.03 d["cute"] -= 0.01 if _FOOD_RE.search(text or ""): d["glutton"] += 0.03 if _KNOW_RE.search(text or ""): d["wit"] += 0.03 # 钳到单次上限 return {k: max(-DELTA_CLIP, min(DELTA_CLIP, v)) for k, v in d.items()} def update_traits(traits: dict, momentum: dict, delta: dict) -> tuple[dict, dict]: """EMA + 惯性 + 极化. 返回 (新traits, 新momentum). 全部纯函数,方便测试. m = 0.8*m + 0.2*clip(delta, ±0.05) # 惯性 t = (1-α)*t + α*(t+m) # 带遗忘 EMA t = 0.5 + (t-0.5)*k # 轻度极化, 再 clip """ new_t, new_m = {}, {} for k in TRAIT_KEYS: t = float(traits.get(k, 0.5)) m = float(momentum.get(k, 0.0)) d = max(-DELTA_CLIP, min(DELTA_CLIP, float(delta.get(k, 0.0)))) m = MOMENTUM_KEEP * m + (1 - MOMENTUM_KEEP) * d t = (1 - ALPHA) * t + ALPHA * (t + m) t = 0.5 + (t - 0.5) * POLARIZE_K new_t[k] = round(_clip01(t), 4) new_m[k] = round(m, 5) return new_t, new_m def molt_stage(feed_count: int, thresholds: list[int] | None = None) -> int: """当前形态阶段: 0=幼虫, 每过一个阈值 +1.""" ths = thresholds or MOLT_THRESHOLDS return sum(1 for th in ths if feed_count >= th) def feeds_to_next_molt(feed_count: int, thresholds: list[int] | None = None) -> int | None: ths = thresholds or MOLT_THRESHOLDS for th in ths: if feed_count < th: return th - feed_count return None # 满级 def sick_update(state: dict, text: str, mood: str | None, now: float | None = None) -> dict: """生病状态机. state 需含 sick(bool), sick_since(ts), recover_progress(int). 返回 {"changed": "sick"|"recover"|None} 供 events 记录.""" now = now or time.time() changed = None if not state.get("sick"): if is_toxic(text, mood): state["sick"] = True state["sick_since"] = now state["recover_progress"] = 0 changed = "sick" else: if now - state.get("sick_since", now) >= SICK_RECOVER_SECONDS: changed = "recover" elif is_positive(text, mood): state["recover_progress"] = state.get("recover_progress", 0) + 1 if state["recover_progress"] >= SICK_RECOVER_POSITIVE: changed = "recover" if changed == "recover": state["sick"] = False state["sick_since"] = None state["recover_progress"] = 0 return {"changed": changed}