The Buddy System: A Virtual Pet Game Inside Your Codebase
A deep dive into Claude Code's Buddy easter egg — deterministic RNG, Mulberry32 PRNG, rarity-weighted distributions, Bones vs Soul
The Problem
In a serious CLI coding tool, you type /buddy, and a duck wearing a crown pops up in your terminal, its eyes rendered as ✦ symbols, with ★★★★★ legendary displayed next to it. It has a name, a personality, and five stat values. Every time you open Claude Code, the same duck appears — it's yours.
This isn't a joke. Claude Code's Buddy system is a fully-featured deterministic companion generator that uses a PRNG (Pseudo-Random Number Generator) to derive a unique virtual pet from your userId. The system touches on hash functions, weighted probability distributions, Bones/Soul persistence separation, and other serious engineering topics.
Let's look at the engineering design behind this "April Fools' easter egg."
System Architecture Overview
The system splits into two entirely different data flows:
- Bones — Deterministically derived, never persisted. Recomputing from the userId always yields the exact same result
- Soul — Model-generated, stored in config after the first hatch. This is the only part that needs persistence
This separation is the most elegant architectural decision in the entire system, which we'll analyze in detail later.
Mulberry32: A Deterministic Random Number Generator
At the heart of the Buddy system is a PRNG called Mulberry32. It's only 6 lines of code, but it determines what every user's companion looks like.
Why Mulberry32
In the world of PRNGs, there are many choices — xorshift128+, PCG, Mersenne Twister. Mulberry32's advantages are:
- Extremely compact — State is just a single 32-bit integer, captured by a closure
- Good output quality — Passes most tests in the BigCrush test suite (more than sufficient for this use case)
- Deterministic — The same seed always produces the same sequence
The source code comment is refreshingly direct: "good enough for picking ducks". This isn't cryptography — it's picking ducks.
Bit Operation Breakdown
Let's break down this algorithm line by line:
0x6d2b79f5 is a carefully chosen constant (decimal 1831565813) whose binary representation has a nearly uniform distribution of 0s and 1s. >>> 0 converts the result to an unsigned 32-bit integer, and dividing by 4294967296 (i.e., 2^32) maps it to the [0, 1) interval — matching the range of Math.random().
From userId to Seed
There are two code paths here:
- Bun environment — Uses Bun's built-in
Bun.hash()(backed by wyhash), taking the lower 32 bits - Fallback path — A hand-written FNV-1a hash. The initial value
2166136261and multiplier16777619are standard FNV parameters
The SALT constant 'friend-2026-401' is appended to the userId before hashing, ensuring that even if someone knows a user's userId, they can't predict the companion without knowing the salt:
The 401 in the name hints at April 1st — April Fools' Day.
Species Encoding: A Trick to Bypass String Checks
The type definition file contains an interesting engineering decision. Look at how species are defined:
Why not just write export const duck = 'duck'? The source comment explains:
One species name collides with a model-codename canary in excluded-strings.txt. The check greps build output (not source), so runtime-constructing the value keeps the literal out of the bundle while the check stays armed for the actual codename.
Anthropic has an excluded-strings.txt file, and the build pipeline scans artifacts for these restricted strings (typically model codenames). One species name happens to collide with a model codename. The solution wasn't to encode just that one species, but to uniformly encode all species names, keeping the code style consistent and avoiding collision concerns when adding species in the future.
The full list of 18 species: duck, goose, blob, cat, dragon, octopus, owl, penguin, turtle, snail, ghost, axolotl, capybara, cactus, robot, rabbit, mushroom, chonk.
The as 'duck' type assertion ensures TypeScript's type system still knows the literal types of these values — type assertions exist at compile time and don't appear in build artifacts.
Rarity System: Weighted Probability Distribution
The total weight is 100, so these values directly represent percentage probabilities. The rollRarity() function implements weighted random selection:
This is the classic "roulette wheel selection" algorithm. Each rarity occupies a segment on a number line, and whichever segment the random number lands in gets selected. The final return 'common' is a floating-point precision safety net — under normal circumstances it will never be reached.
How Rarity Affects the Companion
Rarity isn't just a label; it directly influences two attributes:
Hat — common rarity companions don't get a hat:
Stat floor — Higher rarity means higher base values for all stats:
A legendary companion's dump stat (weakest attribute) is at least 50 - 10 + rand(15) = 40-54 points, while a common companion's peak stat (strongest attribute) is only 5 + 50 + rand(30) = 55-84 points.
Stat System: Peak/Dump Design
The five stat names are full of programmer humor:
DEBUGGING, PATIENCE, CHAOS, WISDOM, SNARK — these aren't game stats, they're a programmer personality test.
Stat allocation uses the classic RPG peak/dump pattern:
The design logic:
- Randomly select a peak stat — it gets a value of
floor + 50 + rand(30) - Randomly select a dump stat, which cannot be the same as the peak — it gets a value of
max(1, floor - 10 + rand(15)) - All remaining stats get
floor + rand(40)
The while (dump === peak) dump = pick(rng, STAT_NAMES) loop guarantees the dump and peak won't be the same stat. In theory this loop could execute multiple times, but each iteration has a 4/5 chance of picking a different stat, so it averages about 1.25 iterations.
Bones vs Soul: An Elegant Persistence Separation
This is the most engineering-rich design in the Buddy system. First, the type definitions:
The StoredCompanion persisted to config only contains the Soul part (name, personality, hatchedAt). The Bones part is never persisted — it's recomputed from the userId every time it's needed.
This Design Solves Three Problems
1. Anti-cheating — Users can edit ~/.claude/config.json, but modifying the rarity field is pointless because it will be overwritten by the recomputed value:
In { ...stored, ...bones }, bones comes last, so even if the config contains old bones fields, they get overwritten by the freshly computed values.
2. Safe upgrades — If the development team renames a species (e.g., changing blob to slime), or reorders the SPECIES array, no data migration is needed. The old config doesn't contain species information at all — regeneration naturally produces the new values.
3. Format evolution — StoredCompanion has only three fields and is very stable. In the future, Bones can freely add new attributes (such as new hat types) without affecting existing persisted data.
Roll Cache: Hot Path Optimization
The comment identifies three hot paths:
- 500ms sprite tick — The companion sprite's animation frame updates
- per-keystroke PromptInput — Input box rendering on every keystroke
- per-turn observer — The observer for each conversation turn
All three paths need the current companion's information, but the userId never changes during a session. A simple single-value cache (not a Map, not an LRU, just a single variable) is sufficient — because under normal use the key is always the same.
The elegance of this caching strategy lies in:
- Zero dependencies (no need for lodash's memoize)
- Minimal memory footprint (caches only one result)
- Natural invalidation (if the userId changes — e.g., switching accounts — it automatically recomputes)
Sprite System: ASCII Art Animation
Each species has three animation frames, where each frame is a 5-row by 12-wide ASCII character matrix:
{E} is an eye placeholder, replaced at render time with the companion's eye type (·, ✦, ×, ◉, @, °).
The hat system renders as an overlay on row 0:
The rendering logic has a subtle detail:
Two key checks:
- Only place hats on empty rows — Some animation frames have effects on row 0 (dragon's
~smoke, robot's*antenna flash); these frames must not be overwritten by hats - Remove blank rows — If there's no hat and all frames have an empty row 0, remove it to save space. But this only happens when all frames have an empty row; otherwise the height would jump between frames
CompanionSprite: Animation in React Components
The companion sprite is rendered in the terminal as a React component (Ink). Key constants define the animation behavior:
IDLE_SEQUENCE is a 15-frame looping sequence:
0— Rest frame (10/15 = 67% of the time, mostly resting)1— Slight movement (2/15 of the time)2— Larger movement (1/15 of the time)-1— Blink effect (1/15 of the time, overlaid on the rest frame)
Each tick is 500ms, so one full cycle takes 7.5 seconds. This pacing makes the companion feel "alive" without being too distracting.
The speech bubble has a fade-out effect: it displays for 20 ticks (10 seconds), with the last 6 ticks (3 seconds) gradually dimming. This gives the user a visual cue that the bubble is about to disappear, rather than vanishing abruptly.
Prompt Integration: How the Companion Interacts with the AI
This prompt tells Claude AI:
- You are not the companion — The companion is a separate entity
- The user knows the difference — No need to explain
- When the user talks to the companion, step aside — Respond in no more than one line
This is a carefully designed prompt boundary that prevents the AI from trying to role-play as the companion or conflict with the companion's speech bubble.
The introduction system also includes deduplication logic:
It checks the message history for an existing intro attachment with the same companion name, avoiding redundant companion introductions in long conversations that would waste tokens.
Release Timeline: Teaser Window Design
Two time functions define the release strategy:
- Teaser window (April 1-7) — If the user hasn't hatched a companion yet, a rainbow-colored
/buddyprompt appears on startup and disappears after 15 seconds - Permanently live (April 8 onward) — The
/buddycommand is always available, but no longer proactively prompted
The source comment explains the design rationale for using local time instead of UTC:
Local date, not UTC — 24h rolling wave across timezones. Sustained Twitter buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
Using local time means users in Tokyo see the easter egg about 14 hours before users in New York, creating a "discovery wave" spanning 24 hours instead of everyone flooding in at once — which is friendlier to the backend's soul generation (which requires calling the Claude model to generate names and personalities).
"external" === 'ant' is a build-time constant check. In external builds this is always false (because the string "external" doesn't equal "ant"), but in internal builds this value may differ — allowing Anthropic employees to test early.
Rarity Visual System
The color mapping reuses Claude Code's existing theme colors:
commonusesinactive(gray) — the most frequent, no need to be eye-catchinglegendaryuseswarning(gold/orange) — the most prominent in any theme
The shiny flag has a 1% chance:
Combined probability makes a shiny legendary one in ten thousand: 1% * 1% = 0.01%.
A Microcosm of Engineering Culture
The Buddy system may be an easter egg, but its engineering quality is no less rigorous:
Type safety — All constant arrays use as const and satisfies constraints. The type of RARITY_WEIGHTS is Record<Rarity, number>, so if a new rarity tier is added but its weight is forgotten, the compiler will flag the error.
Separation of concerns — Bones (deterministic data), Soul (persisted data), Sprites (rendering logic), and Prompt (AI interaction) are four modules, each with clear responsibilities and no coupling between them.
Defensive programming — The return 'common' at the end of rollRarity() is a floating-point precision safety net; while (dump === peak) prevents peak and dump from overlapping; bones last in the object spread prevents stale data from polluting the result.
Performance awareness — A single-value cache optimizes three hot paths; ASCII sprite matrices use pre-defined arrays rather than runtime generation; feature flags eliminate dead code at build time.
Release strategy — Rather than "ship when done," the team carefully designed a teaser window, timezone-rolling discovery, internal early testing, and other mechanisms.
This system uses roughly 800 lines of code (excluding CompanionSprite's UI code) to embed a complete virtual pet easter egg into a serious development tool. No third-party dependencies, no network requests (except for initial soul generation), no additional processes — just pure deterministic math plus a bit of ASCII art.
This is Anthropic's engineering culture: even an April Fools' easter egg is built with the same rigor as a production feature.