Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PERIPLUS

a survey of one small world — a game-jam entry on the theme explore.

Needs WebGL2, a keyboard and a mouse. No install, no server, and no network calls once the page has loaded.


A periplus is the older thing a map is made of: a list of what you passed, in the order you passed it, written by someone who was actually there. This one is blank.

You are set down on an unmapped moon with an instrument that writes down only the ground you personally stand next to. Everything else — every hill, coast and mountain range you can see from here — is drawn as what it currently is to you: contour lines, index contours and a ten-degree graticule, floating on the real geometry. Walk toward it and it becomes ground: colour, grass, trees, surf.

The moon does not rotate. There is a permanent day side and a permanent night side, and the walk between them is the point.

Eight cairns are out there, stacked by the surveyor who came before you. They stopped surveying at ninety-one percent. The cairns say why.

orbit landfall
An unwalked moon is pure contour line. Landfall.
nightside shelf
The night side — glowing contours are unwalked ground. A coastal cairn site.

Controls

W A S D walk
mouse look (click the canvas to capture the cursor)
Shift run
Space hop / paddle
scroll camera distance
J journal
M enlarge the chart
H hide the interface
Esc release the cursor

Water is swimmable. The whole moon is reachable on foot.

Running it locally

Development — unbundled ES modules, an importmap, and a vendored three.js. This is the version worth reading.

npm install
npm run dev            # → http://localhost:5173/

Single file — everything inlined, three.js included, ~894 kB. This is also what the live site serves, and it opens straight off a file:// URL.

npm run build          # → docs/index.html

How this was made

Built by Claude Opus 5 in Claude Code, in one session, from four human messages. Nothing else was specified. The prompts are reproduced verbatim below with what I took each one to mean — including where I read something into them that was not literally there.

Prompt 1 — the brief

https://www.anthropic.com/news/claude-opus-5 - "Finally, Opus 5 is capable of producing much stronger visual outputs:" https://assets.claude.ai/brand/artifacts/blog/opus/5-aeolus-demo.html & https://assets.claude.ai/brand/artifacts/blog/opus/5-sectio-demo.html#cell --- so, with that in mind, make me a small "gamejam" game. you're not allowed to ask me for clarification or direction, you have to do this start to end yourself. The theme of the game jam is "explore" which means you'll have to make it so the player can explore, and then give them something to explore! And the only other requirement I have is that it's using 3D, because I want to see Opus 5 do 3D stuff. OKAY; you have 1 million tokens budget, Opus 5 live, bypass permissions on, and a good number of MCPs live, like Chrome. I'll check on the results tomorrow.

Hard constraints extracted: a small game-jam-scale game; theme explore; 3D; no clarifying questions; finish end to end unsupervised.

How I read the two demo links. These were the only art direction on offer, so I fetched both and read their actual CSS rather than guessing at it — with curl, not the markdown converter, because the converter strips the stylesheet and the stylesheet was the entire reason for looking. What I took from them: dense design-token theming, precise letter-spacing, small type (10–13px), monospace reserved for anything numeric, one restrained accent against a dark ground, and code that explains why in its comments. I did not copy their palette — those are instruments and this is a game — but the discipline is theirs. It is where PERIPLUS's two-ink rule comes from: cyan is the instrument, amber is the lantern, nothing else gets a hue.

How I read "explore". The weak reading of this theme is a big map with collectibles on it. I wanted exploring to be the mechanic rather than the setting, so the world is literally unfinished until you walk it and walking is the only thing that can write to it. The chart in the corner and the ground under your feet read from the same canvas, so there is no gap between "the map fills in" and "the world becomes real" — they are one operation. That single decision generated nearly every other one in the project.

How I read "3D". As show me real 3D work, not use a 3D library. Hence: a sphere you can walk all the way around with gravity toward its centre; a terrain field evaluated as a pure function of direction; an equirectangular projection painted at runtime and sampled in-shader; an analytically ray-traced planet and ring system in the sky; and movement carried in a parallel-transported tangent frame so that the poles are places you can actually stand.

How I read "small". As a real constraint rather than a hedge. One mechanic, one world, eight destinations, ~4,300 lines. I deliberately did not add caves, weather, crafting, or a second moon.

How I read "you're not allowed to ask for clarification". Every fork became mine to settle, so I settled them by measurement rather than taste wherever that was possible — throwaway Node probes that reported the land/sea ratio, the biome distribution and the cairn spread before any of it was wired to a renderer. About a dozen numbers in src/config.js were set that way instead of by eye.

Prompt 2 — sent mid-task

(note you may have to wait for Chrome MCP access, other claude code sessions may use it today; but it's temporary. You should be temporary too, when you're done with Chrome, free up the Chrome MCP for other sessions)

Interpretation: a courtesy constraint on a shared resource. It ended up mattering more than it looks. The Chrome MCP was held by another session for most of the build, and it went unavailable at exactly the point I needed a browser to diagnose a black screen.

Rather than block on it, I wrote my own CDP driver — about seventy lines against Node's built-in WebSocket — pointed at a separate headless Chrome on its own profile, and never contended for the shared instance again. That harness produced every screenshot in this README and is the reason the game could be tested at all: navigate, wait, evaluate arbitrary JS in the page, screenshot, read the console. It is also what made the window.PX debug surface worth building.

Prompt 3 — the check-in

You're at 47% context; what more do you want to do

Interpretation: neither "stop" nor "keep going" but a request for an honest assessment of what was still unverified. So I led with the actual gap instead of a feature list: I had never played it. Everything to that point was headless screenshots, which meant two entire systems had never once executed.

That produced four targeted verifications, all of which passed:

  • Audio — ~260 lines that had never made a sound, because headless Chrome blocked the AudioContext on every run. Re-ran with autoplay overridden, then tapped the master bus with an AnalyserNode to confirm real signal (peak 0.46, RMS 0.14) rather than merely nodes constructed without throwing.
  • Mouse-look — 100 px of movement yields exactly 13.2°, and after 220 look events the heading vector is still unit-length and tangent to the sphere within 1e-6. That last check is the one that matters: it demonstrates the tangent frame does not drift, which was the whole reason for building it that way.
  • Compass bearings — checked against cases with known answers (N=0°, E=+90°, S=180°, W=−90°). A sign flip here would have sent "north" south, and no screenshot would ever have caught it.
  • Pole traversal — walked from 88.6°N over the pole to 75.4°N with no NaN anywhere, exercising both the parallel-transport frame and the pole-fill branch of the survey painter.

Prompt 4 — publish it

Okay, your next task is to make this into a public repository on my personal github account, TonyDowney, and find a way to make the game playable via the internet. I typically spin up a firebase project for free Spark hosting but I don't particularly care; free and stable is the way to go. Make sure the repo includes a README that goes over all human prompts made to create this game, and how you interpreted it. And for reference, we're at 48% context of the million tokens; well under.

Interpretation of "free and stable": Firebase Spark was offered but explicitly not required, so I chose GitHub Pages served from /docs. It lives in the same repo, needs no second project and no separate CLI login, and has no credential that can quietly expire.

I deliberately did not build it in GitHub Actions. Two reasons: the available token carried no workflow scope, and a committed artefact cannot fail to deploy because a runner image changed underneath it. npm run build regenerates docs/index.html, and that one file is both the live site and the thing you can download and open offline.

One thing this prompt made necessary that the game had not previously needed: a public link attracts phone traffic, and this is a pointer-lock-and-WASD game. The title card now says so on touch devices rather than letting somebody download 894 kB and only then discover they cannot walk.


How it works

The moon is a pure function. src/field.js answers one question — given a direction from the centre, what is here — and every other system asks it: the mesh, the prop scatter, the chart, the ground under the walker's feet, the noun in the readout. There is no baked heightmap anywhere. Domain-warped fBm makes the continents, offset ridged noise carves ranges and basins into them, and e²/(e²+k) maps elevation to displacement — chosen because its slope at the waterline is zero, which is where flat, walkable beaches come from for free.

The survey is a canvas. src/survey.js owns a 1024×512 equirectangular 2D canvas that starts black. Standing somewhere paints a soft white disc onto it — as an ellipse, because equirectangular projection stretches by 1/cos(latitude) and a circle on the moon is not a circle on the chart. That canvas is uploaded as a texture and every shader in the scene samples it to decide whether the ground it is drawing is real yet. The same canvas, multiplied against a baked shaded relief, is the chart in the corner. One source of truth: if it is inked on the chart, it is coloured in out there.

The horizon set the design. On a 155-unit sphere the horizon at eye height is about 27 units away, which is why the survey radius is 15 and not 40 — any larger and the reveal would consume everything visible and the mechanic would erase itself. Climbing is how you buy sight: from a 20-metre summit you can see 95 units, and almost all of it is still a drawing.

Lighting is geography. The sun is fixed in world space, so the sky is a function of where you are standing rather than of what time it is. Terrain, sea, props and the sky dome all resolve their distance haze by calling the same skyBase() — the fog is never a colour someone picked, it is literally the sky in that direction, which is why the horizon dissolves instead of ending.

Everything is generated. No textures, no models, no audio files. The gas giant KEL, its banding, its storm, its ring and the ring's shadow are ray-traced against the sky dome in a fragment shader. Sound is oscillators and shaped noise through one synthesised reverb, and the chord drops a fourth on the night side.

Layout

src/
  config.js    every number that changes how it feels
  noise.js     Perlin 3D, fBm, ridged multifractal
  field.js     the moon as a pure function
  planet.js    icosphere, terrain material, sea
  glsl.js      shared GLSL: sky, ground palette, chart, reveal
  sky.js       gradient, stars, aurora, KEL and its ring
  survey.js    the discovery canvas and the baked relief
  props.js     five instanced meshes, ~22000 objects, 5 draw calls
  cairns.js    site briefs, placement search, the markers
  player.js    walking on a sphere; camera
  ui.js        readouts, chart, compass, journal
  audio.js     synthesised everything
  postfx.js    bloom, SMAA, tone map, grade
  main.js      boot, loop, glue

window.PX exposes the live scene in the console. PX.teleport(lat, lon) and PX.goTo(n) are the useful ones — most of what is worth looking at is on the night side, four hundred metres away.

Three bugs worth writing down

Two were traps in three.js. The third was mine, and it is the most instructive.

  • UnrealBloomPass cannot read from a multisampled composer target. It reads readBuffer.texture and then blends its result additively back into readBuffer. On an MSAA target three resolves the buffer to serve that read and then calls invalidateFramebuffer on the multisample colour attachment — so the scene is gone before the bloom lands on it, and the frame comes back black with only the bloom in it. SMAA does the antialiasing here instead.
  • LightShadow never calls updateProjectionMatrix(). Assigning left/right/top/bottom on a directional light's shadow camera does nothing on its own; the frustum silently stays at its default ±5 and only the ground directly beneath the player ever receives a shadow.
  • The prop scatter put all ~10,000 objects in the northern hemisphere. Candidates come off a Fibonacci spiral, which runs pole to pole — so visiting it in index order filled every per-type cap out of the first band of latitude it crossed, then rejected the entire rest of the moon. The landing site was bare and the nearest tree was 104 metres away. Shuffling the visit order fixes it, and the caps now thin the population uniformly instead of clipping it geographically. A screenshot showed the symptom; only measuring the distance to the nearest instance of each type found the cause.

Turning the dials

src/config.js is the whole surface. seed makes a different moon. landBias is the land/sea ratio. revealRadius is how much of the world you are allowed to make real at once — the single most load-bearing number in the game.


Built with three.js. Everything else is in this repo.

About

A game-jam entry on the theme 'explore'. Walk an unmapped moon — the ground is only contour lines until you stand next to it. Procedural 3D, no assets, one HTML file.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages