Glub

A fish tank where every creature is procedurally generated from a seed, simulated with needs and personality, and rendered entirely with code.

The Problem

Virtual pets usually feel like animated decorations. They swim around randomly, maybe with some idle animations, but there's no there there — no personality, no relationships, no reason to care about one fish versus another.

I wanted to build something where each creature is genuinely unique — not just visually, but behaviorally. A bold eel that explores the far corners of the tank. A lazy puffer that parks itself near the substrate and barely moves. Two fish that gradually bond from spending time near each other. The kind of emergent personality that makes you name them and check on them.

The Approach

Every fish starts as a 32-bit seed run through Mulberry32 PRNG. This seed deterministically selects traits from a combinatorial space of 12 body shapes × 20 patterns × 16 fin styles × 14 eye styles × 10 mouths × 22 special effects × 30+ palettes. Same seed, same fish. Always.

seeded prng (mulberry32)
function createRng(seed: number) {
  let state = seed | 0
  return () => {
    state = (state + 0x6d2b79f5) | 0
    let t = Math.imul(state ^ (state >>> 15), 1 | state)
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}

Trait selection uses weighted random sampling with Y2K aesthetic boosting — sparkle eyes get 1.5× weight, shimmer patterns get extra priority. Rarity emerges naturally from weight inversion: a round fish with spots is common; a crescent body with constellation pattern and bioluminescent fins is rare. I define 12 mythic combinations by hand (Phantom Nebula = ghost effect + constellation pattern, Holographic Angel = angel body + shimmer + rainbow) because some combos are too good to leave to chance.


Movement uses Craig Reynolds' steering behaviors. Each fish has a base speed determined by body shape — dart at 90px/s, seahorse at 25px/s — modified by personality traits. The wander behavior projects a target onto a circle ahead of the fish and jitters it each frame. The jitter amount scales with curiosity (curious fish weave more) and inversely with laziness (lazy fish swim straighter).

steering
// Wander: project target on circle, jitter per frame
wanderDist   = 40 + curiosity * 40    // how far ahead
wanderRadius = 20 + curiosity * 30    // circle size
jitter       = (4 + (1-laziness)*8) * dt  // angular noise

// Seek: arrive with deceleration
desired = normalize(target - pos) * maxSpeed
steering = clamp(desired - velocity, maxForce)

// State-specific modifiers:
//   eating  → 2.5× force toward food
//   scared  → flee at 1.5× max force
//   bonding → match partner direction at 0.8× force

The AI runs on a state machine with 8 states: swimming, idle, eating, sleeping, scared, exploring, bonding, and playing. State transitions are driven by a needs system — hunger decays continuously (~3.3 hours to empty from full), energy drains during activity and restores during sleep, happiness responds to social proximity and feeding.

A hungry fish seeks food within a detection range that scales with its curiosity trait (30 + curiosity * 50 pixels). A social fish near a friend accumulates bonding time, and after 3 seconds within 35 pixels, both enter a synchronized bonding state and add each other to their friendIds list. Permanently.


The rendering is entirely procedural — zero image assets. Each body shape is a hand-crafted pixel grid (2D number arrays where values 1/2/3/4 map to primary/secondary/accent/outline colors). Patterns overlay the body: stripes modulate by row, spots place circles at seeded positions, shimmer interpolates colors on a sine wave per frame.

Everything renders through PixiJS Graphics primitives at a fixed 480×270 resolution with image-rendering: pixelated for that pixel-perfect retro look. Fins attach to body edges with animation systems (wave, flap, trail, pulse). 22 special effects layer on top — bubbles, sparkles, crowns, halos, lightning bolts, rainbow trails, all procedural.

The tank maps real-world time — your actual clock determines whether it's dawn, noon, or midnight in the tank, shifting the lighting overlay and triggering sleep behavior at night. A gratitude coral reef grows logarithmically as you add notes:

coral growth
size = 1 + 29 × log(1 + ageDays) / log(1 + 90)

Day 0  → size  1   (newly planted)
Day 7  → size  5.5 (fast early growth)
Day 30 → size 11.5 (slowing down)
Day 90 → size 28   (approaching asymptote of 30)

The Hard Parts

Making procedural fish look good. When you're combining 12 body shapes with 20 patterns and 16 fin styles, most combinations need to work. I spent more time on the pixel grids than any other part of this project. Each body shape is hand-drawn at pixel level, and the pattern system had to work across all of them — spots that look natural on a round fish look wrong on an eel.

The fix was making pattern parameters (spot density, stripe spacing) scale with body dimensions rather than using fixed values. Plus a palette system with 30+ curated color sets across 10 semantic groups (warm shallows, deep ocean, Y2K glitter, Lisa Frank, vaporwave) — and a blending function that lerps two palettes in RGB space with hue/saturation shifts to generate unique palettes from a seed.


Personality that's visible but not cartoonish. Five personality traits (curiosity, laziness, sociability, skittishness, boldness) drive real behavioral differences, but the challenge is making those differences perceptible without exaggerating them. A bold fish and a skittish fish have different scare radii (80px vs 120px) and different speed modifiers, but the biggest visible difference comes from the wander behavior — curious fish weave through the tank while lazy fish drift in gentle arcs.

Body shapes have personality biases that reinforce visual expectations: eels are bold explorers (curiosity 0.7, boldness 0.8), seahorses are shy (skittishness 0.6, boldness 0.3), darts are energetic social fish (curiosity 0.8, sociability 0.7). Each trait drifts from its base by ±0.3, so you can get a surprisingly bold seahorse, but it's rare.


Emergent vs. designed behavior. I wanted behavior that feels emergent but is actually carefully designed. The bonding system is a good example — it's a proximity timer (3 seconds within 35 pixels triggers bonding), but because fish swim organically via steering behaviors, the bonding looks spontaneous. Two fish happen to drift near each other, stay close long enough, and suddenly they're swimming together.

It's scripted emergence — the rules are simple and deterministic, but the behavior they produce feels unpredictable because it depends on the interaction of multiple independent personality-driven agents navigating a shared space.

What I'd Do Differently

I'd implement proper Boids flocking — separation, alignment, cohesion — for schooling behavior. Right now fish are essentially independent agents that can bond in pairs. Real schooling with Reynolds' three rules would create the mesmerizing coordinated movement that makes aquariums hypnotic, especially with a tank full of same-species fish.

The personality system is currently read-only — traits are set at generation and never change. I'd add personality drift based on experience: a fish that gets scared repeatedly becomes more skittish over time, one that bonds frequently becomes more social. This would make long-term care genuinely matter — the fish you raise from a fry would develop differently based on the environment you provide.

The needs system could use more depth. Hunger decays linearly and food restores a fixed amount. A more interesting model would have metabolism that varies with activity level and size, seasonal appetite changes, and food preferences that develop over time. The bones of the system support this — it's architected with an event bus and modular needs updater — but the current rules are deliberately simple.

projects
🍊froot🪡drape💎hopperkinetic💡lamp
worlds
🚇transit🎟️longway🃏slips🧠poppyfield🍀the wall🧮beads🚩goals
rabbit holes
🔮fortune🗺️adventure📍memory map🖋️tattoo⚖️this or that🪨grade🏰pillowfort🌊drift🎵ipod🍬gummy🏛️palace
the collection
🎨moodboard📚library📖readvibe check💎nyc gems
tools & toys
🔪slicer✂️cutout🌱tend🔠fonts🧷atelier🪄make🖥️the desktop📰the issue
behind the curtain
👋about🎞️about, cinematic🎬making of📓process🧪lab📡status✍️guestbook📅calendar🧾receipt📣promo💌dedicate🧭codemap👁️read my eye🗃️shelf
0/50
the wind — cat stevens
paused