Skip to content

Add play.html: the whole game in one file you can open from a disk - #4

Merged
cursor[bot] merged 2 commits into
mainfrom
cursor/one-file-offline-build-6a10
Aug 13, 2026
Merged

Add play.html: the whole game in one file you can open from a disk#4
cursor[bot] merged 2 commits into
mainfrom
cursor/one-file-offline-build-6a10

Conversation

@BruceMoseti

Copy link
Copy Markdown
Owner

Why

Right now there are two ways to play, and both need something first: the published page needs GitHub Pages switched on (a repository setting no workflow token can set), and make play needs a terminal and a server. There is no way to just open the game.

Double-clicking web/index.html does not work and cannot be made to work: the page is deliberately unbundled plain ES modules, and a browser refuses a module graph over file://. That property is the reason the page needs no toolchain, so it is worth keeping — but it means the repository should ship the one artefact that does open from a disk.

What this is

play.html at the repository root: the same markup, with the stylesheet, the measured figures and all nine modules inlined. 123 kB, no network, no server. Download it and double-click.

play.html opened straight from the filesystem, rendering the full interface

It is a concatenation, not a bundle — no esbuild, no toolchain, in keeping with the rest of web/. That is only legal under three conditions, so web/build/standalone.mjs checks them rather than assuming them: every import is named and static, the graph is acyclic, and no two modules declare the same top-level name. If any stops holding, the build fails naming the offending module and symbol instead of emitting a file that loads and then misbehaves. Renaming imports (canPlaceCue as canPlaceCueAt) become explicit bindings, and import.meta or a dynamic import() anywhere in the graph is rejected outright.

The figures in the prose used to be fetched from web/data/facts.json, which file:// will not read, so the build inlines them into window.__measuredFacts and loadFacts prefers that when present. The served page is unchanged: it still fetches, and its facts still get checked by site_facts.py --check.

How it is kept honest

A stale copy of the game is worse than no copy, so CI does not trust it:

Step What it catches
standalone.mjs --check play.html drifting from web/ — rebuilds and compares byte for byte
browser.mjs --url file://…/play.html the inlined copy failing to load, paint, or finish a game off the disk
input.mjs --url file://…/play.html pointer capture, the drag threshold and the keyboard breaking under file://

Verified locally: the offline file plays complete games (7 and 11 shots) with no console errors or failed requests, passes 15/15 interaction checks, and the served page still plays clean after the facts.js change. The freshness check was confirmed to fail on a deliberately edited play.html, not just to pass.

make standalone rebuilds it and immediately plays it from a file.

Also here

.gitignore had build/, which was meant for setuptools' ./build but also matched web/build/, where this script lives. Anchored to /build/.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 13, 2026 00:46
setuptools writes ./build, but the pattern also matched web/build, which is
source. Anchoring it keeps the artefact ignored and the source trackable.

Co-authored-by: BruceMoseti <BruceMoseti@users.noreply.github.com>
The page is deliberately unbundled, which is why it needs no toolchain and
also why it cannot be opened from a disk: a module graph over file:// is
refused, so double-clicking web/index.html gets you a blank page. Until a
published copy exists there is no way to play without running a server.

play.html is the same markup with the stylesheet, the measured figures and
all nine modules inlined. It is a concatenation rather than a bundle, which
is legal for reasons the build checks instead of assuming: the imports are
named and static, the graph is acyclic, and no two modules declare the same
top-level name. Any of those failing stops the build with the offending
name rather than shipping a file that loads and then misbehaves.

It is generated, so CI rebuilds it, compares it against the committed copy,
and then plays a full game and the whole input suite from file:// — a copy
of the game that has quietly rotted is worse than no copy.

Co-authored-by: BruceMoseti <BruceMoseti@users.noreply.github.com>
@cursor
cursor Bot merged commit e97e3ab into main Aug 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants