Skip to content
Alvaro Sarria
Menu
← All work

Dungeon Dweller

Endless top-down dungeon survival game on a hand-written fixed-timestep engine — vanilla JavaScript, DOM-rendered, zero dependencies.

Problem

I wanted to understand what a framework like React is actually abstracting away — so I built a real-time game with no framework, no engine, and no external libraries at all.

Approach

A top-down arena survival game: you hold a spear in a stone dungeon while skeletons and the occasional armoured brute spawn around you and close in. There is no win condition and no end of the dungeon — enemies keep coming and the run ends at zero health, so your result is the score, kill count and survival time on the game-over card. There is no bundler, no dependency to install and no package.json; the browser loads the source files as they are written.

Architecture

Three unusually thin layers:

  • HTML declares the DOM the game moves around — the arena, the player, the HUD, the overlay, and the <audio> elements that act as the sound bank
  • CSS supplies the artwork and the physical dimensions of every entity, plus sprite selection and the walk-cycle animation. JavaScript never sets a sprite or a width; it measures what CSS produced and flips attributes CSS reacts to
  • JavaScript owns simulation only — positions, collision, damage, timers, input — behind a single DD global with an explicit export list, because classic scripts run with no build step and no module-aware server

Key decisions

  • A fixed 16.67 ms timestep with an accumulator, clamped to 250 ms of real time per frame. Physics is identical on a 60 Hz laptop and a 144 Hz monitor, and returning to a backgrounded tab caps the debt at 15 steps instead of fast-forwarding the run
  • Collision is split into a side-effect-free overlaps and separate resolve / blockMovement steps, which is what stops attacking from displacing bodies and stops an enemy shoving you into a lava pool
  • Scenery positions are functions of arena size, so a window resize re-lays out the field instead of reloading and destroying the run
  • Every balance number lives in CONFIG / ENEMY_VARIANTS at the top of one file, so tuning the game touches one screen of code rather than the loop
  • DOM writes are cached and sprite state is driven by CSS attribute selectors rather than per-frame inline styles — the dominant per-frame cost in a DOM-rendered game

Results

A playable endless run with a spear strike and a mana-costing fireball, two enemy types on the same class with different numbers, timed and pressure-based spawning placed clear of the player, lava and rocks that block movement for both sides, and a HUD tracking health, mana, score and time. Health only comes back from kills, which is the whole tension: retreating indefinitely is not a strategy.

What I’d do differently

I’d move rendering to <canvas> for anything beyond this scope — hand-rolled DOM manipulation is a good learning exercise but doesn’t scale past a small game. The walk-cycle frames are also around 238×356 but display at 40×90, so the sprite set is far heavier than it needs to be, and there are still no touch controls and no mute button even though the audio layer supports one.