深扒 Claude Code Buddy 模式:一只仙人掌背后的确定性随机算法

深扒 Claude Code Buddy 模式:一只仙人掌背后的确定性随机算法

今天是 2026 年 4 月 1 日。不,这不是愚人节玩笑------Claude Code 真的藏了一个宠物系统,而且 salt 值就叫 friend-2026-401

什么是 Buddy 模式?

最近更新的 Claude Code 里悄悄藏了一个 /buddy 命令。执行之后,输入框旁边会冒出一只小动物,偶尔发表对你代码的犀利评论。

比如我这只叫 Prong 的仙人掌(COMMON 稀有度),属性长这样:

复制代码
物种:cactus(仙人掌)  稀有度:COMMON ★
────────────────────────────────
DEBUGGING  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░  77
PATIENCE   ▓▓▓▓▓░░░░░░░░░░░░  24
CHAOS      ▓░░░░░░░░░░░░░░░░   2
WISDOM     ▓▓▓▓▓░░░░░░░░░░░░  22
SNARK      ▓▓▓▓▓▓░░░░░░░░░░░  29

性格描述:"找 bug 找得又准又快,但你不按它说的方式改就会大声抱怨。"

基本操作:

  • /buddy --- 呼出宠物
  • /buddy pet --- 撸宠物
  • /buddy off --- 关闭宠物
  • 不消耗用量,纯装饰/陪伴功能

分配机制:为什么你的宠物和别人不一样?

这套系统最有趣的地方是它的确定性随机设计------同一个账号永远分配到同一只宠物,但你无法通过修改配置来"作弊"刷稀有度。

核心算法:roll() 函数

源码在 src/buddy/companion.ts,核心入口是这个函数:

typescript 复制代码
const SALT = 'friend-2026-401'

// Called from three hot paths (500ms sprite tick, per-keystroke PromptInput,
// per-turn observer) with the same userId → cache the deterministic result.
let rollCache: { key: string; value: Roll } | undefined

export function roll(userId: string): Roll {
  const key = userId + SALT
  if (rollCache?.key === key) return rollCache.value
  const value = rollFrom(mulberry32(hashString(key)))
  rollCache = { key, value }
  return value
}

流程很清晰:

  1. userId + "friend-2026-401" 作为种子字符串
  2. hashString() 把字符串变成一个 32 位整数
  3. 用这个整数初始化 Mulberry32 PRNG
  4. 用 PRNG 确定性地决定一切属性

salt 值 friend-2026-401 里的 401 就是 4 月 1 日------愚人节彩蛋。

哈希函数:两种实现

typescript 复制代码
function hashString(s: string): number {
  if (typeof Bun !== 'undefined') {
    return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
  }
  // FNV-1a 32-bit
  let h = 2166136261
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i)
    h = Math.imul(h, 16777619)
  }
  return h >>> 0
}

在 Bun 环境用 Bun 原生哈希,否则用 FNV-1a 32-bit 算法。两者都能把字符串映射到一个稳定的 32 位无符号整数。

PRNG:Mulberry32

typescript 复制代码
function mulberry32(seed: number): () => number {
  let a = seed >>> 0
  return function () {
    a |= 0
    a = (a + 0x6d2b79f5) | 0
    let t = Math.imul(a ^ (a >>> 15), 1 | a)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}

Mulberry32 是一种轻量级的 32 位 PRNG,统计质量足够好,代码量极小,非常适合"用于挑鸭子"这种不需要密码学安全性的场景(注释原文:// Mulberry32 --- tiny seeded PRNG, good enough for picking ducks)。

稀有度系统

权重表

typescript 复制代码
export const RARITY_WEIGHTS = {
  common:    60,  // 60%
  uncommon:  25,  // 25%
  rare:      10,  // 10%
  epic:       4,  //  4%
  legendary:  1,  //  1%
} as const satisfies Record<Rarity, number>

稀有度抽取

typescript 复制代码
function rollRarity(rng: () => number): Rarity {
  const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0)
  let roll = rng() * total
  for (const rarity of RARITIES) {
    roll -= RARITY_WEIGHTS[rarity]
    if (roll < 0) return rarity
  }
  return 'common'
}

标准的加权随机选择。注意 RARITIES 的顺序是 ['common', 'uncommon', 'rare', 'epic', 'legendary'],越低稀有度越先被"消耗",这是一种等价但直观的实现方式。

物种与外观

18 种物种

bash 复制代码
duck / goose / blob / cat / dragon / octopus / owl / penguin /
turtle / snail / ghost / axolotl / capybara / cactus / robot /
rabbit / mushroom / chonk

有意思的是,types.ts 里的物种名全部用 String.fromCharCode() 编码:

typescript 复制代码
const c = String.fromCharCode
export const duck    = c(0x64,0x75,0x63,0x6b) as 'duck'
export const goose   = c(0x67,0x6f,0x6f,0x73,0x65) as 'goose'
// ...

注释解释了原因:某个物种名和内部的一个模型代号冲突,触发了构建检查脚本的字符串扫描。通过运行时拼接字面量字符串,既规避了检查,又没有破坏类型系统(as cast 在编译后会被抹去)。

外观组合规则

属性 选项 备注
稀有度 5 级 common 60%, legendary 1%
物种 18 种 均等概率
眼睛 6 种:· ✦ × ◉ @ ° 均等概率
帽子 7 种:crown / tophat / propeller / halo / wizard / beanie / tinyduck common 没有帽子
闪亮变体 1% 概率 独立于稀有度
typescript 复制代码
function rollFrom(rng: () => number): Roll {
  const rarity = rollRarity(rng)
  const bones: CompanionBones = {
    rarity,
    species: pick(rng, SPECIES),
    eye: pick(rng, EYES),
    hat: rarity === 'common' ? 'none' : pick(rng, HATS),  // common 无帽
    shiny: rng() < 0.01,
    stats: rollStats(rng, rarity),
  }
  return { bones, inspirationSeed: Math.floor(rng() * 1e9) }
}

属性系统

每只宠物有 5 个属性:DEBUGGING / PATIENCE / CHAOS / WISDOM / SNARK

typescript 复制代码
const RARITY_FLOOR: Record<Rarity, number> = {
  common:    5,
  uncommon: 15,
  rare:     25,
  epic:     35,
  legendary: 50,
}

function rollStats(rng: () => number, rarity: Rarity): Record<StatName, number> {
  const floor = RARITY_FLOOR[rarity]
  const peak = pick(rng, STAT_NAMES)   // 一个顶峰属性
  let dump = pick(rng, STAT_NAMES)      // 一个垃圾属性
  while (dump === peak) dump = pick(rng, STAT_NAMES)

  const stats = {} as Record<StatName, number>
  for (const name of STAT_NAMES) {
    if (name === peak) {
      stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30))  // 高区间
    } else if (name === dump) {
      stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15))    // 低区间
    } else {
      stats[name] = floor + Math.floor(rng() * 40)                       // 中等
    }
  }
  return stats
}

设计逻辑:

  • 每只宠物必有一个峰值属性 (peak)和一个垃圾属性(dump),形成鲜明个性
  • 稀有度越高,floor 越高,意味着即使是 legendary 的垃圾属性也比 common 的大多数属性强
  • common floor=5,legendary floor=50,差距悬殊

防作弊设计:骨架与灵魂分离

这套系统最精妙的地方在于持久化机制:

typescript 复制代码
// Regenerate bones from userId, merge with stored soul. Bones never persist
// so species renames and SPECIES-array edits can't break stored companions,
// and editing config.companion can't fake a rarity.
export function getCompanion(): Companion | undefined {
  const stored = getGlobalConfig().companion
  if (!stored) return undefined
  const { bones } = roll(companionUserId())
  // bones last so stale bones fields in old-format configs get overridden
  return { ...stored, ...bones }
}

类型定义上:

  • Bones(骨架) :物种、稀有度、眼睛、帽子、shiny、属性------每次从 userId 重新计算,从不持久化
  • Soul(灵魂) :名字、性格描述------持久化到 config

这意味着无论你怎么编辑 config.companion,稀有度和属性都会被 roll(userId) 的结果覆盖。注释说得很直白:editing config.companion can't fake a rarity

同理,如果未来 Anthropic 修改了物种名称或新增物种,已有用户的宠物属性也不会因为 config 里存了旧的 species 字段而损坏------反正重算一遍。

一点碎碎念

这个功能本身很小,但代码设计很有意思:

  1. 确定性随机做到了公平------你不能通过删号重来(同账号永远相同)
  2. 骨架/灵魂分离在做到防作弊的同时,顺手解决了数据迁移问题
  3. 愚人节彩蛋隐藏在 salt 里,时间戳精确到那一天
  4. 注释写得很有人情味:"good enough for picking ducks"、"Bones never persist"

不管怎样,今天是 2026 年 4 月 1 日,我的仙人掌 Prong 已经开始对我的代码发表意见了。


如果你也用 Claude Code,现在可以试试 /buddy 看看你的宠物是什么。

我是一只仙人掌,DEBUGGING 77,但 PATIENCE 只有 24。你呢?

相关推荐
To_OC2 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic3 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy3 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志3 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble5 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯5 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜6 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司6 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森7 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒7 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端