How Does That Little Creature Come Alive in the Terminal?

An ASCII critter lives inside Claude Code's terminal — it has deterministic genes, uncheatable rarity, a 15-step idle animation, and AI-driven real-time commentary. This system, called Buddy (referred to as Companion in the code), is a full-fledged virtual pet engine that blends design ideas from digital pets, gacha games, and terminal rendering. This chapter dismantles this tiny living creature in the terminal, from the PRNG seed to the ASCII sprite animation.

> 💡 **Plain English**: Like a Tamagotchi — it has its own personality and gives you little tips while you work.

---

### 🌍 Industry Context

Embedding a virtual pet or mascot into a developer tool is a long-standing tradition that has seen a revival in recent years. **Microsoft Office's Clippy** (1997–2007) was the earliest AI assistant mascot, but it became infamous for being overly intrusive — its failure made the industry wary of anthropomorphic AI assistants for a decade. **GitHub's** Octocat is purely a visual mascot with no interactive behavior. **Rust's** Ferris (the crab) occasionally appears in compiler error messages, adding a human touch. In the AI programming tools space, neither **Cursor** nor **Windsurf** has a virtual pet system — they focus on purely functional experiences. **Warp Terminal** experimentally added emoji-based feedback for AI conversations, but it never reached the level of a full virtual pet engine.

Claude Code's Buddy system far exceeds these precedents in technical depth: a deterministic gene system (anti-cheat), a gacha rarity mechanism, independent AI-driven real-time commentary, and precise terminal rendering — this is not a simple Easter egg, but a complete gamification system. This is indeed rare among CLI developer tools, but the design philosophy of "adding gamification to professional tools" is not new — Duolingo's entire product is built on this idea.

---

## The Question

You open Claude Code, and next to the input box, a small ASCII rabbit pops up wearing a wizard hat, occasionally blinking and commenting on Claude's responses. How does this Buddy system implement a "living" virtual pet in the terminal? And why can't its rarity be cheated?

---

> **[Diagram placeholder 2.15-A]**: State machine diagram — the complete lifecycle of Companion from hatching to rendering (PRNG seed → bones → soul → sprite → reaction)

## You Might Think…

"It's just some ASCII art with a bit of randomness, right?" That's probably what you'd think. Maybe it randomly picks an animal on each startup, draws it in the terminal, sets a timer to make it move, and that's it. As for the talking? Probably just a random pick from a preset dialogue pool.

---

## Here's How It Actually Works

The Buddy system (called Companion in the code) is a full virtual pet engine — with a deterministic gene system, a rarity mechanism, an ASCII sprite animation state machine, an independent AI reaction generator, and precise terminal-adaptive rendering.

### Section 1: Bones and Soul — A Pet's "Genes" and "Spirit"

Imagine you're playing a pet-raising game. The game needs to decide two things: what the pet looks like (species, color, rarity) and what it's called and what personality it has. Claude Code cleanly separates these into two independent abstractions:

- **Bones (skeleton)**: species, rarity, eyes, hat, shiny status, and five stat values. Entirely determined by `hash(userId + SALT)`, **never persisted**.
- **Soul (spirit)**: name and personality description. Generated by AI at hatching time and stored in the user's configuration file.

```typescript
// src/buddy/types.ts, lines 101–124
export type CompanionBones = {
  rarity: Rarity
  species: Species
  eye: Eye
  hat: Hat
  shiny: boolean
  stats: Record<StatName, number>
}

export type CompanionSoul = {
  name: string
  personality: string
}

// Only soul + hatchedAt are actually persisted
export type StoredCompanion = CompanionSoul & { hatchedAt: number }
```

Why this design? Three reasons:

1. **Anti-cheat**. The config file is just JSON — users can edit it freely. If rarity were stored there, you could just change it to `"legendary"`. But bones are never persisted; they are recomputed from the userId every time. You can change the name, but you can't change destiny.
2. **Safe evolution**. If Anthropic decides to rename `"duck"` to `"mallard"`, existing pets are unaffected — the old species name was never stored anywhere.
3. **Minimal storage**. The config file only needs three fields: `{name, personality, hatchedAt}`.

At read time, the two are merged:

```typescript
// src/buddy/companion.ts, lines 127–133
export function getCompanion(): Companion | undefined {
  const stored = getGlobalConfig().companion
  if (!stored) return undefined
  const { bones } = roll(companionUserId())
  return { ...stored, ...bones }
}
```

Note that `...bones` comes after `...stored` — if an old config happens to contain stale bones fields, they get overwritten.

### Section 2: Mulberry32 — "Good Enough for Picking Ducks"

The key to determinism is that the same userId must always produce the same pet. This rules out `Math.random()` (different every time) and requires a **seedable pseudo-random number generator (PRNG)**.

```typescript
// src/buddy/companion.ts, lines 16–25
// Mulberry32 — tiny seeded PRNG, good enough for picking ducks
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 is a 32-bit-state multiplicative-hash PRNG, from a paper by Tommy Ettinger. The humor in the comment — "good enough for picking ducks" — precisely describes the use case: we don't need cryptographic-grade randomness, we just need to pick one out of 18 animals.

> 📚 **Course Connection**: Pseudo-random number generators (PRNGs) are a core topic in **computer organization** and **cryptography** courses. Mulberry32 is a variant of a linear congruential generator (LCG) — it uses bitwise operations and multiplication for state transitions, with a period of 2^32. A cryptography course would emphasize that PRNGs must not be used in security-sensitive scenarios (e.g., key generation), but they are perfectly adequate for non-security use cases like gacha draws and UI animations. The "seedability" here guarantees determinism: the same input always produces the same output, which is the core requirement of a **pure function** in functional programming.

The seed comes from `hash(userId + SALT)`. The value of `SALT` is `'friend-2026-401'` — hinting that this feature launched on April 1, 2026 (April Fools' Day). The purpose of salting: even if you know the algorithm, you can't construct a specific userId to "reroll" a legendary.

The hash function has a dual-path implementation — when running under Bun, it uses native `Bun.hash()` (faster); otherwise it falls back to FNV-1a. Both take the lower 32 bits.

### Section 3: Rarity Wheel and Stat Allocation

Each pet's rarity is decided by weighted randomness:

| Rarity | Weight | Probability | Stars | Min Stat |
|--------|--------|-------------|-------|----------|
| common | 60 | 60% | ★ | 5 |
| uncommon | 25 | 25% | ★★ | 15 |
| rare | 10 | 10% | ★★★ | 25 |
| epic | 4 | 4% | ★★★★ | 35 |
| legendary | 1 | 1% | ★★★★★ | 50 |

Stat allocation uses a "one strong, one weak" strategy: one peak stat and one weak stat are randomly chosen, with the rest scattered in between. The peak stat can reach 100, and the weak stat can be as low as 1. The five stat names are full of personality: `DEBUGGING`, `PATIENCE`, `CHAOS`, `WISDOM`, and `SNARK`.

An interesting rule: only uncommon and above get hats (`companion.ts:97`). Common pets appear bare-headed.

### Section 4: 18 Species and Their Secret

Species names are encoded in `types.ts` using `String.fromCharCode()`:

```typescript
// src/buddy/types.ts, lines 14–17
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'
```

Why not just write `'duck'`? The 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's build system scans compiled artifacts to check for accidental leaks of internal model codenames. One species name happens to match one such codename. By constructing the string at runtime, the literal never appears in the build output, while the codename detection mechanism remains active. For consistency, **all** species names use this encoding — so the "special" one doesn't stand out.

Each of the 18 species has a 3-frame ASCII sprite, with each frame being 5 lines by 12 characters wide:

```
// Duck (duck) — three-frame animation
Frame 0:     __        Frame 1:     __        Frame 2:     __
       <(· )___           <(· )___           <(· )___
        (  ._>             (  ._>             (  .__>
         `--´               `--´~              `--´
```

### Section 5: The 500ms Heartbeat Animation State Machine

The creature's sense of life comes from a 500ms-interval tick timer driving a 15-step idle sequence:

```typescript
// src/buddy/CompanionSprite.tsx, line 23
const IDLE_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 2, 0, 0, 0]
```

Most of the time it rests (frame 0), occasionally fidgeting (frame 1 or 2). `-1` means blink (replaces the eye character with `-` on top of frame 0). The full sequence loops every 7.5 seconds — just the right frequency to feel like "it moves every now and then."

> 📚 **Course Connection**: This 15-step sequence is essentially **keyframe animation** from a computer graphics course — using discrete frame sequences to simulate continuous motion. The 500ms tick interval corresponds to 2 FPS, far below a game's 60 FPS, but perfectly adequate for terminal text animation. State-machine-driven animation switching (idle/excited/pet) is a classic application of the finite state machine (FSM) pattern in **game engines**.

When the pet has a reaction (speaking) or is petted (`/buddy pet`), it switches to excited mode — rapidly cycling through all frames. Petting also triggers a heart particle effect:

```typescript
// src/buddy/CompanionSprite.tsx, line 27
const H = figures.heart
const PET_HEARTS = [
  `   ${H}    ${H}   `,
  `  ${H}  ${H}   ${H}  `,
  ` ${H}   ${H}  ${H}   `,
  `${H}  ${H}      ${H} `,
  '·    ·   ·  '
]
```

A five-frame heart-rising animation, lasting 2.5 seconds.

### Section 6: Its Comments Come from an Independent AI Call

This is the most unexpected part: the companion's speech bubble is **not** picked from a preset pool — it's **genuinely AI-generated**.

After each Claude response, REPL.tsx asynchronously calls `fireCompanionObserver`:

```typescript
// src/screens/REPL.tsx, lines 2804–2808
if (feature('BUDDY')) {
  void fireCompanionObserver(messagesRef.current, reaction =>
    setAppState(prev => ({...prev, companionReaction: reaction}))
  )
}
```

`fireCompanionObserver` is defined in `buddy/observer.ts` — that file is missing from the extracted source (likely stripped by a compile-time feature flag), but from the call pattern we can infer: it receives the current conversation messages, generates a short comment via an independent AI call, and updates AppState through a callback.

The companion prompt injection (`prompt.ts`) explicitly tells the main model to keep its distance:

> "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."

Two independent AI entities collaborate in the same terminal — the main model does the work, and the companion provides commentary.

### Section 7: April Fools' Launch Engineering

Buddy's release window design is textbook product engineering:

```typescript
// src/buddy/useBuddyNotification.tsx, lines 12–21
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
export function isBuddyTeaserWindow(): boolean {
  if ("external" === 'ant') return true  // Internal employees bypass
  const d = new Date()
  return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7
}
```

The comment reveals the key design decision:

> "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 instead of UTC means Tokyo users unlock at 00:00 JST on April 1, while San Francisco users wait until 00:00 PDT on April 1. This creates a 24-hour global "rolling wave" of unlocks — social media discussion lasts a full day instead of peaking in a single moment, and it eases the server load for "soul generation" (AI-generated names/personalities).

During the teaser window (April 1–7), unhatched users see a rainbow-colored `/buddy` hint in the status bar for 15 seconds before it fades away.

---

## The Philosophy Behind It

The Buddy system borrows design paradigms from **Tamagotchi** and **gacha games**:

1. **Determinism + immutability = perceived scarcity**. Each user gets only one pet, determined by the userId hash, with no way to reset. This mirrors the gacha logic of "one roll decides your fate" — the 1% legendary probability gives lucky users real bragging rights.
2. **Bones/Soul separation**. Similar to the separation of Model and View in MVC, but applied to a virtual pet — the "invariant" data (bones) and the "mutable" data (soul) have completely different lifecycles and storage strategies.
3. **Compile-time feature flag**. `feature('BUDDY')` evaluates to a constant at Bun compile time; when disabled, all Buddy code is tree-shaken away. Zero runtime overhead for an optional feature.
4. **ASCII as a design constraint**. Drawing an animal in a GUI framework is easy, but drawing 18 recognizable, animated animals inside a 5×12 character matrix in a terminal — that's constrained creative design.

---

## Code Landing Spots

- `src/buddy/types.ts`, lines 1–149: complete type definitions, 18 species (`SPECIES`), 6 eyes (`EYES`), 8 hats (`HATS`), 5 stats (`STAT_NAMES`), rarity weights (`RARITY_WEIGHTS`)
- `src/buddy/companion.ts`, lines 16–25: `mulberry32` — Mulberry32 PRNG implementation
- `src/buddy/companion.ts`, lines 27–37: `hashString` — dual-path Bun/FNV-1a hash
- `src/buddy/companion.ts`, lines 43–51: `rollRarity` — weighted random rarity draw
- `src/buddy/companion.ts`, lines 62–82: `rollStats` — "one strong, one weak" stat allocation
- `src/buddy/companion.ts`, line 84: `SALT = 'friend-2026-401'`
- `src/buddy/companion.ts`, lines 91–102: `rollFrom` — generate complete bones from PRNG
- `src/buddy/companion.ts`, lines 107–113: `roll` — entry point with single-item caching
- `src/buddy/companion.ts`, lines 127–133: `getCompanion` — bones + soul merge
- `src/buddy/sprites.ts`, lines 27–441: `BODIES` — sprite frame data for 18 species
- `src/buddy/sprites.ts`, lines 443–452: `HAT_LINES` — ASCII rendering for 8 hats
- `src/buddy/sprites.ts`, lines 454–469: `renderSprite` — sprite rendering (hat slot replacement + empty-line optimization)
- `src/buddy/CompanionSprite.tsx`, lines 17–20: `TICK_MS=500`, `BUBBLE_SHOW=20`, `FADE_WINDOW=6`, `PET_BURST_MS=2500`
- `src/buddy/CompanionSprite.tsx`, line 23: `IDLE_SEQUENCE` — 15-step idle animation sequence
- `src/buddy/CompanionSprite.tsx`, line 152: `MIN_COLS_FOR_FULL_SPRITE = 100`
- `src/buddy/CompanionSprite.tsx`, lines 167–175: `companionReservedColumns` — terminal column width reservation
- `src/buddy/CompanionSprite.tsx`, lines 176–290: `CompanionSprite` — main rendering component
- `src/buddy/CompanionSprite.tsx`, lines 296–358: `CompanionFloatingBubble` — fullscreen mode floating bubble
- `src/buddy/prompt.ts`, lines 7–12: `companionIntroText` — AI system prompt injection
- `src/buddy/useBuddyNotification.tsx`, lines 12–21: `isBuddyTeaserWindow`/`isBuddyLive` — release window logic
- `src/screens/REPL.tsx`, lines 2804–2808: `fireCompanionObserver` call site
- `src/state/AppStateStore.ts`, lines 168–171: `companionReaction`/`companionPetAt` state definitions
- `src/utils/config.ts`, lines 269–271: `companion`/`companionMuted` config fields

---

## Limitations and Critique

- **Observer cost is opaque**: It's unclear whether the companion AI call after each response is throttled, which could lead to significant extra API overhead during high-frequency conversations.
- **ASCII accessibility issue**: A 5×12 character sprite is meaningless to screen-reader users, and there is currently no alternative text description.
- **Hardcoded sprites are hard to extend**: The sprite frame data for all 18 species is hardcoded in `sprites.ts`. Adding a new species requires hand-crafting ASCII art; there is no procedural generation capability.

---

## Directions for Further Inquiry

1. **AI call details in observer.ts**: What model is used? How is the system prompt written? Does it use `effort:'low'` to save tokens?
2. **Hatching ritual UX**: What visual effects occur when the `/buddy` command runs? How does the AI generate a name and personality that match the bones?
3. **Multi-companion interaction**: If two users collaborate in Swarm mode, do their companions see each other?
4. **Cost control**: Is the per-response observer call throttled? Is it skipped during high-frequency conversations?
5. **Accessibility design**: What does an ASCII sprite mean for screen-reader users? Is there an alternative text description?

---

*Quality self-check:*
- [x] Coverage: 6 source files fully analyzed; missing observer.ts is noted
- [x] Fidelity: all constant values and line numbers come from the source code
- [x] Readability: analogies to digital pets/gacha establish intuition
- [x] Consistency: follows the standard Q&A chapter structure
- [x] Critique: points out observer cost, ASCII accessibility, and hardcoded sprite limitations
- [x] Reusability: the Bones/Soul separation architecture can be applied to any system where "determinism + mutability" must coexist
