Skip to content

Instantly share code, notes, and snippets.

@zmxv
Created April 1, 2026 05:13
Show Gist options
  • Select an option

  • Save zmxv/7f83671f860c15be02f45b07fee207fc to your computer and use it in GitHub Desktop.

Select an option

Save zmxv/7f83671f860c15be02f45b07fee207fc to your computer and use it in GitHub Desktop.
Claude Code buddy animation
#!/usr/bin/env python3
"""Generate an animated GIF of all companion sprite animations."""
from PIL import Image, ImageDraw, ImageFont
# --- Sprite data ---
SPECIES = [
'duck', 'goose', 'blob', 'cat', 'dragon', 'octopus', 'owl', 'penguin',
'turtle', 'snail', 'ghost', 'axolotl', 'capybara', 'cactus', 'robot',
'rabbit', 'mushroom', 'chonk',
]
EYE = '·'
BODIES = {
'duck': [
[' ', ' __ ', ' <(· )___ ', ' ( ._> ', ' `--´ '],
[' ', ' __ ', ' <(· )___ ', ' ( ._> ', ' `--´~ '],
[' ', ' __ ', ' <(· )___ ', ' ( .__> ', ' `--´ '],
],
'goose': [
[' ', ' (·> ', ' || ', ' _(__)_ ', ' ^^^^ '],
[' ', ' (·> ', ' || ', ' _(__)_ ', ' ^^^^ '],
[' ', ' (·>> ', ' || ', ' _(__)_ ', ' ^^^^ '],
],
'blob': [
[' ', ' .----. ', ' ( · · ) ', ' ( ) ', ' `----´ '],
[' ', ' .------. ', ' ( · · ) ', ' ( ) ', ' `------´ '],
[' ', ' .--. ', ' (· ·) ', ' ( ) ', ' `--´ '],
],
'cat': [
[' ', ' /\\_/\\ ', ' ( · ·) ', ' ( ω ) ', ' (")_(") '],
[' ', ' /\\_/\\ ', ' ( · ·) ', ' ( ω ) ', ' (")_(")~ '],
[' ', ' /\\-/\\ ', ' ( · ·) ', ' ( ω ) ', ' (")_(") '],
],
'dragon': [
[' ', ' /^\\ /^\\ ', ' < · · > ', ' ( ~~ ) ', ' `-vvvv-´ '],
[' ', ' /^\\ /^\\ ', ' < · · > ', ' ( ) ', ' `-vvvv-´ '],
[' ~ ~ ', ' /^\\ /^\\ ', ' < · · > ', ' ( ~~ ) ', ' `-vvvv-´ '],
],
'octopus': [
[' ', ' .----. ', ' ( · · ) ', ' (______) ', ' /\\/\\/\\/\\ '],
[' ', ' .----. ', ' ( · · ) ', ' (______) ', ' \\/\\/\\/\\/ '],
[' o ', ' .----. ', ' ( · · ) ', ' (______) ', ' /\\/\\/\\/\\ '],
],
'owl': [
[' ', ' /\\ /\\ ', ' ((·)(·)) ', ' ( >< ) ', ' `----´ '],
[' ', ' /\\ /\\ ', ' ((·)(·)) ', ' ( >< ) ', ' .----. '],
[' ', ' /\\ /\\ ', ' ((·)(-)) ', ' ( >< ) ', ' `----´ '],
],
'penguin': [
[' ', ' .---. ', ' (·>·) ', ' /( )\\ ', ' `---´ '],
[' ', ' .---. ', ' (·>·) ', ' |( )| ', ' `---´ '],
[' .---. ', ' (·>·) ', ' /( )\\ ', ' `---´ ', ' ~ ~ '],
],
'turtle': [
[' ', ' _,--._ ', ' ( · · ) ', ' /[______]\\ ', ' `` `` '],
[' ', ' _,--._ ', ' ( · · ) ', ' /[______]\\ ', ' `` `` '],
[' ', ' _,--._ ', ' ( · · ) ', ' /[======]\\ ', ' `` `` '],
],
'snail': [
[' ', ' · .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '],
[' ', ' · .--. ', ' | ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '],
[' ', ' · .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~ '],
],
'ghost': [
[' ', ' .----. ', ' / · · \\ ', ' | | ', ' ~`~``~`~ '],
[' ', ' .----. ', ' / · · \\ ', ' | | ', ' `~`~~`~` '],
[' ~ ~ ', ' .----. ', ' / · · \\ ', ' | | ', ' ~~`~~`~~ '],
],
'axolotl': [
[' ', '}~(______)~{', '}~(· .. ·)~{', ' ( .--. ) ', ' (_/ \\_) '],
[' ', '~}(______){~', '~}(· .. ·){~', ' ( .--. ) ', ' (_/ \\_) '],
[' ', '}~(______)~{', '}~(· .. ·)~{', ' ( -- ) ', ' ~_/ \\_~ '],
],
'capybara': [
[' ', ' n______n ', ' ( · · ) ', ' ( oo ) ', ' `------´ '],
[' ', ' n______n ', ' ( · · ) ', ' ( Oo ) ', ' `------´ '],
[' ~ ~ ', ' u______n ', ' ( · · ) ', ' ( oo ) ', ' `------´ '],
],
'cactus': [
[' ', ' n ____ n ', ' | |· ·| | ', ' |_| |_| ', ' | | '],
[' ', ' ____ ', ' n |· ·| n ', ' |_| |_| ', ' | | '],
[' n n ', ' | ____ | ', ' | |· ·| | ', ' |_| |_| ', ' | | '],
],
'robot': [
[' ', ' .[||]. ', ' [ · · ] ', ' [ ==== ] ', ' `------´ '],
[' ', ' .[||]. ', ' [ · · ] ', ' [ -==- ] ', ' `------´ '],
[' * ', ' .[||]. ', ' [ · · ] ', ' [ ==== ] ', ' `------´ '],
],
'rabbit': [
[' ', ' (\\__/) ', ' ( · · ) ', ' =( .. )= ', ' (")__(") '],
[' ', ' (|__/) ', ' ( · · ) ', ' =( .. )= ', ' (")__(") '],
[' ', ' (\\__/) ', ' ( · · ) ', ' =( . . )= ', ' (")__(") '],
],
'mushroom': [
[' ', ' .-o-OO-o-. ', '(__________)', ' |· ·| ', ' |____| '],
[' ', ' .-O-oo-O-. ', '(__________)', ' |· ·| ', ' |____| '],
[' . o . ', ' .-o-OO-o-. ', '(__________)', ' |· ·| ', ' |____| '],
],
'chonk': [
[' ', ' /\\ /\\ ', ' ( · · ) ', ' ( .. ) ', ' `------´ '],
[' ', ' /\\ /| ', ' ( · · ) ', ' ( .. ) ', ' `------´ '],
[' ', ' /\\ /\\ ', ' ( · · ) ', ' ( .. ) ', ' `------´~ '],
],
}
EYES = ['·', '✦', '×', '◉', '@', '°']
HATS = {
'crown': ' \\^^^/ ',
'tophat': ' [___] ',
'propeller': ' -+- ',
'halo': ' ( ) ',
'wizard': ' /^\\ ',
'beanie': ' (___) ',
'tinyduck': ' ,> ',
}
RARITIES = [
('common', (120, 120, 150)),
('uncommon', (80, 200, 120)),
('rare', (100, 149, 237)),
('epic', (200, 120, 255)),
('legendary', (255, 200, 60)),
]
RARITY_WEIGHT = {'common': 60, 'uncommon': 25, 'rare': 10, 'epic': 4, 'legendary': 1}
BUDDY_COLORS = [
(255, 50, 50), (255, 165, 0), (255, 255, 0),
(0, 200, 0), (50, 100, 255), (180, 50, 255),
]
# --- Layout ---
COL_W = 12
COLS = 6
FONT_SIZE = 16
FONT_PATH = '/System/Library/Fonts/Menlo.ttc'
BG = (22, 22, 38)
FG = (220, 220, 220)
LABEL = (120, 120, 150)
ACCENT = (255, 130, 160)
PAD_X, PAD_Y = 24, 12
MARGIN = 32
SECTION_GAP = 32
TITLE_GAP = 8
FRAME_MS = 300
BLANK = ' ' * COL_W
# --- Rendering ---
def render_sprite(species, frame_idx):
"""Return lines for one animation frame, dropping unused hat slot."""
frames = BODIES[species]
body = list(frames[frame_idx % len(frames)])
if not body[0].strip() and all(not f[0].strip() for f in frames):
body = body[1:]
return body
def render_all():
"""Pre-render all frames with bottom-aligned height normalization."""
rendered = []
for sp in SPECIES:
sp_frames = [render_sprite(sp, f) for f in range(len(BODIES[sp]))]
max_h = max(len(fr) for fr in sp_frames)
for fr in sp_frames:
while len(fr) < max_h:
fr.insert(0, BLANK)
rendered.append(sp_frames)
global_h = max(len(fs[0]) for fs in rendered)
for sp_frames in rendered:
for fr in sp_frames:
while len(fr) < global_h:
fr.insert(0, BLANK)
return rendered, global_h
def draw_title(draw, tx, ty, font, char_w):
"""Draw the title line with rainbow /buddy."""
title = 'claude code buddies'
draw.text((tx, ty), title, font=font, fill=ACCENT)
sx = tx + (len(title) + 1) * char_w
draw.text((sx, ty), '(type ', font=font, fill=FG)
sx += 6 * char_w
for ch, color in zip('/buddy', BUDDY_COLORS):
draw.text((sx, ty), ch, font=font, fill=color)
sx += char_w
draw.text((sx, ty), ' to activate)', font=font, fill=FG)
def draw_sprites(draw, all_rendered, frame_idx, cell_w, label_h, line_h):
"""Draw the sprite grid."""
cell_h = label_h + max(len(fs[0]) for fs in all_rendered) * line_h + PAD_Y * 2
eye = EYES[frame_idx % len(EYES)]
for i, sp in enumerate(SPECIES):
col, row = i % COLS, i // COLS
x0 = MARGIN + col * cell_w + PAD_X
y0 = MARGIN + line_h + SECTION_GAP + row * cell_h + PAD_Y
draw.text((x0, y0), sp, font=draw._default_font, fill=LABEL)
fc = len(BODIES[sp])
for j, line in enumerate(all_rendered[i][frame_idx % fc]):
draw.text((x0, y0 + label_h + j * line_h), line.replace(EYE, eye), font=draw._default_font, fill=FG)
def draw_hats(draw, y, font, cell_w, line_h):
"""Draw hats in two rows (4+3) aligned with grid columns."""
x_left = MARGIN + PAD_X
draw.text((x_left, y), 'hats', font=font, fill=ACCENT)
y += line_h + TITLE_GAP
hat_items = list(HATS.items())
for ri, row_hats in enumerate([hat_items[:4], hat_items[4:]]):
if ri > 0:
y += line_h
for ci, (name, art) in enumerate(row_hats):
hx = MARGIN + ci * cell_w + PAD_X
draw.text((hx, y), name, font=font, fill=LABEL)
draw.text((hx, y + line_h * 2), art, font=font, fill=FG)
y += line_h * 3
return y
def draw_rarity(draw, y, font, char_w, cell_w, line_h):
"""Draw rarity bar chart with legend."""
x_left = MARGIN + PAD_X
draw.text((x_left, y), 'rarity', font=font, fill=ACCENT)
y += line_h + TITLE_GAP
bar_w = COLS * cell_w - 2 * PAD_X
bar_h = line_h + 4
total = sum(RARITY_WEIGHT.values())
bx = x_left
for name, color in RARITIES:
seg_w = max(1, int(bar_w * RARITY_WEIGHT[name] / total))
draw.rectangle([bx, y, bx + seg_w - 1, y + bar_h - 1], fill=color)
bx += seg_w
y += bar_h + TITLE_GAP
lx = x_left
for name, color in RARITIES:
sw = char_w
sy = y + (line_h - sw) // 2
draw.rectangle([lx, sy, lx + sw - 1, sy + sw - 1], fill=color)
lx += sw + char_w // 2
label = f'{name} {RARITY_WEIGHT[name]}%'
draw.text((lx, y), label, font=font, fill=color)
lx += (len(label) + 2) * char_w
return y + line_h
def calc_static_height(line_h):
"""Height of hats + rarity sections."""
h = SECTION_GAP + line_h + TITLE_GAP # hats title
h += line_h * 3 + line_h + line_h * 3 # two hat rows + gap
h += SECTION_GAP + line_h + TITLE_GAP # rarity title
h += line_h + 4 + TITLE_GAP + line_h + PAD_Y # bar + legend + padding
return h
def draw_frame(frame_idx, all_rendered, max_h, font, char_w, line_h):
"""Render one animation frame as a PIL Image."""
label_h = line_h + 4
cell_w = COL_W * char_w + PAD_X * 2
cell_h = label_h + max_h * line_h + PAD_Y * 2
n_rows = (len(SPECIES) + COLS - 1) // COLS
title_h = line_h + SECTION_GAP
grid_h = n_rows * cell_h
img_w = COLS * cell_w + MARGIN * 2
img_h = title_h + grid_h + calc_static_height(line_h) + MARGIN * 2
img = Image.new('RGB', (img_w, img_h), BG)
draw = ImageDraw.Draw(img)
draw._default_font = font
draw_title(draw, MARGIN + PAD_X, MARGIN + PAD_Y, font, char_w)
eye = EYES[frame_idx % len(EYES)]
for i, sp in enumerate(SPECIES):
col, row = i % COLS, i // COLS
x0 = MARGIN + col * cell_w + PAD_X
y0 = MARGIN + title_h + row * cell_h + PAD_Y
draw.text((x0, y0), sp, font=font, fill=LABEL)
fc = len(BODIES[sp])
for j, line in enumerate(all_rendered[i][frame_idx % fc]):
draw.text((x0, y0 + label_h + j * line_h), line.replace(EYE, eye), font=font, fill=FG)
y = MARGIN + title_h + grid_h + SECTION_GAP
y = draw_hats(draw, y, font, cell_w, line_h)
y += SECTION_GAP
draw_rarity(draw, y, font, char_w, cell_w, line_h)
return img
def main():
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
bbox = font.getbbox('M')
char_w, char_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
line_h = char_h + 4
all_rendered, max_h = render_all()
frames = [draw_frame(f, all_rendered, max_h, font, char_w, line_h) for f in range(len(EYES))]
out = 'sprites.gif'
frames[0].save(out, save_all=True, append_images=frames[1:], duration=FRAME_MS, loop=0)
print(f'Saved {out} ({frames[0].size[0]}x{frames[0].size[1]}, {len(frames)} frames)')
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment