EntLab

How it works

There is no JavaScript version of EntLab. The website ships the actual Python program and a Python interpreter, and runs them in your tab.

Who wrote this

EntLab was written by Claude, Anthropic's AI, working from Jonah Feinberg's direction. Every Python module, this website, the documentation you're reading, and the build tooling were written by Claude across a series of sessions. Jonah set the goals, made the design calls, tested it, and said what to change. That seems worth saying plainly rather than leaving you to work it out from the commit history.

The shape of the thing

Two folders. One is an ordinary Python project; the other is a static website you can drop on any host.

EntLab/                  the program
  main.py                entry point, for the browser build as well
  entlab/                app code: world, view, menus, widgets, rulefile, …
  rulesets/*.py          the nine bundled rules
  tests/                 headless checks (python3 tests/run_all.py)
  packaging/             icons, PyInstaller spec, build_web.py

EntLab Website/          the site (drag this into Netlify)
  index.html             home page          ┐ hand-written
  docs/*.html            these pages        │ plain HTML/CSS/JS,
  style.css  site.js     shared             │ no framework, no build step
  logo.svg               ┘
  app/                   GENERATED: EntLab compiled to WebAssembly

So / is a normal website and /app/ is the program. The site half is deliberately boring: plain files a browser already knows how to show.

What happens when you press Launch

  1. The browser loads app/index.html, the loader page made by pygbag. That is the black "Loading, please wait…" screen.
  2. It fetches a CPython 3.12 interpreter compiled to WebAssembly from a CDN (main.wasm plus main.data). This is the roughly 15 MB, and it is cached from then on.
  3. The interpreter starts and unpacks entlab.apk, which is a ZIP of main.py, entlab/ and rulesets/, into a filesystem held in memory. As far as Python is concerned, those are now real files.
  4. It reads the # /// script block at the top of main.py to learn what the program needs, and downloads those wheels from the same CDN: pygame-ce and NumPy.
  5. It runs main.py as __main__, the same file you would run in a terminal.

From there it is the program itself, unchanged. main.py notices it is running under Emscripten, skips the pip install step the desktop version does on first run, and starts the app.

The one structural change

A desktop game loop is while True:. In a browser that locks the tab forever, because JavaScript never gets control back to paint anything or handle a click.

So the app loop runs the same frame function as the desktop build, but yields once per frame:

async def run_async(self):
    while self._frame():
        await asyncio.sleep(0)      # hand the frame back to the browser

One quirk worth knowing: under pygbag, asyncio is pygbag's own scheduler. asyncio.run() doesn't block. It puts the coroutine on a requestAnimationFrame loop and returns straight away.

pygame draws into an SDL surface, and Emscripten blits that to a <canvas> filling the window.

Fitting the window

A browser tab has no window manager, so nothing sends the resize event a desktop app waits for. The app reads window.innerWidth / innerHeight directly and re-checks a few times a second, re-taking the display mode when they drift apart. It also subtracts the height of the EntLab bar at the top of the page, so the board is never hidden underneath it.

Gestures the browser gets wrong

Two input paths needed building by hand.

Trackpad pinch. Every browser reports a pinch as a scroll event with the Ctrl flag set, without ever sending a real Ctrl keypress. SDL only knows about keys it actually saw, so the app read the gesture as an ordinary scroll and panned instead of zooming. A small piece of JavaScript now catches those events, stops the page itself from zooming, and passes the app the amount to zoom by.

Touchscreen pinch. On a phone this isn't a scroll event at all. SDL turns your first finger into a mouse drag, which is what lets you draw, but the second finger is dropped, so there was no way to zoom or pan. The same code now tracks both fingers, turns them into zoom and pan, and tells the app to throw away the stroke your first finger had already started, so pinching doesn't smear a line across the board.

On a touch device the app also switches to a chunkier layout: a taller tool row with finger-sized buttons, and a toolbar that wraps onto extra rows instead of running off the edge of a narrow screen.

What differs from the desktop app

  • Files don't stick around. The filesystem lives in memory, so worlds, settings and logs are gone when you reload. Download a .grid if you want to keep it.
  • The first visit is slow and later ones aren't, because that 15 MB is cached.
  • It runs slower than the desktop app, since everything goes through WebAssembly. The simulation gets a smaller slice of each frame here so the interface stays responsive under a heavy world, and the status bar shows the rate it is really managing.

Everything else (the menus, the tools, the keybinds, the rulesets, the history scrubber) is literally the same code.

Building and deploying

One command regenerates the compiled half. It only ever replaces app/, so the home page and these docs are never touched:

python3 packaging/build_web.py

It copies main.py, entlab/ and rulesets/ into a temporary folder, runs pygbag over them, moves the result into the website folder, and restyles pygbag's loader page to match the site.

Deploying is dragging the EntLab Website folder onto Netlify Drop. About a megabyte of static files; no server, no build step, no configuration. GitHub Pages and Cloudflare Pages work identically.

The test suite unzips the built archive and compares every module against the project byte for byte, so an out-of-date build fails loudly instead of quietly shipping old code.