NOCTURNE

Static site · No framework · Cloudflare Pages

A browser arcade that behaves like a place

Six games sit on the floor and two stay sealed. A back room sits behind a bad address. A pull cord in the corner turns the lights off, a radio in the other corner plays something low, and some things only exist in the dark. What you do in one room shows up in another, because the arcade keeps notes.

Play it · Source

The cabinets

01 · Reflex

PULSE

Drift through purple shards and collect green motes. Every eight motes buys a shockwave. Movement eases toward the cursor rather than snapping to it, so a sudden reversal costs you something.

02 · Puzzle

LATTICE

Rotate conduits until current reaches every lamp. Boards are generated as a random spanning tree and then scrambled, which makes them always solvable and always fully connected.

03 · Memory

ECHO

Repeat a growing sequence of pads. The nine tones are a pentatonic scale, so a wrong answer still sounds like music instead of a buzzer.

05 · Stealth

VEIL

Sweeping searchlights read motion, not position. Fans mount on the walls and bars slide the whole floor, so one side of the room is never safe. Sprint through a beam and exposure spikes; hold still and it still climbs, slower.

06 · Precision

SPLICE

Two columns of colored terminals. Match every pair before the panel’s clock runs out — a clean splice buys a sliver of time back, a wrong guess costs much more than that.

07 · Reflex

WYRM

Classic snake, re-skinned as a glowing trail on a dark grid. Every mote eaten grows the trail and tightens the tick rate.

04 · Sealed

MOTH

Climb toward a lamp while your glow burns down. The glow is also your draw radius, so running low on light means you cannot see the next beam coming.

08 · Sealed

STATIC

Hold a drifting station until it locks. Each lock makes the next band thinner and faster. Leave the band too long and the signal dies. Unlocks once VEIL, SPLICE, and WYRM each have a real score — no relation to the moths.

Unlisted · Puzzle

CIPHER

A crossword whose answers are already in the building. Lives in the back room, which the 404 door opens onto.

Unlisted · Search

SEEK

A letter field of words the arcade already named. Drag a straight line in any of eight directions.

Unlisted · Memory

TWIN

Sixteen tiles, eight pairs. A miss turns back over. Fewer turns score higher.

MOTH is not reachable on a first visit. It opens once all eight hidden moths have been found. STATIC is a second, unrelated secret: it opens once VEIL, SPLICE, and WYRM each have a real score. CIPHER, SEEK, and TWIN are a third secret: they live at /back.html, and the 404 corridor door is how you get there.

The radio

A small FM tuner fixed to the bottom-left corner of every page. It plays Icecast streams from Nightride FM through HTML5 audio. Clicks to other rooms on the same origin swap the page in place so the player is not torn down. A refresh, a new tab, or the back button still starts it off, because browsers will not autoplay with sound before a gesture. Station and volume persist in localStorage under nocturne.radio.station and nocturne.radio.volume.

What makes it feel like one building

Most of the work is not in the games. It is in the thin layer above them that gives four separate pages a shared memory.

Three values persist in localStorage, and every page reads all three:

Key Holds Visible effect
nocturne.lights Lit or dark The pull cord changes the whole site, not one page
nocturne.moths Which moths are caught Drives the fourth cabinet
nocturne.best.<game> High scores The arcade floor reads them back onto the cards

nocturne.js owns all three. The games never touch storage directly — they call Nocturne.submitScore(), Nocturne.revealMoth(), and Nocturne.toast(), and get persistence, notices, and the unlock chain for free.

flowchart LR
  A[Pull cord] -->|writes lights| S[(localStorage)]
  B[Catch a moth] -->|writes moths| S
  C[Finish a run] -->|writes best score| S

  S --> D[Arcade floor: cabinet cards and the 4th cabinet]
  S --> E[Every page: lighting state]
  S --> F[Game pages: best score in the HUD]

Lighting is CSS, not JavaScript

data-lights="out" on <html> swaps one block of custom properties. Every rule that differs between lit and dark reads from those properties rather than hard-coding a colour, so the entire site changes with a single attribute.

That design is what lets the cord do more than theming. Several of the eight moths are rendered only in the dark, which makes the cord a search tool.

NoteThe flash-of-lit-room problem

An inline script in each page’s <head> restores the lighting state before first paint. Waiting for nocturne.js to load would show a lit room for a frame or two to someone who deliberately turned the lights off — on every single navigation. It is three lines, and skipping them undoes the effect.

The moths

Eight, gated several different ways. The gating is the tutorial: finding them teaches you what the site can do.

Moth Where How it is gated
sign Arcade floor, resting on the neon sign Visible, deliberately low contrast
cellar Arcade floor, in the footer Lights out only
pulse PULSE, on the screen bezel Lights out only
lattice LATTICE, corner of the board Clear one board
echo ECHO, below the pads Reach round 5
about About page, in the footer Lights out only
back Back room, by the sign Lights out only
wyrm WYRM, on the screen bezel Grow to length 10

Two CSS classes keep those gates apart. .moth--hidden is revealed by darkness; .moth--locked stays invisible whatever the lights are doing, until a game calls revealMoth(). Using one class for both would have let a player collect the earned moths just by pulling the cord.

Catching the eighth rewrites the fourth cabinet in place — no reload — and opens /games/moth.html.

Arriving early

That URL is guessable, and that is fine. Someone who goes looking for it before finding the moths gets a SEALED panel that says how many they have and roughly where to look, rather than a redirect or a wall. Rewarding curiosity with information is the point.

Other things worth clicking

  • The 404. Any bad URL lands you in a dark back corridor. Your cursor is a torch, there is exactly one door, and finding it opens the back room.
  • The Konami code. Works anywhere. Does nothing useful.
  • The greeting. The line under the sign reads your clock, so it is not the same text at 3am as it is at noon.

How it is built

No framework, no bundler, no build step. The whole site is HTML, CSS, and a handful of small JavaScript files on Cloudflare Pages.

site/
  index.html          Arcade floor
  about.html          What it keeps, plus hints
  404.html            The back corridor; the door opens `/back.html`
  back.html           Hidden room for CIPHER, SEEK, and TWIN
  games/              One page per cabinet
  assets/css/site.css Both lighting states
  assets/js/
    nocturne.js       Lighting, moths, scores, toasts, Konami
    radio.js          The corner radio, and in-site navigation that keeps it playing
    pulse.js  lattice.js  echo.js  mothgame.js
    veil.js  splice.js  wyrm.js  static.js
    cipher.js  seek.js  twin.js

Each game owns a fixed logical coordinate space — 800×600 for PULSE and MOTH, 640×640 for LATTICE — and the canvas backing store is scaled by devicePixelRatio. Nothing in any simulation thinks in screen pixels, so the games behave identically at every window size.

Deployment

Cloudflare Pages project nocturne. A push to main that touches site/ uploads that folder with wrangler pages deploy. The custom domain is nocturne.ethandbard.com.

Accessibility

Every interactive thing is a real <button> or <a>, so the moths and the ECHO pads are keyboard-reachable and announced by screen readers rather than being decorative <div>s with click handlers.

prefers-reduced-motion disables the sign flicker and the pad transitions. The games themselves need a pointer or the arrow keys; none is playable by keyboard alone on a touch device.

Nothing breaks if localStorage is unavailable. Every read and write is wrapped, and the failure mode is an arcade that forgets you between pages.