cricket-commons / traits.py
lerp666's picture
Upload traits.py with huggingface_hub
24b35ce verified
Raw
History Blame Contribute Delete
6.58 kB
"""性格演化引擎: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}