FarmHeart

Procedural · Generation · Content · 10 min read · October 7, 2026

Procedural Content Generation for Browser Games

Procedural content generation (PCG) creates game content algorithmically instead of by hand. Terrain, dungeons, loot tables, NPC names, quest lines, and weather patterns can all be generated from rules and randomness. For browser games, PCG is especially valuable because it lets a small team create infinite content without shipping massive asset files.

The best procedural generation feels handcrafted. The trick is constraints: not "random terrain" but "terrain that always has a river, exactly two mountain passes, and a sheltered valley for the starting village."

Noise-Based Terrain

Perlin noise and Simplex noise generate smooth, natural-looking random values across 2D or 3D space. Use them for terrain heightmaps, biome distribution, cave systems, and cloud patterns.

// Simple terrain generation with layered noise
function generateTerrain(width, height, seed) {
  const noise = new SimplexNoise(seed);
  const terrain = [];

  for (let y = 0; y < height; y++) {
    terrain[y] = [];
    for (let x = 0; x < width; x++) {
      // Layer multiple noise frequencies
      let elevation = 0;
      elevation += noise.noise2D(x * 0.01, y * 0.01) * 1.0;  // large features
      elevation += noise.noise2D(x * 0.05, y * 0.05) * 0.3;  // medium detail
      elevation += noise.noise2D(x * 0.15, y * 0.15) * 0.1;  // fine detail

      // Classify biome by elevation
      if (elevation < -0.3) terrain[y][x] = 'water';
      else if (elevation < 0.0) terrain[y][x] = 'sand';
      else if (elevation < 0.5) terrain[y][x] = 'grass';
      else if (elevation < 0.8) terrain[y][x] = 'forest';
      else terrain[y][x] = 'mountain';
    }
  }
  return terrain;
}

The magic is in layering. A single noise function produces bland terrain. Three frequencies layered together (large landmasses + medium hills + fine detail) create terrain that looks geographically plausible.

Dungeon Generation

For games with interior spaces (dungeons, buildings, caves), the most common approach is the BSP (Binary Space Partitioning) algorithm:

  1. Start with a large rectangle (the dungeon bounds).
  2. Split it randomly into two halves (horizontal or vertical cut).
  3. Recursively split each half until rooms are the desired minimum size.
  4. Place a room of random size within each leaf partition.
  5. Connect adjacent rooms with corridors.

This guarantees every room is reachable and the layout is reasonable. For more organic caves, use cellular automata: start with random noise, then repeatedly apply a rule like "a cell becomes wall if it has 5+ wall neighbors." After 4-5 iterations, natural-looking cave systems emerge.

Loot Tables and Item Generation

Weighted random selection is the foundation of loot systems. Each item has a weight (relative probability), and you roll against the total weight to pick one:

function weightedRandom(table) {
  const total = table.reduce((s, item) => s + item.weight, 0);
  let roll = Math.random() * total;
  for (const item of table) {
    roll -= item.weight;
    if (roll <= 0) return item;
  }
  return table[table.length - 1];
}

const lootTable = [
  { name: 'Common Seed', weight: 60, rarity: 'common' },
  { name: 'Silver Hoe', weight: 25, rarity: 'uncommon' },
  { name: 'Golden Watering Can', weight: 10, rarity: 'rare' },
  { name: 'Ancient Tractor', weight: 4, rarity: 'epic' },
  { name: 'Mythic Scarecrow', weight: 1, rarity: 'legendary' },
];

For generated items with random properties (damage, durability, enchantments), use a two-stage system: first pick the base item from the loot table, then roll each property within the item's allowed ranges.

Name Generation

Procedural names for NPCs, towns, and items use Markov chains trained on real names. A second-order Markov chain looks at the last two characters to predict the next one, producing names that sound plausible without being real:

class NameGenerator {
  constructor(trainingNames) {
    this.chains = {};
    for (const name of trainingNames) {
      const padded = '^^' + name.toLowerCase() + '$';
      for (let i = 2; i < padded.length; i++) {
        const key = padded.slice(i - 2, i);
        if (!this.chains[key]) this.chains[key] = [];
        this.chains[key].push(padded[i]);
      }
    }
  }

  generate() {
    let name = '^^';
    while (true) {
      const key = name.slice(-2);
      const options = this.chains[key];
      if (!options) break;
      const next = options[Math.floor(Math.random() * options.length)];
      if (next === '$') break;
      name += next;
    }
    return name.slice(2, 3).toUpperCase() + name.slice(3);
  }
}

Quest Generation

Procedural quests use grammar-based generation. Define quest templates as patterns with variable slots:

Fill the slots with context-appropriate values: a fishing village gives fish-related fetch quests, a mountain town gives mining quests. Difficulty scales with the player's level by adjusting quantities, enemy types, and distance.

Seeded Randomness

Use seeded random number generators so the same seed always produces the same world. This enables multiplayer (all clients generate the same terrain from a shared seed), save/load (store only the seed, not the entire world), and debugging (reproduce exact scenarios).

// Simple seeded PRNG (mulberry32)
function mulberry32(seed) {
  return function() {
    seed |= 0; seed = seed + 0x6D2B79F5 | 0;
    let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

Balancing Procedural Content

Related Articles

Ready to Play?

Jump into FarmHeart — free, no download, runs in your browser.

Play FarmHeart