OFFICIAL SOURCE ANALYSIS

Buddy 电子宠物系统

18 个物种、5 级稀有度、RPG 属性、Mulberry32 确定性生成、ASCII 动画系统的源码级完整拆解。

Buddy 电子宠物系统 — 源码级完整拆解

在 Claude Code 的源码里,藏着一个完整的电子宠物系统。一个坐在终端输入框旁边的 ASCII 小动物,会对你的对话做出反应,偶尔冒出气泡说两句。有 18 个物种、5 级稀有度、RPG 属性、帽子系统,还能撸——撸了会飘爱心。它叫 Buddy,计划在 2026 年 4 月 1 日作为愚人节彩蛋上线。

整个系统由 src/buddy/ 目录下的 6 个文件组成,总代码量并不大,但设计精巧得令人惊叹。这里是对每一个细节的完整拆解。

整体架构:五层分工

文件职责关键机制
types.ts数据结构与常量18 物种、5 稀有度、6 眼睛、8 帽子、5 属性的完整定义
companion.ts确定性生成算法Mulberry32 PRNG + FNV-1a 哈希 + 骨架/灵魂分离
sprites.tsASCII 渲染引擎18×3=54 帧精灵画、帽子叠加、眼睛替换
CompanionSprite.tsxReact 动画组件500ms tick、15 帧空闲序列、气泡、爱心
prompt.tsAI 集成「礼让协议」——告诉主 AI 不要扮演宠物
useBuddyNotification.tsx发现设计4 月 1-7 日彩虹提示、/buddy 触发检测

核心设计思路:宠物的外观由你的用户 ID 决定(确定性生成),性格由 AI 取名(LLM 生成)。前者不存储,后者永久保存。

18 个物种

每种动物是一组 5 行高 × 12 字符宽的 ASCII 画,带 3 帧动画(静止、抖动 1、抖动 2)。完整列表:

动物列表

Duck(鸭子)、Goose(鹅)、Blob(果冻怪)、Cat(猫)、Dragon(龙)、Octopus(章鱼)、Owl(猫头鹰)、Penguin(企鹅)、Turtle(乌龟)、Snail(蜗牛)、Ghost(幽灵)、Axolotl(六角恐龙)、Capybara(水豚)、Cactus(仙人掌)、Robot(机器人)、Rabbit(兔子)、Mushroom(蘑菇)、Chonk(胖橘)

Face 表情(窄终端时显示)

Duck: (°> · Cat: =°ω°= · Dragon: <°~°>
Octopus: ~(°°)~ · Ghost: /°°\ · Robot: [°°]
Capybara: (°oo°) · Axolotl: }°.°{ · Chonk: (°.°)

为什么物种名要加密?所有 18 个物种名都不是直接写的字符串,而是用 String.fromCharCode() 编码。比如 capybara 写成 c(0x63,0x61,0x70,0x79,0x62,0x61,0x72,0x61)。原因是 capybara 是 Anthropic 内部的模型代号之一,直接写在代码里会触发构建时的安全扫描。为了统一风格,所有 18 个物种名都用了这种方式。写代码写到要给动物名字加密,也是头一回见。

五级稀有度系统

稀有度概率星级颜色属性下限帽子
Common(普通)60%灰色 (inactive)5
Uncommon(稀有)25%★★绿色 (success)15随机
Rare(珍稀)10%★★★蓝色 (permission)25随机
Epic(史诗)4%★★★★青色 (autoAccept)35随机
Legendary(传说)1%★★★★★橙色 (warning)50随机

稀有度通过权重轮盘算法决定:把所有权重加起来(60+25+10+4+1=100),生成一个 0-100 的随机数,依次减去每级的权重,第一个使结果变负的等级就是你的稀有度。

还有一个隐藏属性——Shiny(闪光),所有稀有度均为 1% 概率触发。Shiny + Legendary = 0.01%,万分之一的终极稀有组合。

核心设计:Bones/Soul 分离

这是整个系统最精巧的架构决策。一个宠物由两部分组成:

CompanionBones(骨架)— 确定性,不存储
  • rarity — 稀有度
  • species — 物种
  • eye — 眼睛样式(6 种:·, ✦, ×, ◉, @, °)
  • hat — 帽子(8 种:none, crown, tophat, propeller, halo, wizard, beanie, tinyduck)
  • shiny — 是否闪光
  • stats — 5 项 RPG 属性

hash(userId + salt) 确定性生成。每次启动 Claude Code 都重新算一遍,永远不写入磁盘。

CompanionSoul(灵魂)— LLM 生成,永久存储
  • name — 名字(如 "Noodle")
  • personality — 性格描述
  • hatchedAt — 孵化时间戳

孵化时由 LLM 一次性生成,永久存储在 ~/.claude/config.json 中。

为什么 Bones 不存储?三个原因:
1. 防作弊 — 你没法通过编辑配置文件把自己的宠物改成 Legendary,因为稀有度根本不存在于配置文件里。
2. 防损坏 — 开发者可以安全地重命名物种或调整数组顺序,不会破坏已有宠物。
3. 简化存储 — 只需要存两个字符串(名字和性格),其余全部按需重算。

确定性生成算法

整个生成过程基于一条链:userId哈希种子PRNG所有属性

Hash 函数

Salt 值

'friend-2026-401' — 拼接在 userId 后面再哈希,防止与其他系统的哈希结果碰撞。

Mulberry32 PRNG

源码注释写道:"tiny seeded PRNG, good enough for picking ducks"(微型种子 PRNG,选鸭子够用了)。魔法常数 0x6d2b79f5,输出一个确定性的 [0, 1) 浮点数。同一个 seed 永远产生同一个序列。

完整的 roll() 流程

roll(userId): 1. seed = hash(userId + 'friend-2026-401') 2. rng = Mulberry32(seed) 3. rarity = rollRarity(rng) // 权重轮盘: 60/25/10/4/1 4. stats = rollStats(rng, rarity) // 5 项属性 5. species = pick(SPECIES, rng) // 18 选 1 6. eye = pick(EYES, rng) // 6 选 1 7. hat = pick(HATS, rng) // 8 选 1(Common 强制 none) 8. shiny = rng() < 0.01 // 1% 闪光概率 9. inspirationSeed = floor(rng() × 1e9) // 灵魂生成用 → return { bones, inspirationSeed }

缓存策略:单条目缓存 rollCache = { key, value }。因为这个函数会从 3 个热路径调用(500ms tick、按键、turn observer),但 userId 在会话内不变,所以只缓存一个条目就够了。

RPG 属性系统

每只宠物有 5 项属性,取名很有 Anthropic 的味道:

属性含义范围
DEBUGGING调试能力1-100
PATIENCE耐心1-100
CHAOS混乱值1-100
WISDOM智慧1-100
SNARK毒舌值1-100

属性生成机制

每只宠物有一个峰值属性(最高值)和一个垃圾属性(最低值),其余三个为散布属性(中间值)。稀有度决定所有属性的下限(floor):

对于每个属性: 峰值 = min(100, floor + 50 + random(0-30)) // 通常 55-100 垃圾 = max(1, floor - 10 + random(0-15)) // 最低可到 1 散布 = floor + random(0-40) // 中间范围 举例: Legendary (floor=50): 峰值 ~80-100, 垃圾 ~40-55, 散布 ~50-90 Common (floor=5): 峰值 ~55-85, 垃圾 ~1-10, 散布 ~5-45

视觉系统

8 种帽子的 ASCII 图案

crown: \^^^/ 皇冠 tophat: [___] 礼帽 propeller: -+- 螺旋桨 halo: ( ) 光环 wizard: /^\ 巫师帽 beanie: (___) 毛线帽 tinyduck: ,> 头顶小鸭子 none: (空) 无帽子(Common 专属)

帽子叠加在精灵的第 0 行(最顶部)。只有当该行为空白时才会叠加——如果精灵自带烟雾或触角等顶部装饰,帽子不会覆盖。如果所有 3 帧的第 0 行都是空白且没有帽子,该行会被裁掉(防止高度在动画中跳动)。

眼睛系统

6 种眼睛样式:· × @ °。精灵模板中使用 {E} 占位符,渲染时全部替换为抽到的眼睛字符。

动画系统

空闲动画循环

IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0] ↑静止 ↑静止 ↑抖1 ↑静止 ↑眨眼 ↑静止 ↑抖2 ↑静止 总长: 15 帧 × 500ms = 7.5 秒一个完整周期 帧 -1 的含义: 用帧 0 但把所有眼睛字符替换为 '-'(闭眼)

兴奋模式

当宠物在说话(有气泡)或被撸(/buddy pet)时,切换到快速动画模式——用 tick % frameCount 循环所有帧,产生快速抖动效果。

/buddy pet 爱心飘升

PET_HEARTS(5 帧 × 500ms = 2.5 秒): 帧 0: ♥ ♥ // 两颗心分开 帧 1: ♥ ♥ ♥ // 三颗心聚拢 帧 2: ♥ ♥ ♥ // 上升 帧 3: ♥ ♥ ♥ // 扩散 帧 4: · · · // 消散为小点 颜色: cyan (autoAccept) 位置: 叠加在精灵头顶

气泡与交互系统

参数说明
TICK_MS500ms基础动画间隔
BUBBLE_SHOW20 ticks = 10 秒气泡显示时长
FADE_WINDOW6 ticks = 3 秒气泡淡出时长
PET_BURST_MS2500ms爱心飘升时长

终端响应式布局

companionReservedColumns() 函数计算精灵+气泡需要的列宽,PromptInput 据此调整输入框的换行位置。如果 Buddy 被静音、未孵化、或终端太窄,返回 0。

渐进式发现设计

4 月 1 日零点(你的本地时间),Claude Code 启动时会在状态栏闪现一个 15 秒的彩虹色 /buddy 提示。如果你好奇地输入了,就会进入孵化流程。4 月 7 日之后提示消失,但 /buddy 命令永久可用。

为什么用本地时区而非 UTC?源码注释解释:"24h rolling wave across timezones. Sustained Twitter buzz instead of a single UTC-midnight spike, gentler on soul-gen load." 不同时区的用户在各自的 4 月 1 日发现彩蛋,社交媒体讨论会持续 24 小时而非集中在 UTC 午夜。同时平滑了 LLM 生成宠物性格的 API 调用负载。一个愚人节彩蛋,连流量分布都想好了。

AI 的「礼让协议」

宠物和主 AI 是独立的两个实体。源码中注入给主 AI 的 prompt:

"A small [SPECIES] named [NAME] sits beside the user's input box and occasionally comments in a speech bubble. You're not [NAME] — it's a separate watcher.

When the user addresses [NAME] directly (by name), its bubble will answer. Your job in that moment is to stay out of the way: respond in ONE line or less, or just answer any part of the message meant for you. Don't explain that you're not [NAME] — they know. Don't narrate what [NAME] might say — the bubble handles that."

这种「AI 之间的礼让协议」在 prompt 工程里可能是第一次见。只在第一次会话时注入(通过检查消息历史中是否已有 companion_intro 类型的 attachment),避免重复。

设计亮点总结

决策机制效果
确定性 + 不可伪造userId 哈希决定一切,配置文件里找不到可以改的数据同一用户永远同一只宠物,且无法作弊
骨架/灵魂分离计算的归计算,创造的归创造代码可以安全更新物种列表而不破坏存量宠物
渐进式发现彩虹提示 → 好奇输入 → 孵化惊喜不强制不打扰,让用户自己发现彩蛋
不干扰工作气泡 10 秒消失,可静音,窄终端自动简化工具属性优先,宠物永远不会挡住工作
跨时区滚动发布本地时区判定,24 小时滚动波社交传播 + 负载平滑一箭双雕
防安全扫描编码String.fromCharCode 编码物种名绕过 Anthropic 内部的代号扫描规则