How to Make a Roguelike Game
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 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.htmlfile.
The 4-Step Blueprint
From an empty script to descending infinitely generated dungeon floors.
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.
>) in the center of the last room so every floor is guaranteed solvable.
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.
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.
localStorage.
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.
Vibe Prompt Configurator
Customize your dungeon aesthetic, upgrade system, and permadeath rules. Copy the generated prompt directly into Claude, Cursor, or Gemini.
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.
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.
<!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 & bump-attack enemies. Reach the cyan stairs (>)!</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>
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."
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
"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
"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
"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."