A Doom-like first-person shooter rendered entirely out of coloured ASCII characters, set in a two-storey shopping mall. Runs in any modern browser — no build step, no dependencies, no server required.
Open index.html and click CLICK TO ENTER THE MALL.
git clone <this repo> && cd asciifps
xdg-open index.html # or: npm run serve -> http://localhost:8080
A single hand-built level, src/mall.js, laid out as a real shopping centre:
- Level 1 — the concourse. Marble floor under a glazed atrium roof, with a fountain you can paddle in, planters, benches and kiosks.
- Level 2 — the upper terrace, ringed by a brass-capped glass balustrade that looks down over the atrium.
- Two moving escalators (one up, one down) that carry you whether you walk or not, plus a grand staircase for when you'd rather take the stairs.
- 18 signed zones, every one of them walkable: PIXEL PALACE (arcade), TOYBOX, PAGE TURNER, MEGAMART, PEAK GEAR, WELLCO pharmacy, BEAN THERE coffee, the RESTROOMS, VOLTZ electronics, VINYL VAULT, LUXE, CINE NINE, and a food court with three stalls and tables.
Each store has its own palette, floor texture, shelving and an illuminated fascia sign drawn from a 3×5 pixel font, so you can read the shopfronts as you approach. Shoppers wander the concourse; rogue security drones and runaway shopping carts do not appreciate your presence.
W A S D |
move / strafe |
| mouse | look |
Shift |
run |
click / Space |
fire |
R |
cycle render resolution |
Tab |
floorplan + store directory |
M |
toggle mini-map |
F |
fullscreen |
Esc |
release the mouse / pause |
Enter |
restart after dying |
Pointer lock is optional — if the browser refuses it, the arrow keys still steer.
R, or the buttons on the title screen. All three hold 60 fps on a 1280×720 canvas.
| mode | grid (at 1280×720) | look |
|---|---|---|
| LOW | ~100 × 28 cells | chunky, high-contrast, very "terminal" |
| HIGH | ~200 × 56 cells | the default sweet spot |
| ULTRA | ~300 × 84 cells | fine detail, readable signage from further off |
The world is not a grid of blocks — it is a height field. Every map cell carries
a floor height, a ceiling height, a material and a ceiling material. floor >= ceiling
means "solid wall". That one representation buys the whole mall:
| feature | how it is expressed |
|---|---|
| stairs / escalator ramps | floor height interpolated across a run of cells |
| the upper terrace | a large region whose floor sits at 3.6 |
| balustrades | floor height just above stepping range, so you see over but can't climb |
| shopfront headers | a cell whose ceiling drops to 2.6, which renders as a fascia |
| counters, racks, planters | one-cell floor bumps |
| the fountain | a rim you can step onto around a shallow basin |
renderWorld() in src/engine.js casts one DDA ray per screen column and walks the
cells front to back, painting four kinds of band into the cell buffer — the floor of
the cell being crossed, its ceiling, the riser where the floor steps up, and the
soffit where the ceiling steps down — clipping each against a top/bottom scanline
range that only ever shrinks. A column terminates when it meets a solid cell or the
two clip bounds cross. Floor and ceiling spans are perspective-correct: each row
inverts the projection to recover the exact world position, so textures follow the
ground properly.
Shading is where the ASCII comes in. Surface patterns (tile joints, brickwork,
escalator treads that scroll with time, ceiling grids, skylight glazing) supply the
structural glyphs; everywhere they leave a blank, a classic brightness ramp
.:-=+*#%@ fills in, so glyph density carries the lighting while the colour carries
the material. Risers may declare a cap colour, which is what turns a balustrade
into a dark panel with a bright brass handrail running up the escalator.
Sprites (shoppers, drones, carts, plants, pickups) and the signage are billboards sampled through the same cell buffer against a per-column depth buffer.
Presentation is one canvas: each row is run-length batched into fillRect calls for
the backgrounds and fillText calls for runs of same-coloured glyphs, with colours
quantised to 5 bits per channel so the runs actually coalesce.
index.html page shell, title screen, resolution buttons
src/art.js 3x5 signage font, sprite sheets, the HUD weapon
src/mall.js materials, surface patterns, and the mall itself
src/engine.js ASCII cell renderer + the height-field raycaster
src/game.js player physics, escalators, entities, combat, HUD, main loop
tools/test.js headless functional tests
npm install # playwright, only needed for the tests
npm test
Drives the real game loop in headless Chromium and asserts the level actually works — that the up escalator carries you to level 2 and leaves you on it, that the down escalator returns you, that the staircase climbs and descends, that the balustrade stops you falling into the atrium, that hitscan fire kills a drone, and that every one of the 18 zones is reachable on foot from the spawn (a flood fill over the height field using the player's own step-up and headroom rules).
- Desktop browsers only — there are no touch controls.
- Because the level is one height field, there is no geometry above other geometry: the upper terrace is a raised deck around the atrium rather than a floor with rooms beneath it. Everything else about the mall follows from that constraint.