我把 Claude Code 里的隐藏彩蛋提取出来了——零依赖的 ASCII 虚拟宠物系统

前言

最近泄漏的 Claude Code(一款 AI 编程工具)源码中有 Buddy 系统,一个虚拟宠物伙伴(Buddy)系统(愚人节彩蛋)。每个用户都会根据自己的 ID 确定性地生成一只独一无二的 ASCII 小宠物,它会在终端里陪你写代码,你还可以摸它的头。

原始实现和 React/Ink、Bun 运行时深度耦合,没法单独拿出来用。于是我把核心逻辑提取出来,重新实现为一个零运行时依赖三端通用 (终端 / 浏览器 / Node.js)的独立包------buddy-sprite

lua 复制代码
    __
  <(· )___
   (  ._>
    `--´
  Quackers ★★★

在线体验:novlan1.github.io/buddy-sprit...

它能做什么?

一句话概括:给你的终端或网页加一只 ASCII 宠物

核心特性:

  • 🎲 确定性生成 :相同的 userId 永远生成相同的宠物,就像区块链地址生成头像一样
  • 🐣 18 种物种:鸭子、猫、龙、幽灵、六角恐龙、机器人、水豚、仙人掌......
  • 🎨 丰富的外观:6 种眼睛样式 × 8 种帽子(皇冠、巫师帽、螺旋桨帽......)
  • 稀有度系统:普通(60%) → 罕见(25%) → 稀有(10%) → 史诗(4%) → 传说(1%)
  • 📊 5 项属性:调试力、耐心、混乱、智慧、毒舌
  • 闪光变体:1% 概率获得闪光宠物
  • 💬 对话气泡:带自动淡出动画
  • 🤚 摸头互动:浮动爱心动画
  • 😴 待机动画:眨眼、小动作、呼吸
  • 📦 零依赖:纯 TypeScript,打包后约 14KB

技术实现

1. 确定性生成:从字符串到宠物

整个生成系统的核心思想是:一个字符串输入,确定性地输出一只完整的宠物

复制代码
userId + salt → FNV-1a 哈希 → Mulberry32 PRNG 种子 → 宠物属性

FNV-1a 哈希

首先用 FNV-1a 算法把字符串转成一个 32 位整数:

ts 复制代码
function hashString(input: string): number {
  let hash = 2166136261
  for (let i = 0; i < input.length; i++) {
    hash ^= input.charCodeAt(i)
    hash = Math.imul(hash, 16777619)
  }
  return hash >>> 0
}

FNV-1a 的优点是实现简单、分布均匀、碰撞率低,非常适合这种场景。

Mulberry32 伪随机数生成器

有了种子之后,用 Mulberry32 算法生成伪随机数序列:

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

Mulberry32 是一个周期为 2³² 的 PRNG,质量远超简单的 LCG,但实现只有几行代码。关键是它是确定性的------相同的种子永远产生相同的随机数序列。

加权稀有度抽取

稀有度的抽取使用加权随机:

ts 复制代码
const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }

function rollRarity(rng: () => number): Rarity {
  const totalWeight = Object.values(RARITY_WEIGHTS).reduce((sum, w) => sum + w, 0)
  let roll = rng() * totalWeight
  for (const rarity of RARITIES) {
    roll -= RARITY_WEIGHTS[rarity]
    if (roll < 0) return rarity
  }
  return 'common'
}

传说级宠物只有 1% 的概率,但如果你的 userId 命中了,那它就是永远的传说。

2. ASCII 精灵帧动画

每种物种都有 3 帧 ASCII 动画,通过模板中的 {E} 占位符来替换不同的眼睛样式:

ts 复制代码
const BODIES = {
  cat: [
    ['            ', '   /\\_/\\    ', '  ( {E}   {E})  ', '  (  ω  )   ', '  (")_(")   '],
    ['            ', '   /\\_/\\    ', '  ( {E}   {E})  ', '  (  ω  )   ', '  (")_(")~  '],
    ['            ', '   /\\-/\\    ', '  ( {E}   {E})  ', '  (  ω  )   ', '  (")_(")   '],
  ],
  // ... 18 种物种
}

帽子作为独立的装饰行叠加在精灵顶部:

ts 复制代码
const HAT_LINES = {
  crown: '   \\^^^/    ',
  wizard: '    /^\\     ',
  halo: '   (   )    ',
  // ...
}

渲染时只需要简单的字符串替换和行拼接,没有任何 Canvas 或 DOM 操作。

3. 动画状态机:BuddyEngine

BuddyEngine 是整个系统的核心,它是一个基于 tick 的动画状态机:

scss 复制代码
┌─────────────┐
│  Idle State  │ ← 默认状态,播放待机序列
│  [0,0,0,0,  ││   1,0,0,0,  │   ← -1 = 眨眼, 1/2 = 小动作帧│  -1,0,0,2,  ││   0,0,0]    │
└──────┬──────┘
       │ speak() / pet()
       ▼
┌─────────────┐
│ Active State │ ← 快速循环所有帧
│  tick % 3   │
└──────┬──────┘
       │ 超时
       ▼
┌─────────────┐
│  Fade State  │ ← 气泡淡出
└──────┬──────┘
       │
       ▼
     回到 Idle

每个 tick(500ms)引擎会计算当前帧数据,然后通过回调发出一个 BuddyFrame 对象:

ts 复制代码
interface BuddyFrame {
  spriteLines: string[]        // ASCII 精灵行
  name: string                 // 宠物名字
  face: string                 // 单行脸部表情
  bubbleLines: string[] | null // 对话气泡文字
  isBubbleFading: boolean      // 气泡是否正在淡出
  isPetting: boolean           // 是否正在显示爱心
  colorKey: ColorKey           // 稀有度颜色键名
}

这个设计的精妙之处在于:引擎只负责计算状态,不负责渲染。渲染完全交给适配器层。

4. 适配器模式:一套引擎,多端渲染

scss 复制代码
                    ┌──────────────────┐
                    │   BuddyEngine    │
                    │  (动画状态机)     │
                    └────────┬─────────┘
                             │ BuddyFrame
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
     ┌────────────┐  ┌────────────┐  ┌────────────┐
     │  Terminal   │  │  Browser   │  │  Custom    │
     │  Adapter    │  │  Adapter   │  │  Adapter   │
     │ (ANSI码)    │  │ (DOM/CSS)  │  │ (自定义)    │
     └────────────┘  └────────────┘  └────────────┘
  • 终端适配器:使用 ANSI 转义码实现颜色和光标控制,原地刷新避免闪烁
  • 浏览器适配器:创建 DOM 元素,用 CSS transition 实现淡入淡出
  • 自定义适配器 :直接消费 BuddyFrame,想怎么渲染都行

快速上手

安装

bash 复制代码
npm install buddy-sprite

终端使用

ts 复制代码
import { assembleCompanion } from 'buddy-sprite'
import { createTerminalBuddy } from 'buddy-sprite/terminal'

const companion = assembleCompanion('your-user-id', {
  name: 'Quackers',
  personality: '一个热爱调试的快乐小伙伴',
  hatchedAt: Date.now(),
})

const buddy = createTerminalBuddy(companion)
buddy.start()
buddy.speak('嘎嘎!你好呀!')
buddy.pet()  // 摸头,会出现爱心动画

// 不用时记得销毁
// buddy.destroy()

浏览器使用

html 复制代码
<div id="buddy-container"></div>
<script type="module">
  import { assembleCompanion } from 'buddy-sprite'
  import { createBrowserBuddy } from 'buddy-sprite/browser'

  const companion = assembleCompanion('your-user-id', {
    name: 'Quackers',
    personality: '快乐',
    hatchedAt: Date.now(),
  })

  const buddy = createBrowserBuddy(
    document.getElementById('buddy-container'),
    companion,
  )
  buddy.start()
  buddy.speak('来自浏览器的问候!')
</script>

自定义渲染

如果你想在其他环境(比如 Electron、游戏引擎、甚至硬件终端)中使用,可以直接消费引擎输出:

ts 复制代码
import { BuddyEngine, assembleCompanion } from 'buddy-sprite'

const companion = assembleCompanion('your-user-id', {
  name: 'Quackers',
  personality: '快乐',
  hatchedAt: Date.now(),
})

const engine = new BuddyEngine(companion, (frame) => {
  // frame.spriteLines --- ASCII 精灵行
  // frame.bubbleLines --- 对话气泡文字
  // frame.isPetting   --- 是否正在显示爱心
  // 你可以用任何方式渲染这些数据
  console.clear()
  console.log(frame.spriteLines.join('\n'))
})
engine.start()

架构总览

r 复制代码
buddy-sprite/
├── src/
│   ├── index.ts          # 统一导出
│   ├── types.ts          # 类型定义(稀有度、物种、眼睛、帽子、属性)
│   ├── sprites.ts        # 18 种物种的 ASCII 精灵帧数据 + 渲染函数
│   ├── companion.ts      # 宠物生成(PRNG、哈希、稀有度抽取、属性生成)
│   ├── engine.ts         # BuddyEngine --- 动画状态机(核心)
│   └── adapters/
│       ├── terminal.ts   # 终端适配器(ANSI 转义码)
│       └── browser.ts    # 浏览器适配器(DOM 渲染)
└── examples/
    ├── terminal-demo.ts  # 终端演示脚本
    └── browser-demo.html # 浏览器演示页面(直接打开,无需构建)

整个项目的设计遵循几个原则:

  1. 零依赖:不依赖任何第三方包,纯 TypeScript 实现
  2. 确定性:相同输入永远产生相同输出,没有任何随机性
  3. 关注点分离:引擎只管状态计算,适配器只管渲染
  4. 三端通用:ESM + CJS + DTS,终端/浏览器/Node.js 都能用

一些有趣的设计细节

为什么用 FNV-1a + Mulberry32?

在浏览器和 Node.js 中,Math.random() 不支持种子。我们需要一个确定性的随机数生成器,而且要足够轻量。FNV-1a 哈希 + Mulberry32 PRNG 的组合总共不到 20 行代码,但质量足够好,分布均匀,周期为 2³²。

为什么属性有"峰值"和"低谷"?

每只宠物会随机选一个"峰值属性"和一个"低谷属性",其余属性随机分布。这让每只宠物都有自己的"性格"------比如一只高混乱低耐心的猫,或者一只高智慧低毒舌的猫头鹰。比起所有属性都差不多的平庸分布,这种设计更有趣。

为什么帽子只给非普通稀有度?

普通宠物(60% 概率)没有帽子,这让帽子成为一种"稀有标志"。当你看到一只戴着皇冠的传说级龙,那种感觉是不一样的。

总结

buddy-sprite 是一个有趣的小项目,它展示了几个值得关注的技术点:

  • 确定性随机生成:FNV-1a + Mulberry32 的组合在前端场景中非常实用
  • ASCII 帧动画:用最简单的字符串操作实现流畅的动画效果
  • 引擎-适配器分离:一套核心逻辑,多端渲染,这种架构模式值得借鉴
  • 零依赖设计:在依赖爆炸的前端生态中,保持克制是一种美德

如果你觉得有趣,欢迎 Star ⭐ 和 PR:


本文项目灵感来源于 Claude Code 泄漏源码中隐藏的 Buddy 系统。原始实现使用 React + Ink,buddy-sprite 是一个全新的独立实现。

相关推荐
IAUTOMOBILE2 小时前
Python 流程控制与函数定义:从调试现场到工程实践
java·前端·python
好大哥呀3 小时前
C++ Web 编程
开发语言·前端·c++
爱学习的小仙女!4 小时前
面试题 前端(一)DOCTYPE作用 标准模式与混杂模式区分
前端·前端面试题
小小小小宇4 小时前
前端转后端基础- 变量和类型
前端
Cobyte5 小时前
1.基于依赖追踪和触发的响应式系统的本质
前端·javascript·vue.js
主宰者5 小时前
C# CommunityToolkit.Mvvm全局事件
java·前端·c#
前端小咸鱼一条6 小时前
16.迭代器 和 生成器
开发语言·前端·javascript
小江的记录本6 小时前
【注解】常见 Java 注解系统性知识体系总结(附《全方位对比表》+ 思维导图)
java·前端·spring boot·后端·spring·mybatis·web
web守墓人6 小时前
【前端】记一次将ruoyi vue3 element-plus迁移到arco design vue的经历
前端·vue.js·arco design