Back to Articles
2026 / 02
| 4 min read

Deep Dive: Building a Canada Goose Simulator

How a deadpan text adventure about mild chaos became the BBS system's most extensively tested game—79 tests, procedural honk audio, and goose migration instead of game over.

emulator games text-adventure testing audio

Deep Dive: Building a Canada Goose Simulator

How a deadpan text adventure about mild chaos became the BBS system’s most extensively tested game


So somewhere around Day 12 of this project, I found myself debugging why a virtual goose wasn’t properly intimidating virtual joggers. As you do.

The Canada Goose Simulator started as a joke — “what if we had a text adventure where you’re just… a goose?” — but like all the best jokes, it got out of hand in the most wonderful way. By the time I was done, I had a 6,000+ line game with 79 tests, procedural honk audio generation, and a migration system that teleports dead geese to random Canadian towns instead of showing a game over screen.

(Because geese don’t die. They migrate. That’s just science.)

The Core Design: Mild Chaos Simulator

The premise is simple: you’re a Canada Goose. You have 20 days to cause mild chaos across six zones. You can achieve victory by completing any of five goals:

// goals.ts - The five paths to goose glory
export const VICTORY_GOALS = {
  AGENT_OF_CHAOS: { target: 50, description: 'Disturb 50 NPCs' },
  FOOD_HOARDER: { target: 75, description: 'Collect 75 food items' },
  NEST_BUILDER: { target: 15, description: 'Gather 15 nesting materials' },
  GOSLING_GUARDIAN: { target: 3, description: 'Raise 3 goslings to adulthood' },
  TERRITORIAL_TYRANT: { target: 6, description: 'Claim all 6 territories' }
};

The design document grew to 761 lines. For a text adventure about being a goose. I may have gotten carried away.

The Honking System

Here’s where things got interesting. Honking isn’t just a button you press — it’s a nuanced interaction system with diminishing returns and NPC-specific responses.

// actions.ts - Honk mechanics
export function processHonk(game: GameState, target?: string): ActionResult {
  const timeUnits = 2; // Honking costs 2 time units
  
  // Find NPCs in range (max 2 affected per honk)
  const affectedNPCs = game.currentZone.npcs
    .filter(npc => !npc.recentlyHonkedAt)
    .slice(0, 2);
  
  // Diminishing returns - NPCs get desensitized
  affectedNPCs.forEach(npc => {
    npc.honkResistance = (npc.honkResistance || 0) + 1;
    npc.recentlyHonkedAt = game.currentTick;
    
    // Some NPCs... enjoy being honked at
    if (NPC_ENJOYS_HONKING.includes(npc.type)) {
      // Children, elderly folks, and other waterfowl
      // may give you food instead of running away
      if (Math.random() < 0.3) {
        return { type: 'gift', item: 'bread_crumb' };
      }
    }
  });
  
  return { affectedCount: affectedNPCs.length, timeSpent: timeUnits };
}

The key insight was that not all NPCs should fear the honk. Children think it’s hilarious. Elderly folks might toss you bread. Other waterfowl? They honk back. It’s mutual respect.

Procedural Goose Audio

Of course, a goose game needs goose sounds. I built an audio generator that synthesizes seven distinct effects:

// audio.ts - GooseAudioGenerator
export class GooseAudioGenerator {
  private ctx: AudioContext;
  
  generateHonk(intensity: number = 1.0): AudioBuffer {
    // Goose honks are surprisingly complex:
    // - Fundamental frequency around 200-400Hz
    // - Strong harmonics at 2x and 3x fundamental
    // - Characteristic "break" in the middle
    // - Nasal resonance (formant around 1000Hz)
    
    const duration = 0.3 + (intensity * 0.2);
    const samples = this.ctx.sampleRate * duration;
    const buffer = this.ctx.createBuffer(1, samples, this.ctx.sampleRate);
    const data = buffer.getChannelData(0);
    
    for (let i = 0; i < samples; i++) {
      const t = i / this.ctx.sampleRate;
      const envelope = this.honkEnvelope(t, duration);
      const fundamental = Math.sin(2 * Math.PI * 280 * t);
      const harmonic2 = 0.5 * Math.sin(2 * Math.PI * 560 * t);
      const harmonic3 = 0.25 * Math.sin(2 * Math.PI * 840 * t);
      
      // The characteristic honk "break"
      const break_factor = t > duration * 0.4 && t < duration * 0.6 
        ? 0.3 : 1.0;
      
      data[i] = envelope * break_factor * (fundamental + harmonic2 + harmonic3);
    }
    
    return buffer;
  }
  
  generateFlap(): AudioBuffer { /* wing flutter ~3-5Hz modulated noise */ }
  generateWaddle(): AudioBuffer { /* footstep with webbed foot squelch */ }
  generateSplash(): AudioBuffer { /* water entry/exit */ }
  generatePeck(): AudioBuffer { /* sharp attack, short decay */ }
  generateVictory(): AudioBuffer { /* triumphant honk sequence */ }
  generateDamage(): AudioBuffer { /* distressed honk */ }
}

The honk break in the middle — that brief quieter moment — is what makes a honk sound like a honk rather than just a tone. It’s the same phenomenon as the “crack” in a rooster’s crow. Birds are weird.

The Gosling System

Raising goslings turned out to be the most complex subsystem. You can find eggs, protect nests, and raise baby geese who follow you around and eventually grow up:

// goslings.ts - Parenting mechanics
export interface Gosling {
  id: string;
  name: string;
  age: number; // days
  hunger: number; // 0-100
  bond: number; // how attached they are to you
  personality: 'bold' | 'timid' | 'mischievous';
}

export function updateGosling(gosling: Gosling, parentZone: Zone): GoslingUpdate {
  gosling.hunger += 10; // They're always hungry
  
  if (gosling.hunger > 80) {
    gosling.bond -= 5; // Hungry goslings lose faith in you
  }
  
  // Mischievous goslings cause trouble
  if (gosling.personality === 'mischievous' && Math.random() < 0.2) {
    const victim = parentZone.npcs[Math.floor(Math.random() * parentZone.npcs.length)];
    if (victim) {
      return { 
        event: 'gosling_mischief',
        message: `${gosling.name} honks at ${victim.name} without your approval!`
      };
    }
  }
  
  // Adulthood at age 10 (days, not years)
  if (gosling.age >= 10) {
    return { event: 'gosling_matured', gosling };
  }
  
  return { event: 'none' };
}

Each gosling has a personality that affects gameplay. Timid ones hide during conflicts. Bold ones try to help (they can’t, but they try). Mischievous ones cause chaos independently, which either helps or hurts your goals.

Migration (Not Death)

When your goose takes too much damage, you don’t die. You “migrate” to a random Canadian town and lose some progress. I added 100 real Canadian town names:

// data.ts - Migration destinations
export const MIGRATION_TOWNS = [
  'Moose Jaw, Saskatchewan',
  'Medicine Hat, Alberta',
  'Flin Flon, Manitoba',
  'Dildo, Newfoundland',
  'Climax, Saskatchewan',
  'Head-Smashed-In Buffalo Jump, Alberta',
  'Swastika, Ontario', // Yes, this is a real place. Founded 1906.
  'Saint-Louis-du-Ha! Ha!, Quebec',
  'Punkeydoodles Corners, Ontario',
  'Crotch Lake, Ontario',
  // ... 90 more
];

export function handleDeath(game: GameState): MigrationResult {
  const destination = MIGRATION_TOWNS[Math.floor(Math.random() * MIGRATION_TOWNS.length)];
  
  game.player.health = 100;
  game.player.foodCount = Math.floor(game.player.foodCount * 0.5); // Lose half your food
  game.currentZone = ZONES.PARK; // Reset to starting zone
  
  return {
    message: `You feel a sudden urge to fly south. Very south. To ${destination}.`,
    followUp: `After a long journey, you return to the park, somewhat wiser.`
  };
}

(The town names are all real. Canada is a special place.)

The Testing Marathon

Here’s where the engineering brain took over. I wrote 79 tests covering:

  • All 5 victory conditions
  • All failure conditions (death/migration, timeout, quit)
  • Game state consistency across saves
  • Every command and zone transition
  • Audio event triggers
  • Stress tests (500 random commands, full 20-day cycles)
// game.test.ts - A sampling of the chaos
describe('Canada Goose Simulator', () => {
  describe('Victory Conditions', () => {
    it('should award Agent of Chaos victory at 50 disturbed NPCs', async () => {
      const game = new GooseGame();
      
      // Methodically honk at exactly 50 NPCs
      for (let i = 0; i < 50; i++) {
        await game.processInput('travel park'); // Reset zone
        await game.processInput('honk');
      }
      
      expect(game.state.victory).toBe('AGENT_OF_CHAOS');
      expect(game.state.ended).toBe(true);
    });
    
    it('should track gosling maturation correctly', async () => {
      const game = new GooseGame();
      game.state.goslings = [
        createGosling('Gerald', 'mischievous'),
        createGosling('Brenda', 'timid'),
        createGosling('Steve', 'bold')
      ];
      
      // Advance 10 days
      for (let day = 0; day < 10; day++) {
        await game.advanceDay();
      }
      
      expect(game.state.maturedGoslings).toBe(3);
      expect(game.state.victory).toBe('GOSLING_GUARDIAN');
    });
  });
  
  describe('Stress Tests', () => {
    it('should handle 500 random commands without state corruption', async () => {
      const game = new GooseGame();
      const commands = ['honk', 'travel park', 'travel pond', 'peck', 'fly', 'look', 'status'];
      
      for (let i = 0; i < 500; i++) {
        const cmd = commands[Math.floor(Math.random() * commands.length)];
        await game.processInput(cmd);
        
        // State should never be corrupted
        expect(game.state.player.health).toBeGreaterThanOrEqual(0);
        expect(game.state.player.health).toBeLessThanOrEqual(100);
        expect(game.state.day).toBeLessThanOrEqual(20);
      }
    });
  });
});

79 tests. For a goose game. I regret nothing.

The Humor System

A deadpan game needs deadpan writing. I built a humor system that generates contextually appropriate dry observations:

// humor.ts - Deadpan commentary
export const HONK_OBSERVATIONS = [
  "The {npc} looks at you with a mixture of fear and resignation.",
  "Your honk echoes across the {zone}. A distant goose honks back.",
  "The {npc} had plans today. They don't anymore.",
  "You assert your dominance. The {npc} accepts this.",
  "The laws of man do not apply to geese.",
  "You honk. It's not personal. It's not NOT personal either.",
  "The {npc} will remember this. You will not.",
];

export const MIGRATION_MESSAGES = [
  "You have migrated to {town}. This is a lateral move.",
  "The geese of {town} regard you with suspicion. You fit right in.",
  "You arrive in {town}. It's somehow exactly as you expected.",
  "The migration was long. {town} doesn't seem worth it.",
];

The goal was to match the energy of Untitled Goose Game but in text form — chaos delivered with a completely straight face.

What I Learned

Building a silly game properly taught me a few things:

  1. Absurdist premise, serious implementation — The goose game is ridiculous, but the code is clean. Comedy doesn’t excuse bad engineering.

  2. Audio synthesis is addictive — Once you can make a goose honk, you want to make it waddle. Then splash. Then peck. Suddenly you’re researching waterfowl acoustics at 2 AM.

  3. Comprehensive testing enables fearless iteration — With 79 tests, I could refactor the honk system three times without worrying about breaking gosling maturation.

  4. Canadian town names are a gift — Seriously, look them up. “Head-Smashed-In Buffalo Jump” is a UNESCO World Heritage Site.

Playing the Game

The Canada Goose Simulator is available on the BBS at 555-4667 (555-GOOS). Dial in, accept your goose nature, and cause some mild chaos.

And if you achieve all five victory conditions? There might be a Golden Goose Trophy waiting for you. But you didn’t hear that from me.


See also: Deep Dive: Bell 103 Audio Modem — for when you need to understand HOW you’re connecting to honk at virtual NPCs.