Skip to main content

How to Make a Roguelike Game

Copied to clipboard!
No manual dungeon map editors required

How to Vibe Code a Procedural Roguelike Game

Stay in your creative flow. Direct AI models to generate BSP dungeon rooms, turn-based tactical movement, field-of-view fog, permadeath stakes, and synergistic item pools in a single HTML5 Canvas file in under 25 minutes.

Core Stack

HTML5 Canvas + JS

Generation

Procedural Rooms

AI Partner

Claude / Gemini

Target Time

15–25 Mins

The Philosophy

The Vibe Coding Roguelike Philosophy

Roguelikes are defined by randomly generated levels, permadeath, and tactical top-down gameplay where every run tells a new story. Historically, building a roguelike meant weeks of debugging room-corridor carving algorithms, raycast field-of-view (FOV), and A* monster pathfinding before you ever designed an upgrade. Vibe coding flips the script: you act as the Dungeon Architect & Systems Designer, describing the dungeon vibe, upgrade synergies, and risk-reward loops while AI writes the procedural generator and turn scheduler.

Old-School Dungeon Grind

  • Spawning unreachable rooms with broken corridor-carving math.
  • Writing hundreds of lines of shadowcasting boilerplate just to hide unexplored tiles.
  • Hardcoding rigid item effects that cannot stack or synergize during a run.
  • Getting lost in engine setup before testing whether bump-to-attack combat feels snappy.

The Vibe Coder Way

  • Instant HTML5 Canvas tile dungeon with connected rooms, stairs, loot chests, and monsters.
  • Describe the run loop: "Turn-based top-down dungeon crawler with FOV fog, permadeath, and 3-choice level-up upgrades."
  • Expand item pools conversationally: "Add a vampiric dagger relic and poison traps on Floor 3+."
  • Playtest run balance immediately in your browser and share a single index.html file.
Execution

The 4-Step Blueprint

From an empty script to descending infinitely generated dungeon floors.

Step 01

Prompt the Procedural Map Generator

Start by asking your AI for a 2D tile grid generator that places non-overlapping rectangular rooms and connects their centers with L-shaped corridors.

πŸ’‘ Pro Vibe Tip: Always spawn the player in the center of the first room and the stairs down (>) in the center of the last room so every floor is guaranteed solvable.
Step 02

Wire Turn-Based Movement & Bump Combat

Keep the turn loop crisp: enemies only move and attack right after the player takes a step. Stepping into an enemy tile triggers a melee strike with floating damage numbers.

πŸ”‘ Key Constraint: Tell the AI: "Include a scrolling combat log below the canvas so the player can read every hit, heal, and floor transition."
Step 03

Add Item Pools, Upgrades & Permadeath

Layer in the roguelike magic: health potions, gold chests, field-of-view fog of war, and a 3-card upgrade draft every time the player descends a floor or levels up.

✨ Iteration Secret: When HP hits 0, wipe the current run state (permadeath) but save the deepest floor reached and total gold to localStorage.
Step 04

Playtest & Release on Itch.io or Web

Test monster scaling across Floors 1 to 10, tune potion drop rates so survival feels tense, and upload your single-file index.html to itch.io or GitHub Pages.

πŸ† The Grand Payoff: Infinite replayability in a zero-dependency browser roguelike that boots in 50 milliseconds.
Interactive Tool

Vibe Prompt Configurator

Customize your dungeon aesthetic, upgrade system, and permadeath rules. Copy the generated prompt directly into Claude, Cursor, or Gemini.

Special Feature Vibe
Optional custom details added straight into your master prompt.
Generated Master Prompt
Ready to paste into Claude, Gemini, or ChatGPT Single-file HTML5 Canvas ready
Game Design Spec

Essential Elements of a Roguelike Game

What makes a Roguelike endlessly replayable? Make sure your dungeon crawler nails these six foundational pillars:

  • Randomly Generated Levels

    Every run and every floor must generate a fresh layout of rooms, corridors, monsters, and loot so players rely on adaptability rather than memorization.

  • High-Stakes Permadeath

    When HP hits zero, the run is over and the player starts fresh from Floor 1β€”making every low-health hallway decision genuinely thrilling.

  • Top-Down or Isometric Perspective

    Render a clear tactical grid from above so players can read enemy spacing, chokepoints, traps, and escape routes at a glance.

  • Turn-Based Tactical Combat & AI

    Time advances only when the player actsβ€”allowing careful positioning, bump-to-attack melee calculations, and predictable monster pursuit.

  • Synergistic Upgrades & Item Pools

    Organize items and level-up perks into rarity pools (weapons, armor, potions, passive relics) that combine into powerful run-defining builds.

  • Field-of-View (FOV) & Exploration

    Shroud unvisited rooms in darkness and dim previously explored tiles outside the player's torch radius to sustain suspense around every corner.

Dungeon Balance Checklist Before Shipping: Guarantee a valid path from spawn to the exit stairs on every floor, surface clear combat feedback in a message log, and support both WASD/Arrow keys and click-to-step controls.
Starter Template

The "Golden" Single-File HTML5 Roguelike Game

Save this as index.html and open it in any browser to play a complete procedural dungeon crawler with room generation, FOV fog of war, bump combat, potions, and permadeath.

index.html
HTML5 Canvas + Vanilla JS
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vibe Rogue: Depths of Ember</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; font-family: system-ui, -apple-system, monospace; }
    body {
      background: #070a12;
      color: #f8fafc;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
      padding: 14px;
    }
    .rogue-wrap {
      background: #111827;
      border: 1px solid #1f2937;
      border-radius: 18px;
      padding: 16px;
      max-width: 640px;
      width: 100%;
      box-shadow: 0 20px 50px rgba(0,0,0,0.65);
    }
    .hud {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      gap: 8px;
      margin-bottom: 10px;
    }
    .stat {
      background: #0b0f19;
      border: 1px solid #1e293b;
      border-radius: 10px;
      padding: 7px;
      text-align: center;
    }
    .stat-k { font-size: 0.66rem; color: #94a3b8; text-transform: uppercase; }
    .stat-v { font-size: 0.98rem; font-weight: 800; color: #f8fafc; margin-top: 2px; }
    canvas {
      display: block;
      width: 100%;
      height: auto;
      background: #030712;
      border-radius: 12px;
      border: 1px solid #1e293b;
    }
    .log-bar {
      margin-top: 10px;
      background: #0b0f19;
      border: 1px solid #1e293b;
      border-radius: 10px;
      padding: 10px 12px;
      font-size: 0.82rem;
      color: #cbd5e1;
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 10px;
    }
    .btn {
      background: #10b981;
      color: #052e16;
      border: none;
      border-radius: 8px;
      padding: 7px 12px;
      font-weight: 800;
      cursor: pointer;
      white-space: nowrap;
    }
  </style>
</head>
<body>
  <div class="rogue-wrap">
    <div class="hud">
      <div class="stat"><div class="stat-k">Floor</div><div class="stat-v" id="flEl">1 (Best: 1)</div></div>
      <div class="stat"><div class="stat-k">HP</div><div class="stat-v" id="hpEl" style="color:#f43f5e;">25 / 25</div></div>
      <div class="stat"><div class="stat-k">ATK / DEF</div><div class="stat-v" id="atkEl" style="color:#38bdf8;">5 / 1</div></div>
      <div class="stat"><div class="stat-k">Level (XP)</div><div class="stat-v" id="lvEl" style="color:#a855f7;">1 (0/20)</div></div>
      <div class="stat"><div class="stat-k">Gold</div><div class="stat-v" id="gdEl" style="color:#f59e0b;">0</div></div>
    </div>
    <canvas id="dungeon" width="600" height="384"></canvas>
    <div class="log-bar">
      <span id="logEl">Use WASD or Arrow Keys to move &amp; bump-attack enemies. Reach the cyan stairs (&gt;)!</span>
      <button class="btn" id="newRunBtn">New Run</button>
    </div>
  </div>

  <script>
    const canvas = document.getElementById('dungeon');
    const ctx = canvas.getContext('2d');
    const COLS = 25, ROWS = 16, TILE = 24;
    let map = [], seen = [], monsters = [], items = [];
    let stairs = { x: 1, y: 1 };
    let hero = {}, floor = 1, bestFloor = Number(localStorage.getItem('vibe_rogue_best') || 1);

    function startNewRun() {
      floor = 1;
      hero = { x: 1, y: 1, hp: 25, maxHp: 25, atk: 5, def: 1, lv: 1, xp: 0, nextXp: 20, gold: 0, dead: false };
      generateFloor();
      log('Entered Floor 1. Permadeath is activeβ€”tread carefully!');
      render();
    }

    function generateFloor() {
      map = Array.from({ length: ROWS }, () => Array(COLS).fill(1));
      seen = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
      monsters = [];
      items = [];
      const rooms = [];

      for (let i = 0; i < 22; i++) {
        const w = 3 + Math.floor(Math.random() * 4);
        const h = 3 + Math.floor(Math.random() * 3);
        const rx = 1 + Math.floor(Math.random() * (COLS - w - 2));
        const ry = 1 + Math.floor(Math.random() * (ROWS - h - 2));
        const overlap = rooms.some((r) => rx <= r.x + r.w && rx + w >= r.x - 1 && ry <= r.y + r.h && ry + h >= r.y - 1);
        if (!overlap) {
          rooms.push({ x: rx, y: ry, w, h, cx: Math.floor(rx + w / 2), cy: Math.floor(ry + h / 2) });
          for (let y = ry; y < ry + h; y++) {
            for (let x = rx; x < rx + w; x++) map[y][x] = 0;
          }
        }
      }

      for (let i = 1; i < rooms.length; i++) {
        let x = rooms[i - 1].cx, y = rooms[i - 1].cy;
        const tx = rooms[i].cx, ty = rooms[i].cy;
        while (x !== tx) { map[y][x] = 0; x += x < tx ? 1 : -1; }
        while (y !== ty) { map[y][x] = 0; y += y < ty ? 1 : -1; }
      }

      hero.x = rooms[0].cx;
      hero.y = rooms[0].cy;
      stairs = { x: rooms[rooms.length - 1].cx, y: rooms[rooms.length - 1].cy };

      rooms.slice(1).forEach((r, idx) => {
        if (Math.random() < 0.75) {
          const isElite = idx === rooms.length - 2 || Math.random() < 0.25;
          monsters.push({
            x: r.cx, y: r.cy,
            name: isElite ? 'Dread Knight' : 'Crypt Goblin',
            glyph: isElite ? 'K' : 'g',
            color: isElite ? '#f43f5e' : '#fb923c',
            hp: (isElite ? 12 : 7) + floor * 3,
            atk: (isElite ? 4 : 2) + floor,
            xp: isElite ? 14 : 8
          });
        }
        if (Math.random() < 0.65) {
          const types = ['potion', 'gold', 'sword'];
          const t = types[Math.floor(Math.random() * types.length)];
          items.push({ x: r.x + 1, y: r.y + 1, type: t });
        }
      });
    }

    function stepHero(dx, dy) {
      if (hero.dead) return;
      const nx = hero.x + dx, ny = hero.y + dy;
      if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS || map[ny][nx] === 1) return;

      const foe = monsters.find((m) => m.x === nx && m.y === ny);
      if (foe) {
        const dmg = Math.max(1, hero.atk + Math.floor(Math.random() * 2));
        foe.hp -= dmg;
        if (foe.hp <= 0) {
          monsters = monsters.filter((m) => m !== foe);
          hero.xp += foe.xp;
          hero.gold += 5 + floor * 2;
          log('βš”οΈ Slain ' + foe.name + '! (+' + foe.xp + ' XP)');
          if (hero.xp >= hero.nextXp) {
            hero.lv++;
            hero.xp -= hero.nextXp;
            hero.nextXp = Math.floor(hero.nextXp * 1.5);
            hero.maxHp += 6;
            hero.hp = hero.maxHp;
            hero.atk += 2;
            hero.def += 1;
            log('✨ LEVEL UP! Lv ' + hero.lv + ': Full Heal, +2 ATK, +1 DEF!');
          }
        } else {
          log('Hit ' + foe.name + ' for ' + dmg + ' dmg (' + foe.hp + ' HP left).');
        }
      } else {
        hero.x = nx; hero.y = ny;
        const itm = items.find((i) => i.x === nx && i.y === ny);
        if (itm) {
          items = items.filter((i) => i !== itm);
          if (itm.type === 'potion') {
            const heal = 10 + floor * 2;
            hero.hp = Math.min(hero.maxHp, hero.hp + heal);
            log('πŸ§ͺ Drank Crimson Potion (+' + heal + ' HP).');
          } else if (itm.type === 'gold') {
            const g = 15 + floor * 5;
            hero.gold += g;
            log('πŸ’° Found treasure chest (+' + g + ' Gold).');
          } else {
            hero.atk += 2;
            log('πŸ—‘οΈ Equipped Runed Blade (+2 ATK)!');
          }
        }
        if (hero.x === stairs.x && hero.y === stairs.y) {
          floor++;
          if (floor > bestFloor) {
            bestFloor = floor;
            localStorage.setItem('vibe_rogue_best', String(bestFloor));
          }
          hero.hp = Math.min(hero.maxHp, hero.hp + 5);
          generateFloor();
          log('πŸͺœ Descended to Floor ' + floor + '! (+5 HP recovery)');
          render();
          return;
        }
      }

      // Monster turns
      monsters.forEach((m) => {
        const dist = Math.hypot(hero.x - m.x, hero.y - m.y);
        if (dist <= 6) {
          if (Math.abs(hero.x - m.x) + Math.abs(hero.y - m.y) === 1) {
            const edmg = Math.max(1, m.atk - hero.def);
            hero.hp -= edmg;
            log('πŸ’₯ ' + m.name + ' strikes you for ' + edmg + ' dmg!');
            if (hero.hp <= 0) {
              hero.hp = 0; hero.dead = true;
              log('πŸ’€ PERMADEATH on Floor ' + floor + '! Click New Run to try again.');
            }
          } else {
            const sx = Math.sign(hero.x - m.x), sy = Math.sign(hero.y - m.y);
            const tryX = m.x + sx, tryY = m.y + sy;
            if (map[m.y][tryX] === 0 && !monsters.some((o) => o.x === tryX && o.y === m.y)) m.x = tryX;
            else if (map[tryY][m.x] === 0 && !monsters.some((o) => o.x === m.x && o.y === tryY)) m.y = tryY;
          }
        }
      });

      render();
    }

    function log(msg) { document.getElementById('logEl').textContent = msg; }

    function render() {
      ctx.fillStyle = '#030712';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      for (let y = 0; y < ROWS; y++) {
        for (let x = 0; x < COLS; x++) {
          const dist = Math.hypot(hero.x - x, hero.y - y);
          const vis = dist <= 5.2;
          if (vis) seen[y][x] = true;
          if (!seen[y][x]) continue;

          ctx.fillStyle = map[y][x] === 1 ? (vis ? '#1e293b' : '#0f172a') : (vis ? '#0f172a' : '#070b14');
          ctx.fillRect(x * TILE, y * TILE, TILE - 1, TILE - 1);
        }
      }

      // Stairs
      if (seen[stairs.y][stairs.x]) {
        ctx.fillStyle = '#38bdf8';
        ctx.font = 'bold 18px monospace';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('>', stairs.x * TILE + 12, stairs.y * TILE + 12);
      }

      // Items
      items.forEach((i) => {
        if (Math.hypot(hero.x - i.x, hero.y - i.y) <= 5.2) {
          ctx.fillStyle = i.type === 'potion' ? '#10b981' : (i.type === 'gold' ? '#f59e0b' : '#38bdf8');
          ctx.font = 'bold 15px monospace';
          ctx.fillText(i.type === 'potion' ? '!' : (i.type === 'gold' ? '$' : '/'), i.x * TILE + 12, i.y * TILE + 12);
        }
      });

      // Monsters
      monsters.forEach((m) => {
        if (Math.hypot(hero.x - m.x, hero.y - m.y) <= 5.2) {
          ctx.fillStyle = m.color;
          ctx.beginPath(); ctx.roundRect(m.x * TILE + 3, m.y * TILE + 3, TILE - 7, TILE - 7, 5); ctx.fill();
          ctx.fillStyle = '#030712';
          ctx.font = 'bold 13px monospace';
          ctx.fillText(m.glyph, m.x * TILE + 12, m.y * TILE + 13);
        }
      });

      // Hero
      ctx.fillStyle = hero.dead ? '#64748b' : '#10b981';
      ctx.beginPath(); ctx.roundRect(hero.x * TILE + 2, hero.y * TILE + 2, TILE - 5, TILE - 5, 6); ctx.fill();
      ctx.fillStyle = '#022c22';
      ctx.font = 'bold 14px monospace';
      ctx.fillText('@', hero.x * TILE + 12, hero.y * TILE + 12);

      document.getElementById('flEl').textContent = floor + ' (Best: ' + bestFloor + ')';
      document.getElementById('hpEl').textContent = hero.hp + ' / ' + hero.maxHp;
      document.getElementById('atkEl').textContent = hero.atk + ' / ' + hero.def;
      document.getElementById('lvEl').textContent = hero.lv + ' (' + hero.xp + '/' + hero.nextXp + ')';
      document.getElementById('gdEl').textContent = hero.gold;
    }

    window.addEventListener('keydown', (e) => {
      const mapKeys = { ArrowUp:[0,-1], KeyW:[0,-1], ArrowDown:[0,1], KeyS:[0,1], ArrowLeft:[-1,0], KeyA:[-1,0], ArrowRight:[1,0], KeyD:[1,0] };
      if (mapKeys[e.code]) {
        e.preventDefault();
        stepHero(mapKeys[e.code][0], mapKeys[e.code][1]);
      }
    });

    document.getElementById('newRunBtn').addEventListener('click', startNewRun);
    startNewRun();
  </script>
</body>
</html>
Troubleshooting

How to Debug Procedural Roguelikes Without Losing Your Vibe

When procedural maps spawn walled-off rooms or monsters stack on the same tile, use these three vibe fixes:

Guaranteed Room Connectivity

Never let the stairs spawn behind solid rock: "Connect every generated room[i] center to room[i-1] center with an L-shaped floor tunnel so all rooms belong to a single connected graph."

Collision-Free Monster Turns

Stop two enemies from merging onto the same grid coordinate: "Before moving a monster to (tryX, tryY), check that map[tryY][tryX] === 0 and no other living monster occupies (tryX, tryY)."

Clean Permadeath Reset

Ensure old monsters or buffs never bleed into the next run: "Encapsulate all run state inside a startNewRun() function that re-initializes hero stats, floor=1, and entity arrays from scratch."

Extensions

The Vibe Prompt Vault

Copy-paste prompts to add 3-card upgrade drafts, ranged spells, and boss floors to your roguelike.

πŸƒ Prompt: Add a "Pick 1-of-3 Synergistic Relics" Draft Modal on Level-Up
Prompt Template

"I have a working HTML5 Canvas roguelike in index.html. Whenever the player levels up or descends a floor, pause movement and display a 3-card 'Choose Your Blessing' modal drawn from a pool of 10 stackable roguelike upgrades (e.g., Vampiric Fangs: heal 2 HP on kill, Thorns Armor: reflect 3 dmg, Torchmaster: +2 FOV radius, Berserker: +4 ATK when below 40% HP)."

πŸ”₯ Prompt: Add Ranged Fireball & Frost Nova Spells with Cooldowns
Prompt Template

"Extend my turn-based HTML5 roguelike with 2 active spell hotkeys: pressing [1] casts 'Fireball' at the nearest visible monster within 4 tiles, and pressing [2] casts 'Frost Nova' which freezes all adjacent monsters for 2 turns. Track spell cooldowns in turns rather than real-time seconds."

πŸ‰ Prompt: Add a Throne Room Boss Encounter Every 5th Floor
Prompt Template

"Modify generateFloor() in my roguelike so that whenever floor % 5 === 0, it generates a grand pillared Throne Room Arena containing a Boss monster ('Ember Dragon') with a telegraphed 3x3 flame breath attack. Lock the exit stairs until the Boss is defeated."

We use cookies for ads and analytics.