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
DDglobal 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
overlapsand separateresolve/blockMovementsteps, 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_VARIANTSat 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.