GitHub - VorticonCmdr/lemmings: Lemmings in HTML-in-Canvas

GitHub

15 min read Original article โ†—

A modern, WebGL-enhanced reconstruction of the classic Lemmings game using the experimental HTML-in-Canvas API (layoutsubtree, drawElementImage, onpaint).

In this implementation, each Lemming is a real <div> child of a <canvas> element. This creates a DOM-based Entity-Component-System (ECS) where entities are rendered directly into the canvas every frame while retaining native mouse hit-testing and accessibility support on the drawn pixels.

โ–ถ Play now โ€” requires Chrome 148 to 154 with chrome://flags/#canvas-draw-element enabled (see Runtime Requirement below).


๐Ÿ’š Special thank you to


Motivation

I wanted to level up my agent coding skills and how to better learn than to practise? After recently reading about HTML-in-canvas I thought this might be something interesting to target and since I love playing Lemmings, DHTML Lemmings came to mind.

I really didn't expect to release something since Lemmings in HTML & canvas isn't exactly ground breaking but I soon discovered that there are still treasures buried in this old game.

There was no reconstruction of the EGA, CGA or Tandy version and no 2-player mode (which didn't exist in the DOS version)


โš ๏ธ Runtime Requirement

The experimental HTML-in-Canvas API is in origin trial and needs Chrome 148 to 154. It will not run on standard browsers, Safari, or Firefox (which will display an unsupported banner).

To run this project:

  1. Use Chrome 148 to 154 (or a Chromium build supporting the flag).
  2. Navigate to chrome://flags/#canvas-draw-element.
  3. Set the flag to Enabled and restart the browser.

๐Ÿš€ Running the Project

This project is fully client-side, bundled/served via Vite:

Open the printed http://localhost:5173 URL in Chrome with the flag enabled.

For a production build (a dist/ folder ready to serve statically, e.g. on GitHub Pages):

npm run build
npm run preview   # serve dist/ locally to sanity-check the build

๐ŸŽฎ Features

1. Single Player Mode

  • 346 Playable Levels: Every rating from the original DOS Lemmings, Oh No! More Lemmings, and the Holiday/Xmas Lemmings packs (Fun/Tricky/Taxing/Mayhem, Crazy/Wild/Wicked/Havoc/Tame, Flurry/Blizzard '93 & '94, Frost, Hail, Xmas '91 & '92) plus a 20-level Amiga 2-player pack.
  • Live .DAT Decoding: Terrain, placed objects, steel areas, and level metadata for every rating, including 2-player, are decoded directly from the original DOS/Amiga binary files (gamedata/<pack>/LEVEL<n>.DAT, gamedata/Amiga-Two-Player/Level NN.lvl, etc.) in the browser at play time โ€” no offline conversion step. See docs/gamedata.md.
  • 8 Original Skills: Full state machine implementations for Climber, Floater, Bomber (Exploder), Blocker, Builder, Basher, Miner, and Digger.
  • Classic UI & Controls: Interactive skill toolbar with skill counters, release-rate controllers (+/-), fast-forward (4x speed), pause, and a "nuke" button.
  • EGA / CGA / Tandy Video Modes: A pause-menu "Video Mode" setting recolors terrain, objects, and the lemming sprites themselves to match real EGA (per-level palette), CGA (native 2bpp, lemmings pack), or Tandy (fixed hardware palette, lemmings/oh-no-more-lemmings) hardware output โ€” VGA stays the default fixed palette. CGA "special" (VGASPEC-backed) levels are decoded via their own native CGASPEC<n>.DAT art rather than re-quantizing VGA art.
  • 4 Music Engines + Real DOS Sound Effects: A "Music Engine" setting switches between the Amiga ProTracker rotation, real per-pack AdLib (OPL2) music decoded from ADLIB.DAT, pre-rendered MP3 tracks, and real per-pack Tandy 3-voice PSG music decoded from TANDYSND.DAT.

2. 2-Player Lockstep Multiplayer

  • P2P Connection: Direct peer-to-peer multiplayer using WebRTC data channels via PeerJS.
  • Lockstep Synchronization: A fully synchronized local simulation. Instead of sharing state variables for hundreds of entities, peers exchange only tick-stamped player commands.
  • Input Delay Buffer: Commands are queued and executed with a fixed buffer (INPUT_DELAY_TICKS = 6 ticks, ~480ms) to hide network latency and keep the simulations perfectly aligned.

3. Level Editor

  • Live Terrain/Object Palette: Stamp and Objects tools are decoded straight from the original GROUND<n>O.DAT/VGAGR<n>.DAT graphic-style files โ€” no baked tile catalog. Placed pieces are real interactive HTML-in-Canvas entities (native click/drag hit-testing via drawElementImage), the same ECS pattern used for lemmings and decorations in real gameplay.
  • Grid Painting: Interactive canvas interface to paint terrain semantic values directly (Air, Soft, Soft Right, Soft Left, Steel, Water, Fire, Exit, Slicer).
  • Smart Brushes: An Exit tool that paints tall columns to ensure walking Lemmings collide with the exit trigger (sampled at mid-body, y+16).
  • Level Configurations: Configure starting parameters such as skill availability counts, release rates, saved percentage targets, time limits, and music.
  • Local Storage: Saves, loads, and manages draft playtests and custom levels in localStorage under lemmings.customLevels.v1.
  • Export/Import Real DOS Levels: "Export Level Packโ€ฆ"/"Import Levelโ€ฆ" convert custom levels to/from real, LZ-compressed DOS LEVEL*.DAT files, playable in an actual DOSBox install. See docs/level-export-import.md.

4. Custom Graphics Pack Editor

  • graphicspack.html: A standalone tool to author your own DOS-compatible ground/object graphics sets โ€” import/quantize your own art or clone an existing pack, hand-edit on a palette-constrained pixel canvas (pencil/eraser/bucket/eyedropper), and manage object animation frames/trigger boxes.
  • Real Export: Saves for use with this project's own editor/game, or exports as real GROUND<n>O.DAT/VGAGR<n>.DAT files for a genuine DOSBox pack folder. See docs/graphics-pack-editor.md.

5. Deterministic Replay, Rewind & Cheats

  • Input Logging: Every simulation-affecting input (skill selection, release rate adjustments, nuking) is recorded along with the simulation tick it occurred on.
  • URL-based Sharing: Replay records are compressed, encoded, and appended to the URL (?replay=...). The result page offers a "Copy replay link" button to share playthroughs.
  • Rewind: A scrub bar (next to Pause/Fast-Forward) jumps the simulation to an earlier tick by restoring a snapshot and headlessly fast-forwarding the command log back to it โ€” non-destructive while watching a replay, a true rewind/retry while playing live. See docs/rewind.md.
  • Debugging Cheats: Appending ?cheat=1 to the URL loads the level with 99 of every skill available for sandbox testing.

6. WebGL Shaders & Visual FX

  • CRT Simulation: CRT filter overlay including scanlines and vignette shading.
  • GL Shimmer & Lighting: WebGL fragment shaders (src/webgl-effects.js, src/lighting.js) that sample the rendered canvas to create shimmery liquid surfaces and lighting calculations.
  • Screen FX: Skill-triggered WebGL screen distortions โ€” bomber shockwave/shake, basher chromatic aberration, miner jitter, digger squish, blocker shield shimmer, floater turbulence, builder holographic mesh โ€” plus a secret Shift+D tuning popover for live-adjusting effect parameters. See docs/screen-fx.md.
  • Hover Highlight: The Lemming currently hovered by the mouse pointer while a skill is armed is recolored with the same red tint 2-player mode uses for team 1.
  • Particle System: Exploding sparks and liquid particles rendered directly onto the canvas view. See docs/particle-system.md.

7. Headless Level Solver

  • solver.html: Pick any level and run a headless, best-effort Monte Carlo Tree Search (src/solver.js) for a skill-assignment sequence that solves it โ€” no Canary/flag needed to search, only to watch the resulting replay. See docs/solver.md.

8. Amiga ProTracker Audio Engine

  • Chiptune Synthesis: Custom Amiga audio tracker player synthesized in JavaScript (src/AmigaAudioEngine.js) to play retro music formats.
  • Original SFX: Reuses digital audio files (upgrade/lemmings/sounds/*.ogg) for classic sound cues ("Let's Go!", "Oh No!", splats, explosions, and tools).

๐Ÿ“‚ Directory Structure

โ”œโ”€โ”€ src/                  # Modern ES Module codebase (JS + game.css) โ€” no build-time deps beyond Vite
โ”œโ”€โ”€ assets/               # First-party static assets the live game fetches directly: UI chrome
โ”‚                         # (skill toolbar/control-bar icon strips, background.png), the menu's
โ”‚                         # handwriting-style font, and the editor's custom cursor SVGs
โ”œโ”€โ”€ gamedata/             # Original DOS/Amiga game packs (LEVEL*.DAT, GROUND*O.DAT, VGAGR*.DAT,
โ”‚                         # MAIN.DAT, VGASPEC*.DAT, ADLIB.DAT, TANDYSND.DAT, ...), fully committed
โ”‚                         # to this repo โ€” live-decoded at play time/edit time for every rating,
โ”‚                         # including gamedata/Amiga-Two-Player/ (2-player's raw *.lvl source).
โ”‚                         # See docs/gamedata.md and docs/2-player-dos-assets.md.
โ”œโ”€โ”€ levels/               # Shared object/decoration art (levels/{gfx,ani,doors,traps}/*.gif) +
โ”‚                         # levels/music/*.ogg (unused leftover, see CLAUDE.md)
โ”œโ”€โ”€ logos/                # Result-screen rating badge art (complete/failed/holiday93/holiday94/...)
โ”œโ”€โ”€ upgrade/<pack>/       # Optional, non-vanilla polish grouped by gamedata pack (see CLAUDE.md):
โ”‚                         # backgrounds/*.webp, lemmings/sounds/*.ogg (shared SFX), <pack>/music/
โ”œโ”€โ”€ graphics_tools/       # Node scripts used to reverse-engineer/verify the binary formats above
โ”‚                         # (render_*.js/.mjs previews, parity_check_phase*.mjs, lem_decompress.js)
โ”œโ”€โ”€ vite-plugins/         # Custom Vite plugins (gamedataDirListing.js โ€” see vite.config.js)
โ”œโ”€โ”€ scripts/              # One-off maintenance shell scripts (e.g. convert-backgrounds-to-webp.sh)
โ”œโ”€โ”€ docs/                 # Binary format specs + architecture notes โ€” see "Documentation Reference"
โ”œโ”€โ”€ index.html            # Main menu, level selector, and Level Editor mount
โ”œโ”€โ”€ game.html             # Main gameplay viewport and multiplayer lobby mount
โ””โ”€โ”€ *.html                # Standalone dev tools in the root โ€” see "Developer Tools" below

๐Ÿ› ๏ธ Codebase Architecture

The modern ported game is organized as a collection of ES modules located under the src/ folder โ€” no vendored third-party engine code, everything below is native:

Core Application

  • index.html / src/menuMain.js: Initial entry page. Sets up the main menu, difficulty level selector, custom level manager, and initializes the Level Editor.
  • game.html / src/gameMain.js: Handles the main gameplay view, runs the fixed 80ms simulation step loop (TICK_SECONDS = 0.08), and coordinates WebRTC lobby connections.
  • src/entities.js: Contains the Entity-Component-System (ECS) engine. Implements the 8 skill state machines, coordinate systems, step-up (โ‰ค12px) / step-down (โ‰ค8px) walking geometry, and coordinate checks. The state machine itself (stepLemming/applyJob) is DOM-free โ€” DOM writes and sound playback are separated out (syncEntityVisuals, stepLemming's returned sounds array) so it's callable headlessly, e.g. for the rewind feature's fast-forward. See docs/rewind.md.
  • src/terrain.js: Manages the level collision matrix (mapped onto a grid of 0-9 values). Manages drawing the offscreen canvas where Digger, Basher, and Miner actions physically carve paths or Builders place stairs. snapshot()/restore() bundle both for the rewind feature.
  • src/leveldata.js: loadLevelDef() is the single entry point gameMain.js calls to load a level for every rating; it dispatches to gamedataLevelLoader.js, which live-decodes all of them (including 2-player). Also owns Run-Length Encoding (RLE) grid compression/decompression, load-bearing for the level editor's custom-level storage format.
  • src/gamedataLevelLoader.js: Live-decodes a real level directly from gamedata/<pack>/LEVEL<n>.DAT (or DLVEL<n>.DAT for Oh No! More Lemmings) for the 6 DOS packs, and from gamedata/Amiga-Two-Player/Level NN.lvl for 2-player โ€” terrain-piece/object/steel-area placement and metadata โ€” using native src/levelReader.js/src/groundReader.js/src/vgaspecReader.js/src/oddTableReader.js, and bakes the result into one shared levelDef shape the rest of the engine needs no awareness of which pipeline produced. See docs/gamedata.md / docs/2-player-dos-assets.md.
  • src/gamedataPacks.js: Shared per-pack table (filename casing, level-file prefix, which graphic styles/ODDTABLE/VGASPEC a pack ships, and the rating โ†’ {pack, config.json group} mapping) used by every live-decode module.
  • src/groundCatalog.js / src/groundReader.js: Live-decode a graphic style's terrain-piece bitmaps and object animation strips from GROUND<n>O.DAT/VGAGR<n>.DAT โ€” shared by the level editor's palette, the graphics pack editor, and gamedataLevelLoader.js.
  • src/vgaspecReader.js / src/cgaspecReader.js: Decode VGASPEC<n>.DAT/CGASPEC<n>.DAT-backed "special" levels โ€” a fully custom background image instead of tiled ground pieces โ€” for VGA/EGA and CGA video modes respectively.
  • src/lemmingSpriteCatalog.js: Recolors lemming sprites for the EGA/CGA/Tandy Video Mode settings from real per-level/hardware palettes (VGA stays the fixed default palette).
  • src/decorations.js: Manages non-terrain interactive and animated game decorations (e.g., animated entrance hatches, exits, and traps) as real HTML-in-Canvas ECS entities. Decompresses animated GIF frames via WebCodecs (ImageDecoder) into custom offscreen sprite strips to guarantee synchronization with the simulation ticks.

Multiplayer & Controls

  • src/network.js: Interfaces PeerJS/WebRTC. Manages connection heartbeats, packet routing, and connection watchdogs.
  • src/editor.js / src/editorHistory.js: The painting loop, palette options, mouse brushes, undo/redo history, and validation rules for the custom Level Editor. Placed pieces/objects/steel areas are real HTML-in-Canvas ECS entities, not flat canvas drawing.
  • src/editorBake.js / src/levelBakeGeometry.js: Turn the editor's placed-piece lists (or a live-decoded gamedata level) into the flat terrain bitmap + collision grid + levelImages/traps real gameplay consumes.
  • src/datLevelEncoder.js / src/exportDat.js / src/importDat.js: Convert custom levels to/from real, LZ-compressed DOS Lemmings LEVEL*.DAT files โ€” "Export Level Packโ€ฆ"/"Import Levelโ€ฆ" in the My Levels screen. See docs/level-export-import.md.
  • graphicspack.html / src/graphicsPackMain.js / src/groundSetFormat.js / src/customGraphicsPacks.js: Standalone custom graphics pack (ground/object set) authoring tool โ€” paint/import/clone art, then either save it for use in this project's own editor/game (IndexedDB) or export real DOS-compatible GROUND<n>O.DAT/VGAGR<n>.DAT files. See docs/graphics-pack-editor.md.
  • src/replay.js: Handles compressing/decompressing user inputs into URL parameters.
  • src/solver.js: Headless, best-effort level solver (solver.html/src/solverMain.js) โ€” re-simulates a level off-DOM using an event-driven Monte Carlo Tree Search (MCTS) that branches at genuine decision points (spawn, wall, gap, trap) and hands back a replay command log for the best attempt found. See docs/solver.md.
  • src/rewind.js / src/worldSnapshot.js: Reverse playback / rewind โ€” jump the live simulation or a loaded replay to an earlier tick by restoring a one-time snapshot and headlessly fast-forwarding the recorded command log back to the target tick. A "Rewind" scrub bar in the skill toolbar; non-destructive while watching a replay, a true rewind/retry (discards what was going to happen next) during live play. See docs/rewind.md.
  • src/ui.js / src/debugPanel.js / src/effectsTuning.js: Toolbar renderers, keyboard shortcuts (1-8 for skills, Escape to pause, Arrow keys for rate/volume), the pause menu's full settings surface (CRT/Lighting/Particles/Screen FX/Background, Music Engine, Video Mode), and a secret Shift+D popover for live-tuning Screen FX shader parameters.
  • src/settings.js: Wraps localStorage to persist volume levels, sound/music toggles, particle settings, and shader states.
  • src/progress.js / src/progressMp.js: Level unlock trackers in localStorage.

Audio & Visuals

  • src/sound.js / src/musicManager.js: Controls HTML5 audio playback and connects the tracker/hardware audio engines. musicManager.js switches live between 4 Music Engines: the shared Amiga ProTracker .mod rotation, real per-pack AdLib (OPL2) music decoded from ADLIB.DAT, pre-rendered per-pack .mp3 tracks, and real per-pack Tandy 3-voice PSG music decoded from TANDYSND.DAT. See docs/music-engine-naming.md.
  • src/AmigaAudioEngine.js / src/gamedataMusicPlayer.js / src/oplAudioEngine.js / src/tandyAudioEngine.js: Low-level audio synthesis โ€” the Amiga ProTracker player, the AdLib/OPL2 driver (also the source of sfx-explorer.html's sound effects, src/dosSfxMapping.js), and a from-scratch Tandy 1000 PSG driver port (src/tandySfxMapping.js; see docs/SOUND_FORMAT.md / docs/SOUND_FORMAT_ADLIB.md).
  • src/webgl-effects.js: WebGL-based visual shader processors (shimmers, reflections, CRT scanlines, Screen FX distortions), tuned by a family of src/*Tuning.js files (crtTuning.js, waterTuning.js, lavaTuning.js, foamTuning.js, sandTuning.js, moltenTuning.js, iceFloeTuning.js, throwerTuning.js, lightingTuning.js, particlesTuning.js) backing the Shift+D debug popover above.
  • src/lighting.js: WebGL effects for active entities and shadows.
  • src/particles.js / src/fireworks.js: Simulation of explosion fragments and fireworks. See docs/particle-system.md.
  • src/colorPalette.js / src/imageQuantizer.js / src/planarImage.js / src/binaryReader.js / src/datContainer.js / src/datContainerCompressor.js / src/mainDatCatalog.js: Shared low-level building blocks (palette handling, image quantization for custom-art import, planar bitplane decode, binary stream reading, DOS .DAT container packing/unpacking, MAIN.DAT asset catalog) used across the live-decode and dev-tool code paths.

๐Ÿ› ๏ธ Developer Tools & Utilities

The project includes several standalone pages (accessible in the root directory, all wired into vite.config.js's build) designed to examine, author, and process original Lemmings/Oh No! More Lemmings game assets:

  • explorer.html (DOS Lemmings Asset Explorer): A web-based inspector to load and visualize graphics, palettes, levels, and animations decoded live from the original MS-DOS/Amiga .DAT/.lvl files. See docs/explorer.md.
  • converter.html (GFX & Level Converter): No longer a required build step โ€” real gameplay and the level editor both live-decode terrain/objects directly now for every rating. Kept as a standalone tool to export a portable snapshot of a level. See docs/converter.md.
  • graphicspack.html (Graphics Pack Editor): Author custom DOS-compatible ground/object graphics sets โ€” paint on a palette-constrained pixel canvas, manage object animation frames/trigger boxes, save for use in this project or export real GROUND<n>O.DAT/VGAGR<n>.DAT files. See docs/graphics-pack-editor.md.
  • sprite-editor.html (Sprite Editor): A specialized paint program to modify pixel sheets, set anchor offsets, and test individual frame sequences. See docs/sprite-editor.md.
  • gif2sprite.html (GIF to Spritesheet Converter): Converts standard multi-frame animated GIFs into linear horizontal sprite strips suitable for standard CSS step-animations. See docs/gif2sprite.md.
  • solver.html (Level Solver): Pick a level and it runs a headless, best-effort Monte Carlo Tree Search (src/solver.js) for a skill-assignment sequence that solves it, then offers a "Watch replay" link into game.html. No Canary/flag needed to search โ€” only to watch the result play out. See docs/solver.md.
  • assembly.html (Exploded Assembly Viewer): Loads a real DOS level straight from gamedata/ and flies every terrain/object piece into place โ€” a dev viewer sharing its live-decode core with game.html's between-levels piece-flying screen. See docs/assembly.md.
  • sfx-explorer.html (DOS Sound Effect Explorer): Plays each pack's real, live-decoded AdLib/Tandy sound effects side-by-side with the current upgrade/lemmings/sounds/*.ogg set, for building/confirming a nameโ†”index mapping by ear.
  • missing.html (Missing Upgrades): An asset-coverage audit โ€” checks every optional upgrade/ asset (backgrounds/sounds/music) against what's actually on disk and reports what's still missing, with a direct game.html link for every catalog level (including no-title ones, dimmed).
  • graphics_tools/: Node scripts (not browser pages) used to reverse-engineer and cross-verify the binary formats above against real DOS asset dumps โ€” per-format render_*.js/.mjs previews and parity_check_phase*.mjs regression checks; graphics_tools/sound_dispatch_explorer.html is one further, standalone CGALEMMI.EXE sound-dispatch research artifact.

๐Ÿ“œ Documentation Reference

The docs/ directory has both architecture notes and binary format specs:

  • docs/gamedata.md: Which gamedata/ packs back which ratings, filename-casing gotchas, and how live decoding works end to end.
  • docs/2-player-dos-assets.md: How 2-player came to fully live-decode from its raw Amiga .lvl source, reusing the lemmings pack's own (byte-identical) ground/object art.
  • docs/gif-to-gamedata-migration.md: The history of retiring pre-rendered .gif/converter-script level art in favor of live gamedata/ decoding for every rating.
  • docs/editor.md: How the level editor's live-decode palette and ECS placed pieces work.
  • docs/graphics-pack-editor.md: The custom graphics-pack authoring tool's format/verification writeup, including two real decoder bugs found and fixed along the way.
  • docs/level-export-import.md: Exporting custom levels to real, LZ-compressed DOS LEVEL*.DAT files and importing them back โ€” byte-level field mapping, unit-conversion gotchas, and bugs found/fixed along the way.
  • docs/rewind.md: How the lemming state machine became headless-callable, the terrain/world snapshot primitives, and the reverse-playback/rewind feature built on top โ€” live vs. replay semantics, the benchmark behind skipping a keyframe cache, and how it was verified.
  • docs/music-engine-naming.md: The upgrade/<pack>/music/track-NN.{mod,mp3} naming convention the Amiga/MP3 music engines expect, and each pack's real track count.
  • docs/solver.md: How the headless level solver's MCTS search/rollout policy works, its known limitations, and how it was verified.
  • docs/screen-fx.md: The skill-triggered WebGL Screen FX distortions โ€” design choices and the debug-popover tuning surface.
  • docs/particle-system.md: Current state of the environmental particle system and its related shader effects.
  • docs/assembly.md: The exploded-assembly dev viewer and the game's shared level-prepare/piece-flying screen.
  • docs/explorer.md: What the DOS Lemmings Asset Explorer inspects and how.
  • docs/sprite-editor.md: How the Sprite Editor's crop/slice/export pipeline works.
  • docs/converter.md: What converter.html still does now that it's optional.
  • docs/webmcp-tools.md: The WebMCP tool surfaces registered by the graphics pack editor, level editor, and game, letting an agent author/play content with no more capability than a human clicking through the same page.
  • docs/messages.md: Post-level result-message logic and text mappings ported from the original engine.
  • docs/GRAPHICS_FORMAT.md: Reverse-engineered DOS Lemmings graphics format reference (ground/object/VGASPEC/CGASPEC bitmaps, palettes).
  • docs/SOUND_FORMAT.md: Reverse-engineered Tandy 1000 TANDYSND.DAT sound/music driver format.
  • docs/SOUND_FORMAT_ADLIB.md: Reverse-engineered AdLib/OPL2 ADLIB.DAT sound/music driver format.